はじめてな Elixir(27) ノード間の通信がどうなってるか調べる

今日は 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 関係のプロセスがあるか見てみます。

```console
myhost:~ yu$ ps a | egrep erl
  578 s000  S+     0:00.01 egrep erl
```

Elixir/Erlang 関係のUNIXプロセスは`erl`がついてるようなので、ps コマンドの出力を egrep コマンドでフィルタしてみました。なお grep でも fgrep でもなく egrep なのは私の趣味です[^egrep]。

[^egrep]: 最初に触った 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 で受けてた、というところもちょっとおもしろいですね。

これだけではターミナルから切り離されたプロセスや特権モードのプロセスはわかりませんので ps コマンドにオプションを足してみてみます。お使いの UNIX によっては ps のオプションが違うかもしれません。

```console
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 を起動します。

```console: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プロセスを見てみます。

```console
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
```

とグチャッと出てきました。これ見やすく書いてみるとこんな感じです。

```console
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 があるのが分かります。これは特権モードではないですがターミナルからは切り離された状態です。

```console
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 が起動するときに特権モードで色々してたりしないのでしょうか。

```console
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プロセスを見てみます。

```console
myhost:~ yu$ ps a | egrep erl
  843 s000  S+     0:00.00 egrep erl
```
先程まで走っていた BEAM がいなくなってます。

```console
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 を立ち上げるときにノードとして立ち上げてみましょう。

```console
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 が出てくることがあります。
![beam-q.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260320/5f6d0e41-756f-b166-77c5-644f8e7a48be.png)
これは「許可」で大丈夫です。何しているかはこの先を読むと分かります。

この場合に走っている Elixir/Erlang 関係のUNIXプロセスを見てみます。

```console
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プロセスも見てみます。

```console
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](http://erlang.org/doc/man/epmd.html)
というUNIXプロセスです。ドキュメントを読むと *Hence, epmd maps symbolic node names to machine addresses.* という文があります。Atom で表現された論理的なノード名を、対応するOSカーネル上の物理アドレスに変換するということのようです。

### 再びElixirを終了してみる
今度も iex を抜けてみます。すると

```console
myhost:~ yu$ ps a | egrep erl
 1140 s000  S+     0:00.00 egrep erl
```

と、一見、先程同様に関係するUNIXプロセスがなくなったように見えます。しかしよく見てみると…

```console
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です。

```console
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とで立ち上げます。

```console:bar
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)
```

```console:foo
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プロセスを見てみます。

```console
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パイプの場合には「画面に出力しているのではない」と判断して、省略せずに全部を出力しているようです。

```console
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` でフィルタします。

```console
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)
```

```console
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プロセスとつながっているのでしょうか。

```console
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ソケットを持っていました。

```console
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 側に接続してみます。

```exs:bfo
iex(bar@myhost)1> Node.list
[]
iex(bar@myhost)2> Node.connect(:foo@myhost)
true
iex(bar@myhost)3> Node.list
[:foo@myhost]
```

繋がりました。foo 側からも接続されているのが分かります。

```exs:foo
iex(foo@myhost)1> Node.list
[:bar@myhost]
```

さあではソケットはどうなっているでしょうか。

```console
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に使ってるファイルディスクリプタを見てみましょう。

```console
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のセッションを張るということですね。

```console
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のセッションがあったりするのかと見てみましたが、使っていないようでした。

```console
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周りとかを調べてみたいです。

#参考文献
- [Elixir v.1.9.1 公式ドキュメント Node](https://hexdocs.pm/elixir/Node.html)
- [はじめてなElixir(0)](https://qiita.com/kikuyuta/items/3c1798d15508040ccaaf)