ここら辺がこの書籍の真骨頂でしょうか。
Chapter11ではよく知られたOTP applicationを作るためのファイル構成とか、書き方な話なのですっ飛ばして、Chapter12のメモ。
ざっと読んで、触った感覚としては、分散システムは無知では手を出さないほうが良いかなーということ。色々考慮漏れで溢れそうな予感…
Erlangベースのシステムでは、processとmessageによって分散システムを構成します。(伝統的なRPCと混同しないように)
BEAMによる分散システムは、複数のnodeがそれぞれ接続されてクラスタ化されることで実現されます。このnodeは、BEAMインスタンスと呼ばれます。
Nodeの接続
以下のようにして、簡単なnodeを立てて接続、クラスタを組むことができます。
node1
$ iex --sname node1@localhost iex(node1@localhost)1> Node.list # Node.connect :node1@localhost の後 [:node2@localhost]
node2
$ iex --sname node2@localhost iex(node2@localhost)1> node :node2@localhost iex(node2@localhost)2> Node.connect :node1@localhost true iex(node2@localhost)3> node :node2@localhost iex(node2@localhost)4> Node.list [:node1@localhost]
node3
node1とnode2の後に以下を実施。
$ iex --sname node3@localhost iex(node3@localhost)1> Node.connect :node1@localhost true iex(node3@localhost)2> Node.list [:node1@localhost, :node2@localhost]
Clusterをすでに構築しているnodeに接続すると、自動的にそのCluster内の他nodeとの接続も行われます。これは、tick messageと呼ばれるメッセージのやり取りが行われるためです。これにより、Clusterに含まれるnodeの生存確認も行います。すでにdisconnectな状態のnodeがあれば、それは Node.list から除かれます。
Standard I/Oの実行と出力先
以下の通り、node1で実行した内容を、 Node.spawn でnode2に渡すと、その処理はnode2で行われ、結果をnode1で表示する、ということができます。
iex(node1@localhost)> Node.spawn :node2@localhost, fn -> IO.puts "hello #{node}" end
#PID<8084.81.0>
hello node2@localhost
これは、すべてのstandard I/Oの出力はClusterのgroup leaderに渡されるためです。group leaderは、処理をinputされたnodeで、ここではnode1を指します。
ここで送るmessageには特に制限はないとのこと。このmessageは、 :erlang.termi_to_binary/1 でエンコードして送られて、受け取ったnodeは :erlang.binary_to_term/1 でデコードするそうです。
Cluster構成の前に
nodeが互いにやり取りを行うにあたり、必ず以下の操作が必要になります。(送信元をclient、送信先をserverと表現)
- ClientがServerのPIDを取得する
- ClientがServerにmessageを送る
そのため、まずはPIDを取得する必要があります。
PIDのlookupは基本的にlocalで行われます。そのため、目的のprocessを自身のlocalで見つけること自体は高速に行われます。
PIDにはルールがあって、以下のようになっています。
- どのnode上にあるprocessか(localなら、ゼロ)
- local内でユニークなnodeの番号
- ↑のnodeの番号が表現可能な範囲を超えると増加する
1 2 3
#PID<8084.81.0>
Global関数を使って広範囲でClusterを構成する
Global関数を使って、globalな領域でclusterを組むことができます。このglobalは、Erlangですでに用意されている関数です。
Elixirだと、GenServerなんかで start_link するときとか、 :global 指定で起動することができます。
実際にglobalを使うと、以下のような形でregisterとwhereisでPIDを得ることができます。
iex(node1@localhost)12> :global.register_name({:todo_list, "bob"}, self)
:yes
iex(node1@localhost)14> :global.whereis_name({:todo_list, "bob"})
#PID<0.64.0>
以下のように、なんらかの他nodeとリンクを張っていると以下のような情報が取得できます。
iex(node1@localhost)34> :global.info
{:state, true, [:node2@localhost], [:node2@localhost], [], [], :nonode@nohost,
#PID<0.14.0>, #PID<0.15.0>, :no_trace, false}
pg2関数を使ってグループに分ける
:pg2関数を使ってグループを作ることもできます。このpg2関数では、同じエイリアス( :doro_list というところ)に複数のnodeをぶら下げることにより、1つのエイリアスに対して複数のnodeをグループ化できます。
ここでは、node1で元となるprocessを作り、そこにnode2とnode1が順に参加、グループとなる例を示しています。
iex(node1@localhost)16> :pg2.start
{:ok, #PID<0.93.0>}
iex(node1@localhost)17> :pg2.create({:doro_list, "bob"})
:ok
iex(node1@localhost)18> :pg2.get_members({:doro_list, "bob"})
[#PID<8084.64.0>]
iex(node1@localhost)19> :pg2.join({:doro_list, "bob"}, self)
:ok
iex(node1@localhost)20> :pg2.get_members({:doro_list, "bob"})
[#PID<0.64.0>, #PID<8084.64.0>]
iex(node1@localhost)21>
iex(node2@localhost)7> :pg2.start
{:ok, #PID<0.85.0>}
iex(node2@localhost)8> :pg2.which_groups
[doro_list: "bob"]
iex(node2@localhost)9> :pg2.join({:doro_list, "bob"}, self)
:ok
iex(node2@localhost)10> :pg2.which_groups
[doro_list: "bob"]
iex(node2@localhost)11> :pg2.get_members({:doro_list, "bob"})
[#PID<9003.64.0>, #PID<0.64.0>]
ここで、例えば同じグループの中で一番距離が近いprocessに処理をお願いしたいとき、以下によりPIDを取得できます。
iex> :pg2.get_closest_pid({:doro_list, "bob"})
#PID<0.64.0>
これは、なんらかの形でnodeをカテゴリわけして、そのカテゴリ全体にbroadcastしたい!といった用途で使われます。
いままでprocessに対して使ってきた monitor や link もこれまで同様にnodeに対しても使えます。nodeのPIDで結びましょう。
他、このようなnode間を結ぶ関数として :rpc もあります。Remote Procesure Callのようです。
http://erlang.org/doc/man/rpc.html
このようなメッセージのやり取りをしていると、特に大きmessageとか処理をやり取りするときも考えると deadlocks 、livelocks 、 starvation に遭遇することがあります。最終的には色々設計からそうならないようにしましょう、ということが必要なのですが、ここら辺は分散システムや並行処理で少なからず考えないといけないところですね…
Cluster design
Clusterは、以下を目標に設計されていきます。
- Clusterは複数のnodeから構成され、それらはすべて同じcode、サービスを提供する
- 変更は、Clusterのnode全体に伝搬される
- Clusterに属する1つのnodeがクラッシュしても、Cluster内の他のnodeは正しく動作し続ける
そのために、ネットワークやら、CAP定理の話やらが書かれています。
ネットワークも関係するため、以下モジュールの紹介もさっとありました
- net_kernel
- net_adm.html
ちょっとしたnodeの補足
node1@localhost の localhost がネットワークのアドレスになると、この関係はネットワークをまたいだ関係になります。
nodeはhiddenな要素を加えることができます。その場合、Node.listで見つけることはできません。例えば、node1とnode2、node3があって、node3だけ --hiddne 要素を付加して iex --sname で起動します。その後、互いに Node.connect/1 を行うと、以下のように各属性によって表示される要素が変わってきます。
iex(node1@localhost)22> Node.list [:node2@localhost] iex(node1@localhost)23> Node.list :connected [:node2@localhost, :node3@localhost] iex(node1@localhost)24> Node.list :hidden [:node3@localhost] iex(node1@localhost)25> Node.list :visible [:node2@localhost]
参考
https://github.com/elixir-lang/elixir/blob/v1.0.5/lib/elixir/lib/node.ex#L77
nodeは、ネットワーク上をTCPで接続する。
Secutiry
Erlangのシステムは、trusted environment(信頼された環境)下で運用されることを前提に作られているようです。そのため、BEAMインスタンスも限られた領域で運輸することが大事とのこと。
なので、nodeでクラスタを構成する場合はFWやACLなど含め、ネットワークを分離したり、AWSでいうavailability zoneや、社内/社外ネットワークやDMZといった区分など、そこらへんの設計をちゃんとしておくことが必要ですね。
締め
- 分散システム(distributed system)は耐障害性(fault tolerance)性を高めることができる
- クラスタ構成はスケールアウトを提供する
- BEAMインスタンス同士、TCPのコネクションを貼ることでクラスタを構成できる
- コネクションが切れたら、disconnectな状態になる
- クラスタを構成するnode間の通信は、process間の通信同様に
sendやreceiveが使われる - BEAMには、
:globalや:rpcといった仕組みがすでにあって、それを使うとよい - node間の通信では、
cast(非同期) よりもcall(同期) を使うと良い。 - ネットワークの分離をちゃんとしましょう
ここまでザーッと学んだわけですが、何かXMPPのクラスタ構成とか、その仕組みを思い出しました… :pg2 で書かれてた、broadcastとか、初めのnode discoveryの仕組み見たい。