[Elixir in Action]Elixirで分散システムを構成するための諸要素を知る

ここら辺がこの書籍の真骨頂でしょうか。
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と表現)

  1. ClientがServerのPIDを取得する
  2. ClientがServerにmessageを送る

そのため、まずはPIDを取得する必要があります。
PIDのlookupは基本的にlocalで行われます。そのため、目的のprocessを自身のlocalで見つけること自体は高速に行われます。

PIDにはルールがあって、以下のようになっています。

  1. どのnode上にあるprocessか(localなら、ゼロ)
  2. local内でユニークなnodeの番号
  3. ↑のnodeの番号が表現可能な範囲を超えると増加する
      1   2   3
#PID<8084.81.0>

Global関数を使って広範囲でClusterを構成する

Global関数を使って、globalな領域でclusterを組むことができます。このglobalは、Erlangですでに用意されている関数です。

Elixirだと、GenServerなんかで start_link するときとか、 :global 指定で起動することができます。

https://github.com/elixir-lang/elixir/blob/e74852ffcc95872915be2b1aae453e74d6c54325/lib/elixir/lib/gen_server.ex#L132

実際に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に対して使ってきた monitorlink もこれまで同様にnodeに対しても使えます。nodeのPIDで結びましょう。

他、このようなnode間を結ぶ関数として :rpc もあります。Remote Procesure Callのようです。
http://erlang.org/doc/man/rpc.html

このようなメッセージのやり取りをしていると、特に大きmessageとか処理をやり取りするときも考えると deadlockslivelocksstarvation に遭遇することがあります。最終的には色々設計からそうならないようにしましょう、ということが必要なのですが、ここら辺は分散システムや並行処理で少なからず考えないといけないところですね…

Cluster design

Clusterは、以下を目標に設計されていきます。

  1. Clusterは複数のnodeから構成され、それらはすべて同じcode、サービスを提供する
  2. 変更は、Clusterのnode全体に伝搬される
  3. Clusterに属する1つのnodeがクラッシュしても、Cluster内の他のnodeは正しく動作し続ける

そのために、ネットワークやら、CAP定理の話やらが書かれています。

ネットワークも関係するため、以下モジュールの紹介もさっとありました

ちょっとしたnodeの補足

node1@localhostlocalhost がネットワークのアドレスになると、この関係はネットワークをまたいだ関係になります。

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間の通信同様に sendreceive が使われる
  • BEAMには、:global:rpc といった仕組みがすでにあって、それを使うとよい
  • node間の通信では、 cast(非同期) よりも call(同期) を使うと良い。
  • ネットワークの分離をちゃんとしましょう

ここまでザーッと学んだわけですが、何かXMPPのクラスタ構成とか、その仕組みを思い出しました… :pg2 で書かれてた、broadcastとか、初めのnode discoveryの仕組み見たい。

広告

コメントを残す

以下に詳細を記入するか、アイコンをクリックしてログインしてください。

WordPress.com ロゴ

WordPress.com アカウントを使ってコメントしています。 ログアウト / 変更 )

Twitter 画像

Twitter アカウントを使ってコメントしています。 ログアウト / 変更 )

Facebook の写真

Facebook アカウントを使ってコメントしています。 ログアウト / 変更 )

Google+ フォト

Google+ アカウントを使ってコメントしています。 ログアウト / 変更 )

%s と連携中