Erlangのノード間通信を理解するために,Erlangの分散プロトコルを実装してErlangノードと通信してみました.(Erlang Advent Calendar用のネタを思いつかなかったので大分前に職場で話した内容です...)
やりたかったこと:
- Erlang Distribution Protocolについての理解を深める
- Rubyから
gen_server:call/2
やrpc:call/4
してみる - Erlang + Ruby + Sinatraで何か作る
Erlangのノード間通信に必要なもの
Erlangノードに接続してメッセージを送受信するためには以下のものが必要です.
- EPMD (Erlang Port Mapper Daemon) : DNS的なやつ
- Erlang Distribution Protocol : ハンドシェイクとメッセージ送受信のプロトコル
- External Term Format : メッセージのシリアライズの仕様
EPMD(Erlang Port Mapper Daemon)
あるホストで起動しているErlangノードがlistenしているポート番号を管理するデーモンです.
Erlangを分散モードで起動すると,自動的に起動して4369番ポートで待ち受けています.
プロトコルの仕様はErlangのドキュメントのDistribution Protocol に書かれています.
TCPで接続してリクエストを送るとレスポンスが返ってくる単純な通信です.認証なども不要で簡単にリクエストを送れます.
- リクエストのメッセージフォーマット: リクエスト長(16bit) + リクエスト
- リクエストの1バイト目がリクエストの種別になっている
種別 | 名前 | 説明 |
---|---|---|
120 | ALIVE2_REQ | ノード登録 |
110 | NAMES_REQ | ノード一覧取得 |
115 | STOP_REQ | ノード抹消(切断時に自動削除されるので通常不要) |
(レスポンスは省略)
リクエストの種別の値のルールは良くわかりませんが,おそらくASCIIコードです.
- NAMES の 'n' (110)
- STOP の 's' (115)
- ALIVE2 が 'x' (120) なのは謎です(単に'a'が埋まってたから?)
ノード一覧取得を取得してみる
1バイトのメッセージ 110(NAMES_REQ) を送ればノードの一覧を取得できます.
適当にErlangを分散モードで起動した状態で,試しにシェルからnetcatで問い合わせてみます.
$ echo -ne "\0x01\x01\x6e" | nc localhost 4369
name node1 at port 46337
node1 が port 46337で起動していることが分わかりました.
Erlang Distoribution Protocol
ノード間通信を始めるためのハンドシェイクの方法と,プロセス間のメッセージの送受信の方法を定めているプロトコルです.link/monitorの方法も定めれています.
プロトコルの仕様はErlangのドキュメントのDistribution Protocol に書かれていますが,足りない情報もあるので https://github.com/erlang/otp/ * /internal_doc あたりにあるドキュメントやErlangのソースを読む必要がありました.
ハンドシェイク
cookieでの認証もこのときに行われます(cookieが違うとハンドシェイクに失敗するのでメッセージを送信できない).
- フォーマット: データ長(16bit) + タイプ(8bit) + 本体
- 認証はチャレンジ&レスポンス
- ハッシュ関数はMD5
メッセージの送受信
- メッセージ長(32bit) + メッセージ
- 2種類の形式があるが,出来ることは同じ
4 Bytes | d Bytes | n Bytes | m Bytes |
---|---|---|---|
Length = d+n+m | DistributionHeader | ControlMessage | Message |
4 Bytes | 1 Bytes | n Bytes | m Bytes |
---|---|---|---|
Length = 1+n+m | Type ('p') | ControlMessage | Message |
- 前者(DistributionHeader あり)の方は少し複雑だがatomをキャッシュできるためサイズを抑えられる
- ControlMessage と Message は External Binary Format(※後述)形式
- ControlMessage はメッセージの種類と宛先プロセスなどの情報が入っています
- Message はErlangプロセスに送信したいメッセージ
rpc:call
を呼ぶ場合以下のようなメッセージが送られます
- ControlMessage = {6, FromPid, '', rex} %% 6 : REG_SEND
- Message = {'$gen_call', {FromPid,Tag}, {call, Mod, Fun, Args, user}}
rpcは rex
という名前のgen_serverが処理するので,そのプロセスにメッセージを送っています.
External Binary Format
term_to_binary/1
, binary_to_term/1
でお馴染みのバイナリフォーマットです.
ドキュメントのExternal Term Formatを読めば良いだけですが,型ごとにデータ長の持ち方等も違うので実装する場合は少し面倒くさいです.
1つの型に複数の表現方法が存在する場合がありますが,互換性のために用意されているもので通信相手がErlangならハンドシェイク時のversionとflagsで有効・無効を切り替えられるため,全てに対応する必要はありません.
Ruby 実装
雑な実装であまり実用的ではないですが,サンプルとしてSinatraでWebサーバ立ててブラウザからErlangのノードの情報を見たり,プロセスのlink/monitorの構造をWebGLで表示できるようにしてみました.
Erlangの関数を呼び出してみる
まずErlangを起動します.
erl -sname node1 -setcookie testtest
irbから上記リポジトリの erlang.rb
をrequireして呼び出します.
irb
irb(main):001:0> require_relative 'erlang'
=> true
irb(main):002:0> erl = Erlang::Erl.new('rubynode@test', "testtest")
=> ...
irb(main):003:0> erl.nodes
=> ["node1"]
irb(main):004:0> erl.rpc_call(erl.nodes[0], :io, :format, ["Hello!\n"])
=> :ok
irb(main):005:0> erl.eval(erl.nodes[0], "1 + 1.")
=> 2
リモートノードでErlangのコードをevalできるのは便利です.個人的に欲しかったので実装しました.
対応した型
基本的な型は一通り対応してみました.mapsとかもRubyのHashと相互変換されます.
TYPE_NEW_FLOAT = 70
TYPE_SMALL_INT = 97
TYPE_INTEGER = 98
TYPE_FLOAT = 99
TYPE_ATOM = 100
TYPE_PORT = 102
TYPE_PID = 103
TYPE_SMALL_TUPLE = 104
TYPE_LARGE_TUPLE = 105
TYPE_NIL = 106
TYPE_STRING = 107
TYPE_LIST = 108
TYPE_BINARY = 109
TYPE_SMALL_BIG = 110
TYPE_LARGE_BIG = 111
TYPE_FUN = 112
TYPE_NEW_REF = 114
TYPE_MAP = 116
WebGLでプロセスを表示してみる
この記事の趣旨とはあまり関係が無い上に何かの役に立つわけでもないものですが,せっかくRubyとErlang間で色々出来るので簡単なWebアプリを作ってみました(普通にErlangで書けば良いだろうと突っ込まれそうですが)
表示サンプル(リンク先は静的ページなのでプロセス殺したりは出来ません):
- Cowboy http://binzume.github.io/erlprocvis/public/procs.html?procs=sample.json
- RabbitMQ http://binzume.github.io/erlprocvis/public/procs.html?procs=sample-mq.json
色々なErlangアプリケーションを起動してプロセスのlinkの関係を見ているだけでもけっこう面白いです.
参考にしたもの
- http://www.erlang.org/doc/
- 重要な部分が省略されていたりするので,分からないことがあったら internal_doc を探すのが良い
- https://github.com/erlang/otp/ * /internal_doc にあるドキュメント
- Akka と Erlang を Reactive に組み合わせるためのライブラリを作りました(okumin)