kakts-log

programming について調べたことを整理していきます

ansible commandとshellモジュールの違い

ansibleで使って対象ノードで特定のコマンドを実行する際、
デフォルトで用意されているcommandモジュールとshellモジュールを使うことができます。
環境変数や、パイプを使ったコマンドを実行させる場合 この2つは挙動が異なるのでまとめてみました。

command モジュール

まずはcommandモジュールについて
command - Executes a command on a remote node — Ansible Documentation

The given command will be executed on all selected nodes. It will not be processed through the shell,
so variables like $HOME and operations like “<”, “>”, “|”, “;” and “&” will not work (use the shell module if you need these features).

commandモジュールはシェルを介して実行されないため、 環境変数($HOMEなど)や、 “<”, “>”, “|”, “;” などのパイプやリダイレクトは使えません。
commandモジュールの意図として、ユーザ環境に依存せずにセキュアにコマンドを実行することがあげられます。
ユーザ環境変数や、パイプ・リダイレクトを使った処理を行いたい場合は shell モジュールを利用するように公式のドキュメントにも書かれています。

If you want to run a command through the shell (say you are using <, >, |, etc), you actually want the shell module instead. The command module is much more secure as it’s not affected by the user’s environment.

shell モジュール

shell - Execute commands in nodes. — Ansible Documentation

commandモジュールの章でも説明したとおり、このshellモジュールは、指定したコマンドをシェル(/bin/sh)を介して実行します。
実行環境に依存した環境変数や、パイプ・リダイレクトを使った処理を実行できます。
これは悪い点もあり、もし実行時に playbookないで変数を使っていて、その変数が意図しない者の場合、そのまま対象ノードに対して実行されてしまうので危険です。
shell moduleで変数を使う場合は、サニタイズのために “{{ var }}” のかわりに “{{ var | quote }}“ を使うべきです。

shell commandモジュール双方のデメリット

qiita.com 上記の記事に、この2つの問題点が書いてあり、読んでみましたが、概ね同意です。
この2つのモジュールをつかってplaybookを書く場合 コマンド実行による冪等性の保証が困難であり、
playbookを読んだだけではシステムの本来あるべき姿(ansbile playbookは本来システムのある「べき」状態を記述するのが正しいはず)がわからなくなること、
そして実行処理は本来シェルスクリプトに任せればよいと思います。
playbookを書く際には他のモジュールを調べた上で、どうしても他のモジュールでは記述できないときにだけ、このshell commandモジュールを使えばよいかと思います。

iostatでのディスクI/Oの指標を見る

iostatとは

「I/O statics」の略称 iostatコマンドを使って、ディスク毎のI/O統計を集計して ディスクI/Oの使用率・飽和度を示す指標をみることができます。
利用シーンとしては、主にディスクI/O関連の調査を行う場合に使われます。

主なオプション

iostatを実行する際の主なオプションは以下のとおりです。

オプション 説明
-c cpu統計を表示
-d ディスク統計を表示
-k 表示サイズ変更 ブロック数(512バイト)→ kb
-m 表示サイズ変更 ブロック数(512バイト)→ mb
-p 使用されているブロックデバイスと、そのパーティション毎の統計を表示する
-x 通常項目に加え、いくつかの拡張統計を表示する

デフォルトでは、 -c -dが有効になっていて、cpu統計・ディスク統計を見ることができます。
具体的には以下のような感じで各統計データを見ることができます。

$ iostat
Linux 2.6.32-431.17.1.el6.x86_64 _x86_64_   (2 CPU)

#cpu統計
avg-cpu:  %user   %nice %system %iowait  %steal   %idle
           0.02    0.00    0.02    0.03    0.00   99.92

#ディスク統計
Device:            tps   Blk_read/s   Blk_wrtn/s   Blk_read   Blk_wrtn
vda               0.46         2.63         9.55    2239142    8134170
vdb               0.00         0.00         0.00       1754          8
dm-0              1.27         2.61         9.47    2221322    8069280
dm-1              0.00         0.01         0.01       6288       6392

各項目の値の示す意味はman iostatで調べることができるので詳細を知りたい場合は読んでみると良いかもです。
ディスク統計の方の各項目について説明すると以下の通りになります。

項目 説明
tps 秒間あたりのトランザクション
Blk_read/s Blk_write/s 秒間あたりに(read | write)されたブロック数
Blk_read Blk_write read | writeの合計数

ブロック数の単位を変えたい場合は -m -kオプションを使って変えることができます。

-x オプションをつかって詳細なI/O統計を表示させる

iostat実行時に-xオプションを付けることで、より詳細なディスクの統計データを表示できます。

# ディスクの詳細な統計データを表示
$ iostat -dx

Device:         rrqm/s   wrqm/s     r/s     w/s   rsec/s   wsec/s avgrq-sz avgqu-sz   await r_await w_await  svctm  %util
vda               0.01     0.87    0.06    0.41     2.62     9.52    26.17     0.00    6.71    2.31    7.30   1.71   0.08
vdb               0.00     0.00    0.00    0.00     0.00     0.00     8.39     0.00    1.22    1.00   49.00   1.22   0.00
dm-0              0.00     0.00    0.06    1.21     2.60     9.44     9.50     0.10   75.09    2.88   78.71   0.63   0.08
dm-1              0.00     0.00    0.00    0.00     0.01     0.01     8.00     0.00   45.32    2.91   87.04   0.90   0.00
項目 説明
rrqm/s 秒間あたりに読み込みキューに登録され、マージされた読み出し要求数
wrqm/s 秒間あたりに読み込みキューに登録され、マージされた書き込み要求数
r/s 秒間あたりにディスクデバイスに発行された読み出し要求数
w/s 秒間あたりにディスクデバイスに発行された読み出し要求数
rsec/s 秒間あたりにディスクデバイスから読み出されたセクタ数
wsec/s 秒間あたりにディスクデバイスから書き込まれたセクタ数
await I/O応答平均時間(ミリ秒) ドライバの要求キーで待っている時間と実際のI/O応答時間を合わせたもの
r_await awaitデータの読み込み処理のみのデータ(ミリ秒)
w_await awaitデータの書き込み処理のみのデータ(ミリ秒)
%util バイスがI/O処理をしていたことによるビジー状態の時間の割合(使用率)

この中で、もっとも重要な指標は awaitで、要求が発行されてからI/Oの応答が終わるまでの時間を表すものです。
%utilも重要な指標ですが、あくまでビジー状況の計測値を表すもので、複数ディスクからなる仮想デバイス環境ではあまり参考にならない指標と言われています。

rrqm/s wrqm/sの値は、パフォーマンス向上のために、複数の読み込み・書き込み要求が1つにマージされていることを表す指標で、
シーケンシャルなワークロードが処理された兆候を表します。

参考

詳解 システム・パフォーマンス

Understanding iostat (Example)

https://stackoverflow.com/questions/4458183/how-the-util-of-iostat-is-computed

Interpreting iostat Output - Server Fault Blog

Ansible でjenkins用サーバをセットアップする

macOSのPCから特定のサーバに対してansibleをつかってパッケージをインストールする方法を解説します。
ここでは、特定のサーバに対してjenkinsとsvnをインストールし、起動させるためにansible playbookを書いていきます。

ansibleのインストール

macでのansibleのインストールは下記にまとまっており、pipを使います。 Installation — Ansible Documentation

今回は、せっかくなので最近でた最新版のansible2.3.0をインストールします。

www.ansible.com

2.3.0はsshでのコネクション周りで大きく改善されており、ansibleの実行速度がかなり改善されているようです。

#pipをインストールする
$ sudo easy_install pip

# pipをつかってansibleをインストールする
$ sudo pip install ansible

playbookを作成

ansibleを実行する際 特定のグループまたは単一のサーバに対してplaybookとよばれるYAML形式のファイルで サーバにインストールするパッケージや起動するプロセスなどを指定することができます。 今回は、単一のサーバに対してplaybookを作るため、シンプルに下記の構成でファイルを作っていきます。

$ tree
.
├── README.md
├── hosts                           // サーバグループとipを設定する
├── jenkins-playbook.yml // jenkinsサーバ用のplaybook

ここで今回設定するサーバのホスト名を"jenkins"として設定します

hostsファイルの設定

ホスト名と実際のサーバのipのひもづけを hostsファイルにて設定します。
今回は単一のサーバに対してのみなので、ipは1つのみ設定します。 ipを192.168.0.1として 下記のように設定します。

./hosts

[jenkins]
192.168.0.1

playbook

ここでは指定したサーバに対して jenkins(最新の安定バージョン)とsvn(1.7.22)をインストールした上で jenkinsを起動させるplaybookを書きます。

./jenkins-playbook.yml

----
- hosts: jenkins
  remote_user: ${YOUR_ACCOUNT}
  tasks:
    - name: Install svn1.7.22 
      become: yes
      yum:
        name: http://opensource.wandisco.com/rhel/6/svn-1.7/RPMS/x86_64/subversion-1.7.22-1.x86_64.rpm
        state: present

    - name: ensure open JDK 1.6 is at the latest version
      become: yes
      yum:
        name: java-1.6.0-openjdk
        state: latest

    - name: get the Jenkins repository
      get_url:
        url: http://pkg.jenkins-ci.org/redhat-stable/jenkins.repo
        dest: /etc/yum.repos.d/jenkins.repo

    - name: add the Jenkins repository
      rpm_key:
        key: http://pkg.jenkins-ci.org/redhat-stable/jenkins-ci.org.key

    - name: ensure jenkins is at the latest version
      become: yes
      yum:
        name: jenkins
        state: latest

    - name: ensure jenkins is running and enabled
      become: yes
      service:
        name: jenkins
        state: started
        enabled: yes

ansibleのplaybookでは、tasks項目で上から設定した順にサーバに変更が適用されていきます。
ここでは最初にsvnの特定のバージョンをrpm指定でyumインストールさせます。

最初の項目でyumと書いていますがこれはansibleのyumモジュールを使うことを意味しています。 yum項目の配下にnameやstateを指定していますが、ここで特定のパッケージや、そのパッケージがどういう状態にしたいか(最新版になっている、removeされているなど)
を指定することができます。
yum - Manages packages with the yum package manager — Ansible Documentation

以下ではplaybookの各taskの内容を解説していきます。

svnの特定バージョンをインストールした状態にする

ここではsvn1.7.22のバージョン指定をしたうえでインストールさせるための設定を書いています。
rpmパッケージはopensource.wandisco.comから選んだものを指定しています。

    - name: Install svn1.7.22    #タスク実行時の名前
      become: yes                      #yum installするために、sudo で実行させる
      yum:
        name: http://opensource.wandisco.com/rhel/6/svn-1.7/RPMS/x86_64/subversion-1.7.22-1.x86_64.rpm     #特定のrpmに対してyum installさせる
        state: present  # nameで指定したパッケージをインストールした状態にする

jenkinsの最新の安定バージョンをインストールし、起動させる

svnの設定に続いてjenkinsに関しての設定を記述しています。
jenkinsの起動に必要なjdkのインストールから、jenkins最新の安定版のインストール、 最後にansibleのserviceモジュールを使って、yumでインストールしたjenkinsのserviceを有効にし、起動させるように記述します。

service - Manage services. — Ansible Documentation

    - name: ensure open JDK 1.6 is at the latest version
      become: yes
      yum:
        name: java-1.6.0-openjdk    
        state: latest     #jdk1.6.0最新版をインストールする


    - name: get the Jenkins repository  urlからファイルをダウンロードする
      get_url:
        url: http://pkg.jenkins-ci.org/redhat-stable/jenkins.repo   #ダウンロード元のurl
        dest: /etc/yum.repos.d/jenkins.repo  #ダウンロード先のファイル名指定

    - name: add the Jenkins repository
      rpm_key:
        key: http://pkg.jenkins-ci.org/redhat-stable/jenkins-ci.org.key

    - name: ensure jenkins is at the latest version
      become: yes
      yum:
        name: jenkins  
        state: latest  #jenkins最新版をインストールする

    - name: ensure jenkins is running and enabled
      become: yes
      service:
        name: jenkins
        state: started    # nameで指定したserviceを起動した状態にする
        enabled: yes     #  nameで指定したserviceを有効にする

以上で設定は終わりです。

ansible-playbookコマンドを実行して設定をサーバに反映させる

一通り設定がおわりましたので、サーバにplaybookで記述した設定を反映していきます。
ansibleをダウンロードした際に、ターミナルで ansible-playbookというコマンドも使えるようになっています。
このコマンドは、ホストに対して、playbookに書かれた設定を反映させるときに使えるものです。
一般的には以下のような形式でコマンドを打つと実行できます。

ansible-playbook {ホスト名} -i {hostsファイル}

-i でhostsファイルの指定がない場合は、デフォルトで /etc/ansible/hosts にあるファイルから読み込むようになっています。 今回は先程記述したhostsファイルを指定します。 さらには、今回yum でのパッケージインストールや、serviceの起動など、sudo権限が必要な処理を行うため、実行時に–ask-sudo-passオプションを付けて
実行時に対象サーバのユーザのパスワードを入力するようにします。

$ ansible-playbook jenkins -i ./hosts  --aks-sudo-pass

これを実行することでサーバへの設定適用が完了します。

Serverspec でサーバの状態をテストする

chefやansibleを使ってサーバの環境構築が主流になってきていて、業務や個人で使っている人も多いと思います。

サーバ構築した後に、そのサーバの状態が正しく設定されているかを確認するとき、
手動でサーバにはいってプロセスの状態を見たりする事もできますが、 管理するサーバ数が多い場合 その作業が非常に大変になっていきます。
その問題を解決するために、サーバの状態を容易にテストできるRspecやServerspecというツールが出現してきて、サーバの状態テストが非常に用意になりました。

今回は、Serverspecというツールをつかってサーバの状態テストをする手順を解説していきます。

Serverspecとは

serverspec.org テスト対象のサーバの状態(特定のサービスが起動しているか、ポートが開いているか)など、サーバのあるべき状態を記述し、対象サーバの状態をテストする事ができます。
テストの記述はrubyで記述し、rubyを知っていればテストコードの追加と設定ファイルの記述を容易に行えます。

webサーバの状態をテストしてみる

前提
対象サーバへはSSH接続を行い、~/.ssh/config ファイルに以下の設定を記述している

Host wap
  HostName ${SERVER_IP_ADDRESS}
  User test_user
  StrictHostKeyChecking no
  PasswordAuthentication no
  IdentityFile "~/.ssh/id_rsa"
  IdentitiesOnly yes
  LogLevel FATAL

対象サーバでtest_userというアカウントを作成し、sudoコマンドを実行できる状態

上記の前提条件の上で、ここでは特定のサーバに対して以下の状態になっているかを簡単にテストしてみます。

- httpdがインストールされている  
- httpd serviceが有効になっている  
- httpd プロセスが起動されている  
- 80番portが開いている  

実際にこの4つの状態は、最初にserverspecを初期化した状態でサンプルとして書かれているので、それを使ってみます。

Serverspecのインストール

ローカル環境(mac OS)において、Serverspecをインストールしていきます。
手順としては非常に簡単でServerspec本家のサイトにInstallationとしてまとまっているので、そのとおりに行えば大丈夫です。
serverspec.org

# gemでserverspecインストール
$ gem install serverspec

# serverspec-initコマンドが使えるようになっているのを確認
$ which serverspec-init
/Users/USERNAME/.rbenv/shims/serverspec-init

gemでserverspecをインストールすると、serverspec-initコマンドが使えるようになります。 serverspec-initコマンドを実行すると、テストしたいサーバの状態(OS, 接続方法, host名)を入力して自動でServerspecの設定ファイルを生成してくれます。
ここでは、外部で作成したcentOSサーバに対してsshでアクセスする際の設定を入力していきます

$ serverspec-init
Select OS type:

  1) UN*X
  2) Windows

Select number: 1        # UN*X環境を設定

Select a backend type:

  1) SSH
  2) Exec (local)

Select number: 1        # SSH接続なので、1

Vagrant instance y/n: n      #Vagrant instanceでないため、 n

Input target host name: wap         #  ~/.ssh/config ファイルに設定したhost名: wap を入力
 + spec/
 + spec/wap/
 + spec/wap/sample_spec.rb
 + spec/spec_helper.rb
 + Rakefile
 + .rspec

これを実行すると、実行したカレントディレクトリ配下にServerspec用のディレクトリ・ファイルが生成されます。

.
├── Rakefile                          
└── spec
    ├── wap
    │   └── sample_spec.rb   # wap サーバのテストを記述
    └── spec_helper.rb           # Serverspec実行時の設定ファイル  

Serverspec でのテストを実行する際には、 rakeコマンドを使ってサーバのテストファイルを呼び出し、テストを行います。 rake実行時の処理はRakefileにかかれており、デフォルトで ./spec配下のディレクトリをチェックし、*_spec.rb のrubyファイルを呼び出して
テストを実行するようになっています。
先程作られた自動生成ファイルは sample_spec.rbとなっていますが、 わかりやすく wap_spec.rbと変更しても動く様になっています。

Rakefile
require 'rake'
require 'rspec/core/rake_task'

task :spec    => 'spec:all'
task :default => :spec

namespace :spec do
  targets = []
  Dir.glob('./spec/*').each do |dir|
    next unless File.directory?(dir)
    target = File.basename(dir)
    target = "_#{target}" if target == "default"
    targets << target
  end

  task :all     => targets
  task :default => :all

  targets.each do |target|
    original_target = target == "_default" ? target[1..-1] : target
    desc "Run serverspec tests to #{original_target}"
    RSpec::Core::RakeTask.new(target.to_sym) do |t|
      ENV['TARGET_HOST'] = original_target
      t.pattern = "spec/#{original_target}/*_spec.rb"
    end
  end
end

そして、Serverspecでのテスト実行時のサーバ接続情報は下記のように設定してみます。
Serverspecでのテスト時に、サービスの起動確認など、内容によっては接続先サーバでのsudoコマンドを使ってテストを行うため、 対象ユーザのパスワードなどを入力させる処理を記述します。
初期生成時にある程度書かれていますが、ここでは微妙に変えています

spec_helper.rb
require 'serverspec'
require 'net/ssh'

set :backend, :ssh

# 対象サーバ内でのsudo passwordの設定処理
# 実行時に 環境変数に ASK_SUDO_PASSWORDが設定されている場合
# コマンドライン上でパスワードを入力させる
if ENV['ASK_SUDO_PASSWORD']
  begin
    require 'highline/import'
  rescue LoadError
    fail "highline is not available. Try installing it."
  end
  # コマンドライン上でパスワードを入力させる
  set :sudo_password, ask("Enter sudo password: ") { |q| q.echo = false }
else
  # ASK_SUDO_PASSWORD設定がオフの場合はここに設定したパスワードを利用する
  set :sudo_password, 'SERVER_USER_PASSWORD'
end
host = ENV['TARGET_HOST']

options = Net::SSH::Config.for(host)

# 接続先サーバのユーザ名
options[:user] ||= 'test_user'

set :host,        options[:host_name] || host
set :ssh_options, options
set :family,      'redhat'

これでサーバ接続時の設定を記述できました。 今回は1つのサーバに対してテストを行いますが、複数のサーバに対してテストを行う場合、設定に合わせてspec_helper.rbファイルの修正も必要になるかと思います。

続いて、対象サーバの状態テストを記述していきます。
./spec/wap/sample_spec.rb にテスト内容を記述していきます。
前述したように、対象サーバのhttpdに対して以下の状態になっていることをテストするための処理を記述します。

- httpdがインストールされている  
- httpd serviceが有効になっている  
- httpd プロセスが起動されている  
- 80番portが開いている  
sample_spec.rb
require 'spec_helper'

# httpdがインストールされている
describe package('httpd') do
  it { should be_installed }
end

# httpdのserviceの状態を記述する
describe service('httpd') do
  it { should be_enabled }     # serviceが有効になっている
  it { should be_running }      # serviceが起動されている
end

# 80番ポートが開いているかテストする
describe port(80) do
  it { should be_listening }
end

テスト実行

ここで、テストするサーバへの接続設定と、テスト内容の記述ができたので、実際にテストを実行してみます。
テスト実行は、rakeコマンドを実行することで可能です。

$ rake spec

rake実行時にカレントディレクトリのRakefileの内容を呼び出してテストを行っていきます。

まずは、対象サーバのhttpdがインストールされているが、サービスが無効・起動していない状態でテストを実行してみます。

$ rake spec

# 最初にsudo用のパスワードを入力する
Enter sudo password: 


# httpdがインストールされている テスト○
Package "httpd"
  should be installed

# 以下のテストが失敗している
Service "httpd"
  should be enabled (FAILED - 1)
  should be running (FAILED - 2)

Port "80"
  should be listening (FAILED - 3)

Failures:

  1) Service "httpd" should be enabled
     On host `wap'
     Failure/Error: it { should be_enabled }
       expected Service "httpd" to be enabled
       sudo -p 'Password: ' /bin/sh -c chkconfig\ --list\ httpd\ \|\ grep\ 3:on
       
     # ./spec/wap/sample_spec.rb:8:in `block (2 levels) in <top (required)>'

  2) Service "httpd" should be running
     On host `wap'
     Failure/Error: it { should be_running }
       expected Service "httpd" to be running
       sudo -p 'Password: ' /bin/sh -c service\ httpd\ status
       httpd is stopped

     # ./spec/wap/sample_spec.rb:9:in `block (2 levels) in <top (required)>'

  3) Port "80" should be listening
     On host `wap'
     Failure/Error: it { should be_listening }
       expected Port "80" to be listening
       sudo -p 'Password: ' /bin/sh -c netstat\ -tunl\ \|\ grep\ --\ :80\\\ 
       
     # ./spec/wap/sample_spec.rb:13:in `block (2 levels) in <top (required)>'

Finished in 0.85183 seconds (files took 13.41 seconds to load)
4 examples, 3 failures

Failed examples:

rspec ./spec/wap/sample_spec.rb:8 # Service "httpd" should be enabled
rspec ./spec/wap/sample_spec.rb:9 # Service "httpd" should be running
rspec ./spec/wap/sample_spec.rb:13 # Port "80" should be listening

テストを実行してみると httpdのパッケージはインストールされているが、httpdのserviceのテストと80番ポートのテストが失敗していることがわかります。
失敗時のログをみるとわかりますが、 サービスが有効かどうかのチェックは 実際にはchkconfigコマンドをつかっていて、 起動の確認はserviceコマンドを実行していることがわかります。 ポートの確認にはnetstatを実行していることがわかります。

ここで、対象サーバのhttpdをchkconfigコマンドを使って有効にし、httpdを起動してみます。

#テスト対象サーバでtest_userとしてログインする

# httpdを有効にする
$ sudo chkconfig httpd on

# httpdを起動する
$ sudo service httpd start
Starting httpd:                                            [  OK  ]

httpdを有効にし、起動した上で再度テストを実行するとすべて成功していることが確認できます。

$ rake spec

# 最初にsudo用のパスワードを入力する
Enter sudo password: 

Package "httpd"
  should be installed

Service "httpd"
  should be enabled
  should be running

Port "80"
  should be listening

Finished in 0.85282 seconds (files took 3.13 seconds to load)
4 examples, 0 failures

参考

Serverspecでサーバの構成をテストする 導入と個人的知見 - Qiita

https://www.ossnews.jp/oss_info/Serverspec

「Serverspec」を使ってサーバー環境を自動テストしよう - さくらのナレッジ

Serverspecでよく使うテストの書き方まとめ - Qiita

Infrastructure as Code ―クラウドにおけるサーバ管理の原則とプラクティス

Infrastructure as Code ―クラウドにおけるサーバ管理の原則とプラクティス

CentOS yum update で特定のパッケージだけアップデートさせないようにする

yum のパッケージ更新をするさい、 yum update コマンドを実行すると、すべてのパッケージとそれに依存するパッケージを更新します。 手動もしくはyum_cronによる定時実行で、これを実行すると、アップデートさせたくないパッケージも更新されてしまいます。
–excludeオプションを使うことで、指定したパッケージのみアップデートさせないようにすることができます。

# jenkinsのみアップデートさせない
$ yum update --exclude=jenkins*

ワイルドカードを使うことで、一致する複数のパッケージに対してもexclude指定ができます。

yum_cronをつかって、自動的に定期実行している場合は、/etc/yum.confに、excludeの指定ができます。

exclude=jenkins*

node.js Promiseを使った非同期処理

node.jsの非同期処理を書く際に便利なPromiseについて紹介します。
主にnode.js 4.x系以降でデフォルト利用可能な機能になります。

Promiseとは

非同期処理を抽象化したオブジェクトで、非同期処理に関する統一的なインターフェースを提供します。
これにより、非同期処理のコールバック関数の書き方が明確になり、可読性があがるというメリットがあります。
ES6(ECMA Script 6)により仕様が定められており、node.js 0.12系以降から徐々にES6の機能が導入されていて。
node.js 4系以降からはデフォルトで使えるようになっています。

http://www.ecma-international.org/ecma-262/6.0/index.html#sec-promise-objects

Node.js ES2015/ES6, ES2016 and ES2017 support

Promiseをどうやって使うか

では、Promiseをつかうってどうやって非同期処理を書くかざっくりとまとめてみると、以下の通りになります。

Promiseクラスのオブジェクトを作成する。
作成したpromiseオブジェクトに対して、成功時と失敗時に行う処理を関数として登録する。

この成功・失敗したときのコールバック関数は、 promise.then()というメソッドで登録することができます。

promiseオブジェクトの作り方

// resolveが呼ばれる
const promise  = new Promise((resolve, reject) {
  const errFlg = false;
  // 何か処理が失敗したときにrejectを呼び出す。
  // このコードではerrFlg = falseなので呼ばれない
  if (errFlg) {
    reject(new Error("An error has occured");
  }
  // 成功時にresolveを呼び出す
  resolve(100);
});

// resolve(成功)が呼ばれた時の処理
const onFulFilled = (value) => {
  console.log("---resolve called--- value: ", value);
};

// reject(失敗)が呼ばれたときの処理
const onRejected = (error) => {
  console.error(error);
};

// .then(成功時の処理, 失敗時の処理)を登録する
promise
  .then(onFulFilled, onRejected);

promiseオブジェクトを使った非同期処理として、成功時にはnew Promiseで渡した関数の第1引数にあるresolve関数、エラー時にはreject関数を呼び出すようにします。

上記の非同期処理の成功・失敗時の処理は、Promise.thenメソッドで指定することができます。

resolve関数呼出し時に引数に値を設定すると、.thenメソッドで指定したコールバック関数で値を受け取ることができます。 そして、処理が失敗した場合は、.thenで指定したコールバック関数は呼ばれずに、.catchで指定したコールバック関数が呼ばれるようになります。
こうすることで、非同期処理の成功時・失敗時の処理をかき分けることができ、可読性があがります。

Promiseオブジェクトの状態について

Promiseオブジェクトには、3つの状態をもっており、ES6 Promisesの仕様で以下のように定められています。

  • Pending: オブジェクトの作成直後の状態
  • FulFIlled: resolveした時
  • Rejected: エラーが発生してrejectした時がRejectedに変化します。

v8/v8.h at 818c6d0ba9e92e3bb0557fabaad886f7cfd9ea8a · v8/v8 · GitHub

node/v8.h at ed12ea371c5352b4b13f9f1c4e0f577fbd30bb2a · nodejs/node · GitHub

下記の図にあるとおり、Promiseの状態は一度FulFilledまたはRejectedに遷移した後、状態が固定され、変化することはありません。

f:id:kakts:20170407003706p:plain
状態が変化したときに、.thenで登録したonFulFilled, onRejectedの関数のどちらかが一度限りだけ呼ばれることになります。
後述しますが、.thenは新たなpromise objectを返すので、メソッドチェーンの形で複数の.thenを登録できます。
.thenを書いた順に処理が走り、毎回promise objectが作成され(Pending)、処理に応じてFulFilled・Rejectedの処理を連続して行うことができます。

厳密ではないですが、.thenのメソッドチェーンを行う際には以下の図ような感じでチェーンのたびに新たにobjectが作られて処理が進んでいきます。 f:id:kakts:20170409150743p:plain

promise.thenを使った成功・失敗時のコールバックの登録

メソッドの仕様は以下のとおりになっています

promise.then(onFulFilled, onRejected)

resolveした時、onFulFilledが呼ばれ、反対にrejectしたときはonRejectedが呼ばれます。 このonFulFilled, onRejectedは必須ではないのでどちらか一方のみ登録することが可能です。 syntax sugarとして、 .catchメソッドも用意されていて、エラー時の処理のみ登録したい場合は.catchを使うと便利です。
実際には.catchは以下の処理と同じです。

promise.then(undefined, onRejected);

node.jsのjavascript エンジンであるv8では、以下の箇所で.catchの実装がされています。
node/builtins-promise.cc at ed12ea371c5352b4b13f9f1c4e0f577fbd30bb2a · nodejs/node · GitHub

TF_BUILTIN(PromiseCatch, PromiseBuiltinsAssembler) {
  // 1. Let promise be the this value.
  Node* const promise = Parameter(0);

  // on_resolveをundefinedにしている。
  Node* const on_resolve = UndefinedConstant();
  Node* const on_reject = Parameter(1);
  Node* const context = Parameter(4);

  Label if_internalthen(this), if_customthen(this, Label::kDeferred);
  GotoIf(TaggedIsSmi(promise), &if_customthen);
  BranchIfFastPath(context, promise, &if_internalthen, &if_customthen);

  Bind(&if_internalthen);
  {
    Node* const result =
        InternalPromiseThen(context, promise, on_resolve, on_reject);
    Return(result);
  }

  Bind(&if_customthen);
  {
    Isolate* isolate = this->isolate();
    Node* const then_str = HeapConstant(isolate->factory()->then_string());
    Callable getproperty_callable = CodeFactory::GetProperty(isolate);
    Node* const then =
        CallStub(getproperty_callable, context, promise, then_str);
    Callable call_callable = CodeFactory::Call(isolate);

    // on_resolveをundefined、on_rejectは.catchの第1引数に登録されたものを使っている
    Node* const result =
        CallJS(call_callable, context, then, promise, on_resolve, on_reject);
    Return(result);
  }
}

promiseオブジェクトの.then .catchメソッドは返り値として新たにpromiseオブジェクトを返すため、
メソッドチェーンをつかって複数の処理を連続して行うことができます。

// new Promiseで promiseオブジェクトを作成する
const promise = new Promise((resolve, reject) => {
  // 処理が正常に終了した場合 resolveを呼ぶ
  resolve(100);
});

// 上記の処理が成功・失敗したときのコールバック処理を登録する
// 成功時の処理 .thenの第1引数に渡す
// 失敗時の処理 .catchの第1引数に渡す
promise
  .then((value) => {
    console.log("----then1 value", value);

    // return した値valueがpromiseオブジェクト以外の場合暗黙的にresolve(value)が呼ばれ、
    // 次のthenメソッドが呼ばれる
    // これをコメントアウトすると次のthenにはvalue=undefinedが渡る 
    // 関数でreturnを書かない場合 暗黙的にundefinedが返されるため
    return 200;
  })
  .then((value) => {
    console.log("----then2 value", value);
  })
  .catch((error) => {
    console.error(error);
  });

そして .thenをメソッドチェーンで複数続けて書く場合、1番目のthenの中でエラーが発生してrejectが呼ばれた場合、2番目のthenは呼ばれずにcatchで指定されたコールバックが実行されます。
下記に例をしめしているのでコードを読むとわかりやすいです。

const taskA = () => {
  console.log("Task A");
  // throwすることでreject(new Error())を呼び出すと同義になる
  throw new Error("throw Error @ Task A");
};

const taskB = () => {
  console.log("Task B"); // 呼ばれない
};

const onRejected = (error) => {
  console.error(error); // => "throw Error @ Task A"
}

const finalTask = () => {
  console.log("Final Task");
}

const promise = Promise.resolve();
promise
  .then(taskA) // ここでエラーが投げられる
  .then(taskB) // 呼ばれない
  .catch(onRejected) // エラー処理
  .then(finalTask); // 呼ばれない

このコードで注意する必要があるのは、
メソッドチェーンの途中で.catchの後ろに.thenをつなげているのですが、
最後の.thenのコールバック関数でエラーが起きたときに、これより後ろに.catchがつながっていないためエラーを拾うことができないので注意が必要です。

複数のPromise objectに対する処理

Promiseには、複数のpromise objectに対して処理を行うインターフェースも用意されています。 promise.all と promise.raceの2つがあり、ここで紹介していきます。

Promise.all

Promise.allは 複数のpromiseオブジェクトの配列を引数に取るもので、 配列中のpromise objectを並列で実行し、すべてのpromise objectがresolveされた時、初めて.thenが呼ばれます。 .thenには 配列中の各オブジェクトでresolveに渡された値が配列で渡ってくるので、それを利用することができます。

const aPromise = new Promise((resolve, reject) => {
  resolve(4);
});

const bPromise = new Promise((resolve, reject) => {
  resolve(5);
});

// .allはpromiseオブジェクトの配列を引数に取る
// 引数に渡したpromiseオブジェクトのすべてがresolveされたときに次の.then を呼ぶ
Promise
  .all([aPromise, bPromise])
  .then((value) => {
    console.log('resolved')
    console.log(value); // [4, 5]を返す
  })
  .catch((error) => {
    console.error('error');
    console.error(error);
  });

v8では以下の箇所で実装されています。
node/promise.js at master · nodejs/node · GitHub

function PromiseAll(iterable) {
  if (!IS_RECEIVER(this)) {
    throw %make_type_error(kCalledOnNonObject, "Promise.all");
  }

  // false debugEvent so that forwarding the rejection through all does not
  // trigger redundant ExceptionEvents
  var deferred = %new_promise_capability(this, false);
  var resolutions = new InternalArray();

  var count;

  // For catch prediction, don't treat the .then calls as handling it;
  // instead, recurse outwards.
  var instrumenting = DEBUG_IS_ACTIVE;
  if (instrumenting) {
    SET_PRIVATE(deferred.reject, promiseForwardingHandlerSymbol, true);
  }

  function CreateResolveElementFunction(index, values, promiseCapability) {
    var alreadyCalled = false;
    return (x) => {
      if (alreadyCalled === true) return;
      alreadyCalled = true;
      values[index] = x;
      // すべてのpromise objectが成功したのでresolve発火
      if (--count === 0) {
        var valuesArray = [];
        %MoveArrayContents(values, valuesArray);
        %_Call(promiseCapability.resolve, UNDEFINED, valuesArray);
      }
    };
  }

  try {
    var i = 0;
    // resolveに必要な残りpromise object数
    // 0になったらresolve発火
    count = 1;
    for (var value of iterable) {
      var nextPromise = this.resolve(value);
      ++count;
      var throwawayPromise = nextPromise.then(
          CreateResolveElementFunction(i, resolutions, deferred),
          deferred.reject);
      // For catch prediction, mark that rejections here are semantically
      // handled by the combined Promise.
      if (instrumenting && %is_promise(throwawayPromise)) {
        SET_PRIVATE(throwawayPromise, promiseHandledBySymbol, deferred.promise);
      }
      ++i;
    }

    // 6.d
    // すべてのpromise objectが成功したのでresolve発火
    if (--count === 0) {
      var valuesArray = [];
      %MoveArrayContents(resolutions, valuesArray);
      %_Call(deferred.resolve, UNDEFINED, valuesArray);
    }

  } catch (e) {
    // エラーになったら即座にcatchされ、rejectされる
    %_Call(deferred.reject, UNDEFINED, e);
  }
  return deferred.promise;
}

Promise.race

Promise.allと同様に、複数のpromiseオブジェクトを引数にとるのがPromise.raceです。
.raceは、.allと違って、登録した複数のpromiseオブジェクトのうち、一番早く処理が終わったもののresolveの結果を.thenに渡します。

// promise.replace
// promiseオブジェクトの配列を引数に渡す
// 引数に渡したオブジェクトの中でどれか一つでもFulFilledまたRejectedになったら次の処理を実行する

const time400 = new Promise((resolve, reject) => {
  console.log("time400");
  setTimeout(() => {
    resolve(400);
  }, 400);
});

const time100 = new Promise((resolve, reject) => {
  console.log("time100");
  setTimeout(() => {
    resolve(100);
  }, 100);
});

const time4000 = new Promise((resolve, reject) => {
  console.log("time4000");
  setTimeout(() => {
    resolve(4000);
  }, 4000);
});

const time700 = new Promise((resolve, reject) => {
  console.log("time700");
  setTimeout(() => {
    resolve(700);
  }, 700);
});

Promise
  .race([time400, time100, time4000, time700])
  .then((value) => {
    // time100が一番早くresolveが呼ばれるので 100が渡ってくる
    console.log("then value:", value);
  })
  .catch((error) => {
    console.error("error");
    console.error(error);
  });

ポイントとしては、引数に渡したすべてのpromiseオブジェクトの処理は実行されるが、一番早くresolveが呼ばれたものに対して.thenへ値が渡されることです。
上記のコードの実行結果として、各promiseオブジェクトのconsole.logが出力されているのが確認できると思います。

promise.raceのv8での実装は以下の通りになります。
https://github.com/nodejs/node/blob/master/deps/v8/src/js/promise.js#L100-L130

function PromiseRace(iterable) {
  if (!IS_RECEIVER(this)) {
    throw %make_type_error(kCalledOnNonObject, PromiseRace);
  }

  // false debugEvent so that forwarding the rejection through race does not
  // trigger redundant ExceptionEvents
  var deferred = %new_promise_capability(this, false);

  // For catch prediction, don't treat the .then calls as handling it;
  // instead, recurse outwards.
  var instrumenting = DEBUG_IS_ACTIVE;
  if (instrumenting) {
    SET_PRIVATE(deferred.reject, promiseForwardingHandlerSymbol, true);
  }

  try {
    for (var value of iterable) {
      // 配列に渡したpromise objectをループ
      // この時点で実際にpromiseの処理を一つずつ行っている。
      // 一番早くresolveされたobjectをすぐに.thenに送る
      var throwawayPromise = this.resolve(value).then(deferred.resolve,
                                                      deferred.reject);
      // For catch prediction, mark that rejections here are semantically
      // handled by the combined Promise.
      if (instrumenting && %is_promise(throwawayPromise)) {
        SET_PRIVATE(throwawayPromise, promiseHandledBySymbol, deferred.promise);
      }
    }
  } catch (e) {
    // 1つでもエラーをcatchしたらrejectする
    %_Call(deferred.reject, UNDEFINED, e);
  }
  return deferred.promise;
}

まとめ

node.js(v8)で実装されているPromiseオブジェクトの概要について解説しました。
非同期処理は、async.jsなどをつかって実装することもできるのですが、Promiseを使うと、成功時・失敗時の処理を分離してかけるので、さらに可読性が上がって良いと思いました。

node.js v7.6系以降でデフォルトで使用可能になるasync/await は、このPromiseをベースにしてつくられたラッパーなので、async/awaitを学ぶ際にはPromiseの仕組みを知っていると理解が深まると思います。
node.jsのasync/awaitに関しては別の記事で取り上げたいと思います。

参考

Promises in Node.js - An Alternative to Callbacks - StrongLoop

azu.github.io

unixファイルシステムについて (詳解linuxカーネル第1章5節)

積読していた詳解linuxカーネルをまた読み進め始めました。 プロセススケジューラやらファイルシステムなどについて非常に詳しく書かれていて面白いです。
今回は第1章5節 ファイルについての項目を読んで整理してみました。

詳解 Linuxカーネル 第3版

詳解 Linuxカーネル 第3版

ファイルとは (1.5.1)

ファイルとは: バイト列として構造化された情報の入れ物のことです。unix|linuxカーネルはファイルの中身には関知しません。
ユーザの側からみたとき、ファイルはツリー構造の名前空間に系統立てられ、末端以外のツリーノードはすべてディレクトリを表します。

ハードリンクとソフトリンク (1.5.2)

unix環境のファイルシステム上でディレクトリとファイル名を結びつける手法としては、一般的にハードリンクとソフトリンクの2つがある。

ハードリンクについて

ディレクトリ中のファイル名は、ファイルハードリンク(file hard link)もしくはリンク(link)と呼ばれる。
1ファイルの実体に対し、同一ディレクトリ内もしくは異なるディレクトリ内に複数のリンクを持つことができます。
つまり、1ファイルの実体が複数のファイル名を持つことができます。

一般的に パス名p1によって識別されるファイルに対して新たにパス名p2の新しいファイルハードリンクを作成するときは、以下のコマンドで行うことができる。

$ ln p1 p2

ただし、ハードリンクの制約としては主に2つあり、以下の通りとなっている。
ディレクトリのハードリンクを作成することはできない。
・ハードリンクは同一ファイルシステム内に含まれるファイルに対してのみ生成できる。

この2つの制約はシステムの運用上非常にやっかいなものとなり、ソフトリンクは、この問題を解決するために導入される

ソフトリンクについて

ソフトリンク(soft link)は、一般的にシンボリックリンク(symbolic link)と呼ばれ、上述したハードリンクによる問題を克服するために導入されました。
ソフトリンクとは、別ファイルの任意のパス名を含むファイルで、パス名はどのファイルシステムのものを参照しても良い。
さらには、存在しないファイルを指定することも可能です。

パス名p1によって識別されるファイルに対して新たにパス名p2の新しいソフトリンクを作成するときは、以下のコマンドで行うことができる。

ln -s p1 p2

-s オプションを付けるだけでソフトリンクを作成することができます。
このコマンドを実行することにより、p2のディレクトリ部分を抽出した上で、そのディレクトリ以下にp2で指定したファイル名でシンボリックリンク型のエントリを生成する。
これにより、p2への参照は自動的にp1への参照へと変換される。

ファイルの種類について (1.5.3)

unix環境において ファイルには以下の種類があります。
- 通常ファイル
- ディレクトリ - シンボリックリンク - ブロック型デバイスファイル - キャラクタ型デバイスファイル - パイプ、名前付きパイプ(named pipe, FIFO) - ソケット

あるプログラムがデバイスファイルにアクセスするとき、そのデバイスファイルに対応するI/Oデバイスに直接アクセスする(詳解linuxカーネル第13章 参照)

パイプとソケットは、プロセス間通信に利用させる特殊ファイルとなる。

ファイルディスクリプタとiノードについて (1.5.4)

ファイルシステムがファイルを操作する際に必要な情報はiノード(inode)と呼ばれるデータ構造が持っている。
各ファイルはiノードを持っていて、ファイルシステムはiノードを利用してファイル操作を行う。
ファイルシステムで使われている関数はシステムによって異なるが、POSIX標準で定められている下記のものは必ず提供されているものとなります。

  • ファイルの種類
  • ファイルに関連付けられるハードリンク数
  • ファイルのバイト長
  • バイス番号
  • inode番号
  • 所有者のユーザID
  • ユーザグループID
  • 各種タイムスタンプ(inode変更時刻 最終アクセス時刻など)
  • アクセス権とファイルのモード