今日は Elixir の Node 同士がどうやって通信しているのか見てみます。ザックリ言って、UNIX ドメインソケットなのか、UDP ソケットなのか、TCP ソケットなのかを調べます。Elixir を動かすのは Macbook Air 上の MacOS 10.13.6 です。
なぜこれを調べたくなったかというと、Elixirプロセスのホットスワップをする場合に、スワップ後もTCPのセッションをそのまま維持できるのかどうかに興味が出たからです。それを調べる前に、Elixir/Erlangがいろんな場合の通信に何のソケットを使っているのか一通り理解しておきたくなったというところです。
以下ではElixirのプロセスと区別するために、UNIXのプロセスに言及する場合は UNIXプロセス
と明示することにします。PIDについても、UNIXのPIDの場合には UNIX PID
と明示することにします。
UNIXプロセスを観察する
一番最初に UNIX プロセスをきれいにするために PC をリブートします。訳は後で説明します。
ノードを作らない場合の動作
目的はノード間の接続のメカニズムを調べることですが、それに先立ってノードを作らないで Elixir の動作をみてみます。
Elixir を動かさずに UNIXプロセスを見る
Elixir を動かさない状態の UNIX プロセスで Elixir / Erlang 関係のプロセスがあるか見てみます。
myhost:~ yu$ ps a | egrep erl
578 s000 S+ 0:00.01 egrep erl
Elixir/Erlang 関係のUNIXプロセスはerl
がついてるようなので、ps コマンドの出力を egrep コマンドでフィルタしてみました。なお grep でも fgrep でもなく egrep なのは私の趣味です1。
これだけではターミナルから切り離されたプロセスや特権モードのプロセスはわかりませんので ps コマンドにオプションを足してみてみます。お使いの UNIX によっては ps のオプションが違うかもしれません。
myhost:~ yu$ ps axu | egrep erl
root 202 0.0 0.1 4331404 7312 ?? Ss 10:39PM 0:00.08 /usr/libexec/powerlogd
yu 616 0.0 0.0 4267752 824 s000 S+ 10:42PM 0:00.00 egrep erl
1行目は root 権限で動いている powerlogd が pow erl ogd と erl に引っかかって出てきてしまっています。2行目は egrep の引数が引っかかってます。要は Elixir/Erlang のUNIXプロセスはオーナやターミナルに関わらず特に走ってないようです。
Elixir を動かしてUNIXプロセスを見る
では、別ターミナルで iex を起動します。
myhost:~ yu$ iex
Erlang/OTP 22 [erts-10.4.4] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:1] [hipe] [dtrace]
Interactive Elixir (1.9.1) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)>
この状態でUNIXプロセスを見てみます。
myhost:~ yu$ ps a | egrep erl
808 s000 S+ 0:00.00 egrep erl
759 s001 S+ 0:00.52 /usr/local/Cellar/erlang/22.0.7/lib/erlang/erts-10.4.4/bin/beam.smp -- -root /usr/local/Cellar/erlang/22.0.7/lib/erlang -progname erl -- -home /Users/yu -- -pa /usr/local/Cellar/elixir/1.9.1/bin/../lib/eex/ebin /usr/local/Cellar/elixir/1.9.1/bin/../lib/elixir/ebin /usr/local/Cellar/elixir/1.9.1/bin/../lib/ex_unit/ebin /usr/local/Cellar/elixir/1.9.1/bin/../lib/iex/ebin /usr/local/Cellar/elixir/1.9.1/bin/../lib/logger/ebin /usr/local/Cellar/elixir/1.9.1/bin/../lib/mix/ebin -elixir ansi_enabled true -noshell -user Elixir.IEx.CLI -extra --no-halt +iex
とグチャッと出てきました。これ見やすく書いてみるとこんな感じです。
myhost:~ yu$ ps a | egrep erl
808 s000 S+ 0:00.00 egrep erl
759 s001 S+ 0:00.52 beam.smp ...略...
と Elixir/Erlang の仮想マシン BEAM が動いているのが分かります。
はて、動いたのはこれだけでしょうか。ps a
だけでは、ターミナルから切り離されたUNIXプロセスが表示されません。特権モードのUNIXプロセスも表示されません。全部のUNIXプロセスを見てみると、もうひとつ erl_child_setup があるのが分かります。これは特権モードではないですがターミナルからは切り離された状態です。
myhost:~ yu$ ps aux | egrep erl
...略...
yu 783 0.0 0.0 4269784 832 ?? Ss 11:04PM 0:00.00 erl_child_setup 256
...略...
じゃあ、もっとUNIXプロセスがあるかもしれません。BEAM が起動するときに特権モードで色々してたりしないのでしょうか。
myhost:~ yu$ ls -l /usr/local/Cellar/erlang/22.0.7/lib/erlang/erts-10.4.4/bin/beam.smp
-r-xr-xr-x 1 yu staff 3632112 7 11 01:05 /usr/local/Cellar/erlang/22.0.7/lib/erlang/erts-10.4.4/bin/beam.smp
と、setuid フラグが立ってないので特権で動かすUNIXプロセスではないことが分かります。
Elixir を終了してUNIXプロセスを見る
ではここで一旦 iex を終了します。そしてUNIXプロセスを見てみます。
myhost:~ yu$ ps a | egrep erl
843 s000 S+ 0:00.00 egrep erl
先程まで走っていた BEAM がいなくなってます。
myhost:~ yu$ ps aux | egrep erl
root 202 0.0 0.1 4331404 7288 ?? Ss 10:54PM 0:00.08 /usr/libexec/powerlogd
yu 848 0.0 0.0 4276968 848 s000 S+ 11:09PM 0:00.00 egrep erl
と念のため、特権モードとターミナルから切り離されたUNIXプロセスも探してみますが居ないようです。
Elixir ノードを作る場合の動作
では今度は Elixir を立ち上げるときにノードとして立ち上げてみましょう。
myhost:~ yu$ iex --sname afo
Erlang/OTP 22 [erts-10.4.4] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:1] [hipe] [dtrace]
Interactive Elixir (1.9.1) - press Ctrl+C to exit (type h() ENTER for help)
iex(afo@myhost)1>
このときに以下のような warning が出てくることがあります。
これは「許可」で大丈夫です。何しているかはこの先を読むと分かります。
この場合に走っている Elixir/Erlang 関係のUNIXプロセスを見てみます。
myhost:~ yu$ ps a | egrep erl
979 s000 S+ 0:00.01 egrep erl
863 s001 S+ 0:00.47 /usr/local/Cellar/erlang/22.0.7/lib/erlang/erts-10.4.4/bin/beam.smp -- -root /usr/local/Cellar/erlang/22.0.7/lib/erlang -progname erl -- -home /Users/yu -- -pa /usr/local/Cellar/elixir/1.9.1/bin/../lib/eex/ebin /usr/local/Cellar/elixir/1.9.1/bin/../lib/elixir/ebin /usr/local/Cellar/elixir/1.9.1/bin/../lib/ex_unit/ebin /usr/local/Cellar/elixir/1.9.1/bin/../lib/iex/ebin /usr/local/Cellar/elixir/1.9.1/bin/../lib/logger/ebin /usr/local/Cellar/elixir/1.9.1/bin/../lib/mix/ebin -elixir ansi_enabled true -noshell -user Elixir.IEx.CLI -sname afo -extra --no-halt +iex
これは先程と同じです。さらに特権モードやターミナルから切り離されたUNIXプロセスも見てみます。
myhost:~ yu$ ps ax | egrep erl
...略...
892 ?? S 0:00.01 /usr/local/Cellar/erlang/22.0.7/lib/erlang/erts-10.4.4/bin/epmd -daemon
893 ?? Ss 0:00.00 erl_child_setup 256
...略...
先程見えた erl_child_setup の他に epmd なるUNIXプロセスが走っています。これは
Erlang Port Mapper Daemon
というUNIXプロセスです。ドキュメントを読むと Hence, epmd maps symbolic node names to machine addresses. という文があります。Atom で表現された論理的なノード名を、対応するOSカーネル上の物理アドレスに変換するということのようです。
再びElixirを終了してみる
今度も iex を抜けてみます。すると
myhost:~ yu$ ps a | egrep erl
1140 s000 S+ 0:00.00 egrep erl
と、一見、先程同様に関係するUNIXプロセスがなくなったように見えます。しかしよく見てみると…
myhost:~ yu$ ps ax | egrep erl
202 ?? Ss 0:00.08 /usr/libexec/powerlogd
892 ?? S 0:00.01 /usr/local/Cellar/erlang/22.0.7/lib/erlang/erts-10.4.4/bin/epmd -daemon
1144 s000 S+ 0:00.00 egrep erl
と、epmd が終了せずに走りっぱなしなのが分かります。何をしているのでしょうか。このUNIXプロセスがオープンしているファイルディスクリプタを見てみます。UNIX PID は892です。
myhost:~ yu$ lsof -n -P -p 892
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
epmd 892 yu cwd DIR 1,4 960 2 /
epmd 892 yu txt REG 1,4 46868 4313675239 /usr/local/Cellar/erlang/22.0.7/lib/erlang/erts-10.4.4/bin/epmd
epmd 892 yu txt REG 1,4 841456 4313881066 /usr/lib/dyld
epmd 892 yu txt REG 1,4 1171918848 4313886080 /private/var/db/dyld/dyld_shared_cache_x86_64h
epmd 892 yu 0r CHR 3,2 0t0 308 /dev/null
epmd 892 yu 1w CHR 3,2 0t0 308 /dev/null
epmd 892 yu 2w CHR 3,2 0t0 308 /dev/null
epmd 892 yu 3u IPv4 0x9f51acbf95b93e15 0t0 TCP *:4369 (LISTEN)
epmd 892 yu 4u IPv6 0x9f51acbf8fa6f16d 0t0 TCP *:4369 (LISTEN)
最後の2行を見ると、TCPソケットで接続を待ち構えています。IPv4とIPv6との2つのソケットで、どちらもポート番号が4369です。これがこの記事の一番最初にPCをリブートした理由です。一旦 iex をノード名付きで立ち上げてしまうと、何もしなかった前には戻れなくて、明示的にUNIXのkillコマンドで落とすなりマシンをリブートするまで、daemonとしてずっと滞在しています。
ノード間の通信を見る
ついにElixirのノードを複数立ち上げて、それらを接続してみることにします。
ノードを2つ立ち上げる
UNIXコマンドを実行するターミナルの他に、Elixirを動かすターミナルを2つ起動します。そしてそれぞれのターミナルでElixirをノード名barとfooとで立ち上げます。
myhost:~ yu$ iex --sname bar
Erlang/OTP 22 [erts-10.4.4] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:1] [hipe] [dtrace]
Interactive Elixir (1.9.1) - press Ctrl+C to exit (type h() ENTER for help)
myhost:~ yu$ iex --sname foo
Erlang/OTP 22 [erts-10.4.4] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:1] [hipe] [dtrace]
Interactive Elixir (1.9.1) - press Ctrl+C to exit (type h() ENTER for help)
この状態でUNIXプロセスを見てみます。
myhost:~ yu$ ps
PID TTY TIME CMD
558 ttys000 0:00.25 -bash
1050 ttys001 0:00.03 -bash
2002 ttys001 0:00.41 /usr/local/Cellar/erlang/22.0.7/lib/erlang/erts-10.4.4
1563 ttys002 0:00.02 -bash
2033 ttys002 0:00.38 /usr/local/Cellar/erlang/22.0.7/lib/erlang/erts-10.4.4
UNIXプロセスも2つ立ち上がってるのが分かります。
なお余談ですが、このコマンド実行では UNIX PID が 2002 と 2033 のUNIXプロセスについては出力が途中で切られてしまってて、全部が見えてません。UNIXパイプを通すと全部見えます。どうも ps コマンドが、標準出力がUNIXパイプの場合には「画面に出力しているのではない」と判断して、省略せずに全部を出力しているようです。
myhost:~ yu$ ps | egrep erl
2091 ttys000 0:00.00 egrep erl
2002 ttys001 0:00.41 /usr/local/Cellar/erlang/22.0.7/lib/erlang/erts-10.4.4/bin/beam.smp -- -root /usr/local/Cellar/erlang/22.0.7/lib/erlang -progname erl -- -home /Users/yu -- -pa /usr/local/Cellar/elixir/1.9.1/bin/../lib/eex/ebin /usr/local/Cellar/elixir/1.9.1/bin/../lib/elixir/ebin /usr/local/Cellar/elixir/1.9.1/bin/../lib/ex_unit/ebin /usr/local/Cellar/elixir/1.9.1/bin/../lib/iex/ebin /usr/local/Cellar/elixir/1.9.1/bin/../lib/logger/ebin /usr/local/Cellar/elixir/1.9.1/bin/../lib/mix/ebin -elixir ansi_enabled true -noshell -user Elixir.IEx.CLI -sname bar -extra --no-halt +iex
2033 ttys002 0:00.38 /usr/local/Cellar/erlang/22.0.7/lib/erlang/erts-10.4.4/bin/beam.smp -- -root /usr/local/Cellar/erlang/22.0.7/lib/erlang -progname erl -- -home /Users/yu -- -pa /usr/local/Cellar/elixir/1.9.1/bin/../lib/eex/ebin /usr/local/Cellar/elixir/1.9.1/bin/../lib/elixir/ebin /usr/local/Cellar/elixir/1.9.1/bin/../lib/ex_unit/ebin /usr/local/Cellar/elixir/1.9.1/bin/../lib/iex/ebin /usr/local/Cellar/elixir/1.9.1/bin/../lib/logger/ebin /usr/local/Cellar/elixir/1.9.1/bin/../lib/mix/ebin -elixir ansi_enabled true -noshell -user Elixir.IEx.CLI -sname foo -extra --no-halt +iex
さて、UNIXプロセス 2002 と 2033 とのファイルディスクリプタを見てみます。大量に出てくるので egrep コマンドを使って TCP
でフィルタします。
myhost:~ yu$ lsof -n -P -p 2002 | egrep TCP
beam.smp 2002 yu 33u IPv4 0x9f51acbf95b950d5 0t0 TCP *:49923 (LISTEN)
beam.smp 2002 yu 34u IPv4 0x9f51acbf90c9e775 0t0 TCP 127.0.0.1:49924->127.0.0.1:4369 (ESTABLISHED)
myhost:~ yu$ lsof -n -P -p 2033 | egrep TCP
beam.smp 2033 yu 51u IPv4 0x9f51acbf91d1f395 0t0 TCP *:49926 (LISTEN)
beam.smp 2033 yu 52u IPv4 0x9f51acbf918b90d5 0t0 TCP 127.0.0.1:49927->127.0.0.1:4369 (ESTABLISHED)
とどちらも TCP のソケットが2つあり、一方はLISTENで接続待ち状態、もう一方は何かのUNIXプロセスとのTCPセッションが確立しています。確立している方はどのUNIXプロセスとつながっているのでしょうか。
myhost:~ yu$ ps x | egrep erl
892 ?? S 0:00.05 /usr/local/Cellar/erlang/22.0.7/lib/erlang/erts-10.4.4/bin/epmd -daemon
...略...
はい、こいつが怪しいですね。これは先程見たときにLISTEN状態のTCPソケットを持っていました。
myhost:~ yu$ lsof -n -P -p 892 | egrep TCP
epmd 892 yu 3u IPv4 0x9f51acbf95b93e15 0t0 TCP *:4369 (LISTEN)
epmd 892 yu 4u IPv6 0x9f51acbf8fa6f16d 0t0 TCP *:4369 (LISTEN)
epmd 892 yu 5u IPv4 0x9f51acbf91d1d775 0t0 TCP 127.0.0.1:4369->127.0.0.1:49924 (ESTABLISHED)
epmd 892 yu 6u IPv4 0x9f51acbf91d1e0d5 0t0 TCP 127.0.0.1:4369->127.0.0.1:49927 (ESTABLISHED)
というように、それぞれの Elixir ノードを動かしているUNIXプロセスとepmdとでTCPセッションを張っています。これにより bar@myhost
と foo@myhost
という論理的なノード名と実行系の物理的なアドレスの紐付けを行ってるのでしょう。
UNIXプロセス 2002 2033 のESTABLISHEDなソケットはどこにつながってるのか分かりました。では LISTEN しているソケット TCP *:49923 と TCP *:49926 は何を待ってるのか、いかにもノード間の接続に使ってそうなので次に行きましょう。
ノードの接続をしてみる
ここまでの2つのノードは独立に起動されただけでした。これらを接続してみましょう。bfo 側から foo 側に接続してみます。
iex(bar@myhost)1> Node.list
[]
iex(bar@myhost)2> Node.connect(:foo@myhost)
true
iex(bar@myhost)3> Node.list
[:foo@myhost]
繋がりました。foo 側からも接続されているのが分かります。
iex(foo@myhost)1> Node.list
[:bar@myhost]
さあではソケットはどうなっているでしょうか。
myhost:~ yu$ lsof -n -P -p 2002 | egrep TCP
beam.smp 2002 yu 33u IPv4 0x9f51acbf95b950d5 0t0 TCP *:49923 (LISTEN)
beam.smp 2002 yu 34u IPv4 0x9f51acbf90c9e775 0t0 TCP 127.0.0.1:49924->127.0.0.1:4369 (ESTABLISHED)
beam.smp 2002 yu 36u IPv4 0x9f51acbf918b7e15 0t0 TCP 127.0.0.1:49967->127.0.0.1:49926 (ESTABLISHED)
これはノードbar側のUNIXプロセスのTCPのファイルディスクリプタです。相変わらず *:49923 で LISTEN してます。接続が確立している2つのうち、前者は接続前からあった epmd との接続です。
そして、最後にあるソケットが新しく確立した TCP セッションです。接続元のIPアドレスとTCPポートの対が127.0.0.1:49967で、接続先のIPアドレスとTCPポートの対が127.0.0.1:49926です。
そうです。この49926こそfooがLISTENで待ち受けていたTCPポート番号です。
ノードfoo側のTCPに使ってるファイルディスクリプタを見てみましょう。
myhost:~ yu$ lsof -n -P -p 2033 | egrep TCP
beam.smp 2033 yu 51u IPv4 0x9f51acbf91d1f395 0t0 TCP *:49926 (LISTEN)
beam.smp 2033 yu 52u IPv4 0x9f51acbf918b90d5 0t0 TCP 127.0.0.1:49927->127.0.0.1:4369 (ESTABLISHED)
beam.smp 2033 yu 53u IPv4 0x9f51acbf925de395 0t0 TCP 127.0.0.1:49926->127.0.0.1:49967 (ESTABLISHED)
相変わらずTCPポート番号49926で待ち受けています。最初のTCPのセッションはepmdとの接続。そして最後のTCPセッションがbarとの接続です。IPアドレスとポート番号の対が、接続元と接続先とで逆向きになっています。
ということで、ElixirのノードでLISTEN状態で待ち受けているTCPソケットは、他のノードからの接続を待ち受けているポートでした。
ノード間の接続はepmd経由ではない
そう言えばと epmd のソケットを見てみると、先ほどと同じで新たな接続はありません。ノード同士の接続の際には epmd はノードの名前解決には使われますが、接続自体はノード間で直接TCPのセッションを張るということですね。
myhost:~ yu$ lsof -n -P -p 892 | egrep TCP
epmd 892 yu 3u IPv4 0x9f51acbf95b93e15 0t0 TCP *:4369 (LISTEN)
epmd 892 yu 4u IPv6 0x9f51acbf8fa6f16d 0t0 TCP *:4369 (LISTEN)
epmd 892 yu 5u IPv4 0x9f51acbf91d1d775 0t0 TCP 127.0.0.1:4369->127.0.0.1:49924 (ESTABLISHED)
epmd 892 yu 6u IPv4 0x9f51acbf91d1e0d5 0t0 TCP 127.0.0.1:4369->127.0.0.1:49927 (ESTABLISHED)
TCPだけなのかどうか
ちなみにUDPのセッションがあったりするのかと見てみましたが、使っていないようでした。
myhost:~ yu$ lsof -n -P -p 892 | egrep UDP
myhost:~ yu$ lsof -n -P -p 2002 | egrep UDP
myhost:~ yu$ lsof -n -P -p 2033 | egrep UDP
まとめ
Elixirの複数のノードが接続されるときにはTCPソケットを用いています。このほかErlangのepmdがノードの名前解決をしており、ノードとepmdもTCPで接続されています。
今回はノードの接続を調べてみました。次に機会があったら spawn の動作とか、OTP周りとかを調べてみたいです。
参考文献
-
最初に触った UNIX マシンは SunOS1.2 でした。そこに検索のコマンドが3つもあって、どうも一番単純なパターン表記しかつかえない fgrep が一番速そうで、研究室のみなさんは正規表現を使わないような fixed なパターンマッチには fgrep を使っているようでした。でも本当にそうなんだろうか、本当はどれが速いんだろうかと grep/egerp/fgrep の性能を(おそらく csh ビルトインの) time コマンドを使って調べたところ、一番複雑なパターの記述が可能な egrep が最も速かったのでした。それ依頼、特段の必要がなければ egrep と打ってます。今もそうなのかは知りません。なお、そのちょっと後に「Hacker の習慣」みたいな文書が流れてきて、20ぐらいある特徴の一つに (Hacker) uses egrep because she timed it. などと書いてあってなかなか嬉しかったです。おそらく VAX/Sun で動く AT&T 版 Version 7 での話でしょう。なお、Hacker を she で受けてた、というところもちょっとおもしろいですね。 ↩