分散

この記事は英語の原文を日本語に翻訳したものです。原文: https://blog.stenmans.org/theBeamBook/#CH-Distribution

翻訳元: happi/theBeamBook 7998e22e78417dbe20e5136b9aee862a1ecaa404(コミット 7998e22

Erlangは、分散コンピューティングを前提に設計された言語です。 本章が扱うのは、ノード、コネクション、メッセージパッシングといった、Erlangにおける分散の主要な側面です。

ノードとコネクション

Erlangにおけるノードとは、Erlangランタイムシステムの1インスタンスを指します。 仕様上はより厳密で、名前付きのERTSインスタンスだけが「ノード」であり、名前付きインスタンス同士だけがErlangの分散機構を通じて通信できます。

本書では、多少不正確ではありますが、分散されていない無名のインスタンスもノードと呼ぶことがあります。 そうしたインスタンスも、HTTP経由やTCPを直接使うことで、他のサブシステムと通信できます。

実ノードにはそれぞれ一意な名前が付いており、通常はアトムと「@」記号、ホスト名を組み合わせた形になります。 たとえばnode1@localhostのようになります。

ノードの名前には、短縮名と長い名前の2種類があります。 短縮名はアトムだけで構成され、ホスト名を含みません(node1など)。 すべてのノードが同一ホスト上で動作している場合に使われます。 一方、長い名前はアトムとホスト名の両方を含みます(node1@localhostなど)。 異なるホストにノードが分散している場合に使われます。 短縮名を使うノードは短縮名を使う他のノードとしか通信できず、長い名前を使うノードは長い名前を使う他のノードとしか通信できない点に注意してください。 分散システム内のすべてのノードで、命名方式を一貫させることが重要です。

ノードはnet_kernel:connect_node/1関数を使って互いに接続を確立できます。 ノードの名前は名前(アトム)とホスト名の2つの部分から構成され、書式はname@hostnameです。

名前を付けずにノードを起動した場合、他のノードに接続できません。

happi@gdc12:~$ iex
Erlang/OTP 24 [erts-12.0.4] [source] [64-bit] [smp:12:12] [ds:12:12:10] [async-threads:1] [jit]

Interactive Elixir (1.12.2) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> Node.alive?
false
iex(2)> :net_kernel.connect_node(:foo@gdc12)
:ignored
iex(3)> Node.alive?
false
iex(4)>

名前を付けてノードを起動すれば、他のノードに接続できます。

happi@gdc12:~$ iex --sname bar
Erlang/OTP 24 [erts-12.0.4] [source] [64-bit] [smp:12:12] [ds:12:12:10] [async-threads:1] [jit]

Interactive Elixir (1.12.2) - press Ctrl+C to exit (type h() ENTER for help)
iex(bar@gdc12)1> Node.alive?
true
iex(bar@gdc12)2> :net_kernel.connect_node(:foo@gdc12)
true
iex(bar@gdc12)3> Node.alive?
true
iex(bar@gdc12)4>

ノードの名前を自分で決めたくない場合は、特別な名前undefinedを付けて起動できます。 この場合、他のノードに接続した時点でシステムが名前を割り当てます。

happi@gdc12:~$ iex --sname undefined
Erlang/OTP 24 [erts-12.0.4] [source] [64-bit] [smp:12:12] [ds:12:12:10] [async-threads:1] [jit]

Interactive Elixir (1.12.2) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> Node.alive?
false
iex(2)> :net_kernel.connect_node(:foo@gdc12)
true
iex(2YOVGWCSCSR8R@gdc12)3> Node.alive?
true
iex(2YOVGWCSCSR8R@gdc12)4>

namesnameフラグを付けずにシステムを起動した場合でも、分散機構を手動で開始することは可能です。

Eshell V15.2.2 (press Ctrl+G to abort, type help(). for help)
1> node().
nonode@nohost
2> net_kernel:start([baz, shortnames]).
{ok,<0.92.0>}
(baz@gdc12)3> node().
baz@gdc12

ノード同士が接続できるのは、同じ「クッキー」を使っている場合に限られます。 クッキーは、認可されたノードだけが接続できるようにするためのセキュリティ機構です。 クッキーは単なるアトムで、互いに接続すべきすべてのノードで一致している必要があります。 クッキーが一致しなければ、接続は確立されません。

クッキーは、Erlangノードを起動するときに-setcookieオプションで設定するか、erlang:set_cookie/2関数を使って動的に設定できます。 クッキーを設定しない場合、Erlangノードは起動時にランダムなアトムをデフォルトのマジッククッキーとして割り当てられ、他のノードのクッキーはnocookieとみなされます。 このマジッククッキーは、ノード間の基本的な認証機構として機能します。

Erlangのネットワーク認証サーバ(auth)は、最初にユーザーのホームディレクトリにある.erlang.cookieというファイルを探し、続いてfilename:basedir(user_config, "erlang")が指すディレクトリを探します。

どちらのファイルも存在しない場合、authはユーザーのホームディレクトリに.erlang.cookieファイルを新規作成します。 このファイルにはユーザーの読み取り専用というUNIXパーミッションが設定され、内容にはランダムに生成された文字列が入ります。

続いて、.erlang.cookieファイルの内容からアトムCookieが生成されます。 ローカルノードのクッキーは、erlang:set_cookie(Cookie)関数によってこのアトムに設定されます。 この操作によって、ローカルノードが他のすべてのノードとやり取りする際に使うデフォルトのクッキーが確立されます。

こうした一連の処理によって、分散Erlangシステムでノード同士が通信する際の基本的なセキュリティと認証が確保されます。

接続が確立されると、ノード同士は自由に通信できるようになります。 ノード間の接続は透過的です。 つまり、接続済みであれば、別のノード上のプロセスへメッセージを送る操作は、同じノード上のプロセスへ送る場合と変わりません。

Erlangの分散では、デフォルトですべてのノードが互いに接続されたフルメッシュになります。 ノードN1が別のノードN2に接続すると、N1はN2が接続しているすべてのノードの一覧を受け取り、それらすべてに接続します。 接続は双方向なので、N2もN1が接続しているすべてのノードに接続します。

この挙動は、システム起動時にコマンドラインフラグ-connect_all falseを指定すれば無効にできます。

Erlangはノード間のSSL接続もサポートしています。 信頼できないネットワーク越しに通信する場合など、ノード間通信を保護したいときに有用です。

ノード接続にSSLを使うには、sslアプリケーションとinet_distのlisten・connectオプションを設定する必要があります。 これには、SSL証明書と鍵を用意し、ノード間通信にSSLを使うようErlangランタイムシステムを設定する作業が伴います。

SSLの利用は暗号化・復号のオーバーヘッドにより性能へ影響することを踏まえておいてください。 そのため、安全な通信の利点が性能面のコストを上回る場合に限って使うべきです。

Erlangの隠しノードは、ノードのネットワークに完全には参加せずに、分散Erlangシステム内で接続を確立できる特殊な種類のノードです。

-hiddenオプションを付けて起動されたノードは、他のノード上でnodes()関数を呼んでも一覧に現れず、システム内の他のノードへ自動的に接続することもありません。 ただし、net_kernel:connect_node/1を使って個々のノードへ接続することはでき、その接続はメッセージパッシングやプロセスの起動など、他の分散操作にも完全に使えます。

隠しノードの主な用途の一つは、ネットワークの他の部分から隔離された接続を作ることです。 たとえば、システム内のすべてのノードに接続することなく、特定のノード群だけに接続したい場合に使えます。 ネットワークトラフィックの管理や、特定の操作の隔離、大規模な分散システム内でのサブネットワークの構築に役立ちます。

隠しノードのもう一つの用途は、ノードのネットワーク全体が大規模かつ動的で、各ノードが他のすべてのノードとの接続を維持するのが現実的でない、あるいは望ましくない場合です。 隠しノードを使うことで、より柔軟でスケーラブルなネットワークトポロジーを構築できます。

隠しノードはセキュリティ機能ではない点に注意してください。 nodes()の一覧に現れず、他のノードへ自動的に接続しないとはいえ、不正アクセスや盗聴に対する追加の保護は何も提供しません。 分散Erlangシステムを保護する必要があるなら、クッキーによる認証やSSL/TLS暗号化といった機能を使うべきです。

コマンドラインフラグや役立つモジュール・関数を含め、Erlangレベルでの分散の完全な説明は、リファレンスマニュアルのDistributed Erlangを参照してください。

ここからは、もっと踏み込んだ話に入ります。BEAMの内部で分散がどう実装されているかを取り上げます。

Erlang分散の仕組み

Erlangは、ノード間の通信にErlang分散プロトコルと呼ばれる独自のプロトコルを使います。 このプロトコルはERTSによって実装されており、ノード間のすべての通信に使われます。

分散プロトコルは、プロセスメッセージ、システムメッセージ、コントロールメッセージなど、複数の種類のメッセージをサポートします。 プロセスメッセージはErlangプロセス間の通信に使われ、システムメッセージはVMの各部分の間の通信に使われます。 コントロールメッセージは、接続の確立や切断など、分散システムの状態を管理するために使われます。

Erlang Port Mapper Daemon(EPMD)

Erlang Port Mapper DaemonEPMD)は、Erlangノード間の接続確立を助ける小さなサーバです。 Erlangの分散機構において重要な役割を担っています。

Erlangノードが名前付きで起動されると(-nameまたは-snameオプションを使用)、EPMDのインスタンスがまだ動いていなければ自動的に起動されます。 これはノード自身が起動する前に、Erlangランタイムシステム(ERTS)によって行われます。

EPMDプロセスは、Erlang VMとは独立したオペレーティングシステムの別プロセスとして動作します。 そのため、起動元のErlangノードが停止してもEPMDは動き続けます。 同じホスト上で複数のErlangノードが動作している場合、それらはすべて同じEPMDインスタンスを使います。 EPMDはデフォルトで4369番ポートをリッスンします。

EPMDの主な役割は、ノード名とTCP/IPポート番号を対応付けることです。 Erlangノードが起動すると、他のノードからの接続を受け付けるTCP/IPのリッスンポートを開き、続いて自身の名前とポート番号をEPMDに登録します。

あるノードが別のノードへの接続を確立しようとするとき、まず(相手ホスト上の)EPMDに問い合わせ、相手ノードの名前に対応するポート番号を尋ねます。 EPMDがポート番号を返すと、ローカルノードはそのポート番号を使って相手ノードへTCP/IP接続を開けます。

EPMDのソースコードは、GitHub上のErlang/OTPリポジトリのerts/epmd/srcディレクトリにあります。 実装は比較的シンプルで、主なロジックは単一のCファイル(epmd_srv.c)に収められています。

EPMDサーバは単純なループで動作し、着信を待ち受けてリクエストを処理します。 リクエストを受け取るとその内容を解析し、ノードの登録や登録解除、ノードのポート番号の問い合わせといった適切な処理を行います。

Erlang分散プロトコル

EPMDとErlangノードの間の通信には、シンプルなバイナリプロトコルが使われます。 メッセージは小さく固定フォーマットになっており、プロトコルの実装が容易で効率的に使えるようになっています。

プロトコルの詳細はERTSリファレンス: 分散プロトコルに記載されています。

Erlang分散プロトコルは、異なるErlangノード間の通信を支える基盤のプロトコルです。 分散Erlangシステムの要件のために特別に設計された独自プロトコルです。

あるノードが別のノードへの接続を確立しようとすると、ハンドシェイクの処理が始まります。 この処理では、通信プロトコルのバージョン、ノード名、分散フラグといったパラメータについて両ノードが合意するために、一連のメッセージがやり取りされます。

ハンドシェイクは、接続を開始する側のノードが対象ノードへSEND_NAMEメッセージを送ることから始まります。 このメッセージには、プロトコルバージョンと送信元ノードの名前が含まれます。

対象ノードは、接続を受け入れる場合はALIVE_ACKメッセージを、拒否する場合はNACKメッセージを返します。 ALIVE_ACKメッセージには、対象ノード自身の名前と、認証に使うランダムな数値であるチャレンジが含まれます。

接続を開始した側のノードは、続いてCHALLENGE_REPLYメッセージを返します。 このメッセージには、チャレンジと共有の秘密情報(マジッククッキー)を使った計算結果が含まれ、対象ノードはこの結果を検証することで開始側ノードを認証します。

最後に、対象ノードがCHALLENGE_ACKメッセージを送ってハンドシェイクが完了します。 この時点で接続が確立され、両ノードはメッセージのやり取りを始められます。

Erlang分散プロトコルは、次のような複数種類のメッセージをサポートします。

  • コントロールメッセージ:分散システムの状態を管理するために使われます。プロセスのリンク・アンリンク、モニタリングとその解除、EXITKILLといったシグナルの送信などが含まれます。
  • データメッセージ:プロセス間でデータを送るために使われます。項データの送信やリモートプロシージャコール(RPC)の実行が含まれます。
  • システムメッセージ:Erlang VMの異なる部分の間の通信に使われます。分散コントローラやポートマッパーデーモン(EPMD)の管理に関するメッセージが含まれます。

代替の分散方式

Erlangのデフォルトの分散機構が、特定のシステムの要件をすべて満たせない場合もあります。 そうした場面で選択肢になるのが代替の分散方式です。

代替の分散機構を使いたくなる理由としては、次のようなものが考えられます。

  • 性能:組み込みの分散機構はTCP/IPを通信に使いますが、特定のワークロードやネットワーク構成にとって最も効率的な選択とは限りません。代替の分散機構では、別のプロトコルや独自のデータフォーマットを使って性能を改善できます。
  • セキュリティ:Erlangの分散機構にはマジッククッキーによる認証などの基本的なセキュリティ機能が備わっていますが、一部のアプリケーションが求める水準には届かない場合があります。代替の分散機構では、暗号化やアクセス制御といった追加のセキュリティ機能を組み込めます。
  • 信頼性の強化:Erlangの分散機構はフォールトトレランスを念頭に設計されており、ノード障害やネットワーク分断にも対応できます。それでも、さらなる信頼性を求める場面はあります。代替の分散機構であれば、ネットワーク分断への対応をより洗練させたり、メッセージ配送についてより強い保証を与えたり、エラーの検知と復旧を強化したりできます。こうした強化はあくまで状況に応じたものであり、Erlangがすでに備えている堅牢な信頼性機能を補うものです。
  • 相互運用性:異なる通信プロトコルを使う他システムとErlangシステムを統合する必要がある場合、代替の分散機構が相互運用性を提供します。これはおそらく最も一般的なユースケースです。CやScalaで書かれた他のプログラムと、Erlangのメッセージやリモートプロシージャコールを使ってやり取りできることには大きな力があります。

Erlangで代替の分散を実装する方法にはいくつかあります。

  • カスタム分散ドライバ:分散プロトコルを実装するカスタムドライバをCで書く方法です。ネットワークプロトコルやデータフォーマットといった通信の低レベルな詳細を自分で制御できます。
  • 分散コールバックモジュール:接続の確立・切断やメッセージの送受信といった分散関連のイベントを扱うコールバックモジュールをErlangで書く方法です。分散ドライバより高いレベルでカスタムの振る舞いを実装できます。
  • サードパーティライブラリ:Erlang向けの代替の分散機構を提供するサードパーティライブラリを使う方法です。こうしたライブラリは通常、低レベルの詳細を抽象化した高レベルのAPIを分散通信のために提供します。

Erlangで代替の分散を実装するには、いくつかの手順を踏みます。

  1. 分散コードの記述:Cで書いた分散ドライバや、Erlangで書いたコールバックモジュール、あるいはその組み合わせになります。このコードは、ハンドシェイクの処理やコントロールメッセージ・データメッセージの処理を含め、Erlang分散プロトコルを実装する必要があります。
  2. Erlang VMの設定:代替の分散機構を使うようVMを設定します。これは、VM起動時に特定のコマンドラインオプションを渡すことで行います。たとえばカスタム分散ドライバを使う場合は、-proto_distオプションに続けてドライバの名前を渡します。
  3. 分散機構のテスト:分散機構を実装して設定したら、正しく動作することを確認するためにテストします。接続の処理、メッセージの送受信、エラー処理など、分散機構のあらゆる機能を検証します。

Erlangのドキュメントには、代替キャリアの実装方法を扱った章があります。

代替のノード発見方法の実装を扱った章もあります。

分散Erlangにおけるプロセス

すでに見てきたとおり、Erlangのプロセスはプロセス識別子(PID)によって識別されます。 PIDには、プロセスが動作しているノード、インデックス、シリアルという情報が含まれます。 インデックスはプロセステーブル内のプロセスへの参照であり、シリアルは同じインデックスを持つ古い(死んだ)プロセスと新しい(生きている)プロセスを区別するために使われます。

分散Erlangでは、PIDが所属先のノードの情報を持ちます。 これは分散システムにおけるメッセージパッシングで重要な意味を持ちます。 あるPIDへメッセージを送るとき、ERTSはそのPIDがローカルノードのものか、リモートノード上のプロセスのものかを知る必要があるからです。

Erlangシェル上でPIDを表示すると、<node.index.serial>という書式になります(<0.10.0>など)。 ノードIDの0はローカルノードを表します。

あるノードから別のノードへメッセージが送られるとき、メッセージに含まれるローカルPIDはErlangランタイムシステムによって自動的にリモートPIDへ変換されます。 この変換は、関係するプロセスにとって透過的です。 プロセスの側から見れば、単にPIDを使ってメッセージを送受信しているにすぎません。

この変換では、PID内のローカルノード識別子0が実際のノード識別子に置き換えられます。 プロセスの一意な番号自体は変わりません。 この変換はterm_to_binary/1によって行われます。

メッセージを受信するときは、受信側プロセスにメッセージが届けられる前に、メッセージ内のリモートPIDがローカルPIDへ変換し直されます。 この変換では、ノード識別子が0に置き換えられ、世代番号が取り除かれます。

このPIDの自動変換によって、Erlangのプロセスは分散機構の詳細を意識することなく、ノードをまたいで透過的に通信できます。

あるPIDへメッセージが送られると、ERTSはPID内のインデックス部分を使ってプロセステーブルからプロセスを探し出し、そのプロセスのメッセージキューにメッセージを追加します。

プロセスが終了すると、プロセステーブル内のそのエントリは解放済みとしてマークされ、PIDのシリアル部分がインクリメントされます。 これにより、新しいプロセスが同じインデックスを再利用して作られても、異なるPIDを持つことが保証されます。

分散Erlangでは、PIDの扱いがもう少し複雑になります。 リモートノード上のPIDへメッセージが送られる場合、ローカルのERTSはメッセージを届けるためにリモートノード上のERTSと通信する必要があり、これはErlang分散プロトコルを使って行われます。

  1. Erlangノード1が、たとえばspawn/4を通じてspawnリクエストを開始します。
  2. このリクエストはノード1上のERTSによって処理されます。
  3. ERTSは分散プロトコル経由でSPAWN_REQUESTメッセージを送信します。OTP 23以降では{29, ReqId, From, GroupLeader, {Module, Function, Arity}, OptList}に続けてArgListが送られます。
  4. このメッセージはノード2上のERTSで受信されます。
  5. ノード2上のERTSは、Erlangノード2上でspawnリクエストを開始します。
  6. ノード2はModule:Function(ArgList)を呼び出す新しいプロセスを作成します。
  7. ノード2上のERTSは、分散プロトコル経由でSPAWN_REPLYメッセージ{31, ReqId, To, Flags, Result}を送り返します。Flagsパラメータは各ビットが特定のフラグを表すビットフィールドで、これらのフラグはビットごとのORで組み合わされます。現在定義されているフラグは次のとおりです。
    • フラグ1:起点となったプロセス(To)と、新しく生成されたプロセス(Result)との間にリンクが張られている場合に立ちます。このリンクは、新しいプロセスが存在するノード上で設定されます。
    • フラグ2:起点となったプロセス(To)から、新しく生成されたプロセス(Result)へのモニタが設定されている場合に立ちます。このモニタも、新しいプロセスが存在するノード上で設定されます。
  8. このメッセージはノード1上のERTSで受信されます。
  9. 最後に、ノード1上のERTSが呼び出し元にPIDを返します。

分散Erlangにおけるリモートプロシージャコール

リモートプロシージャコール(RPC)は、分散Erlangの基本的な要素です。 あるノード上のプロセスが、まるでローカルの関数呼び出しであるかのように、別のノード上の関数を呼び出せるようにします。 実装の詳細は次のとおりです。

最も基本的なレベルでは、Erlangにおける RPC はrpc:call/4関数を使って行われます。 この関数は、リモートノードの名前、呼び出す関数を含むモジュールの名前、関数の名前、関数に渡す引数のリストという4つの引数を取ります。

RPCの例を示します。

Result = rpc:call(Node, Module, Function, Args).

この関数が呼ばれると、次の手順が実行されます。

  1. 呼び出し元のプロセスが、リモートノード上のrexサーバプロセスへメッセージを送ります。このメッセージには関数呼び出しの詳細が含まれます。
  2. リモートノード上のrexサーバがメッセージを受け取り、指定された関数を新しいプロセス内で呼び出します。
  3. 関数はリモートノード上で実行を完了し、その結果がメッセージとして呼び出し元プロセスへ送り返されます。
  4. rpc:call/4関数が結果のメッセージを受け取り、呼び出し元に結果を返します。

rexサーバはすべてのErlangノードに標準で備わっており、受信したRPCリクエストの処理を担います。 その名前は「Remote EXecution」に由来します。

rexサーバはRPCリクエストを受け取ると、それを処理する新しいプロセスをspawnします。 このプロセスが要求された関数を呼び出し、結果を呼び出し元へ送り返します。 関数が例外を投げた場合は、その例外が捕捉され、エラーとして呼び出し元へ返されます。

RPCに使われるメッセージは通常のErlangメッセージであり、送信には標準のErlang分散プロトコルが使われます。 そのため、RPCはセレクティブレシーブやパターンマッチングといった、Erlangのメッセージパッシング機構が持つあらゆる機能を活かせます。

Erlangは、同期的なrpc:call/4関数に加えて、非同期のRPC機構も提供しています。 これはrpc:cast/4関数を使って行われ、動作はrpc:call/4に似ていますが、結果を待ちません。 代わりに、リモートノードへリクエストを送るとすぐにnoreplyを返します。

非同期RPCは、呼び出し元が結果を待つ必要がない場合や、呼び出す関数が意味のある結果を返さない場合に役立ちます。

大規模システムにおける分散

システムが大きくなるにつれて、ノード間の接続数は指数関数的に増える可能性があります。 とくに、すべてのノードがすべてのノードに接続するというデフォルト設定のもとでは、その傾向が顕著です。 この増加はネットワークトラフィックの急増を招き、接続を管理し性能を維持するシステムの能力を圧迫しかねません。

分散システムでは、データがネットワークを越えて移動する必要があります。 あるノードから別のノードへデータが届くまでの時間、すなわちネットワークレイテンシは、とくにノードが地理的に離れている場合にシステムの性能へ影響します。

Erlangの非同期メッセージパッシングモデルはネットワークレイテンシをうまく扱えます。プロセスはメッセージを送った後に応答を待つ必要がなく、他の処理を続けられるからです。 それでも、地理的に分散したシステムでErlangの分散機構を使うことは推奨されません。 Erlangの分散は、データセンター内、できれば同じラック内での通信を想定して設計されています。 地理的に分散したシステムには、他の非同期通信のパターンを使うことが勧められます。

大規模システムでは、障害は避けられません。 ノードがクラッシュしたり、ネットワーク接続が失われたり、データが壊れたりすることがあります。 システムは、大きなダウンタイムを伴わずにこうした障害を検知し、そこから復旧できなければなりません。

これには、組み込みの障害検知・復旧機構で対処できます。 スーパービジョンツリーによって、システムはプロセスの障害を検知し、失敗したプロセスを自動的に再起動できます。

複数のノードにまたがってデータの一貫性を保つことは、大きな課題です。 あるノードでデータが更新されると、その変更はそのデータのコピーを持つ他のすべてのノードへ伝播させる必要があります。 これに対処する一つの方法は、分散させる必要がある状態そのものを避けることです。 可能であれば、真の状態はデータベースなど一箇所だけに保持します。

Erlangは、データの一貫性を管理するためのツールやライブラリをいくつか提供しています。 分散データベース管理システムであるMnesiaもその一つです。 Mnesiaはトランザクションをサポートし、複数のノードにまたがってデータを複製できます。 ただし、ネットワーク分断やノード再起動の後にMnesiaがデフォルトで行う同期処理は、よほど小さなテーブルでない限りコストが高すぎるという難点があります。 詳しくはMnesiaの章で扱います。 大規模なデータセットには実績のある高性能なACID準拠のSQLデータベースを、イベント処理にはメッセージキューを使うことが、多くの場合で推奨されます。

Dist Port

Erlangの分散機構は、ノード間通信バッファと呼ばれるバッファを使います。 デフォルトのサイズは128MBで、ほとんどのワークロードにとって妥当な値です。 ただし環境によってはノード間トラフィックが非常に重く、バッファの容量に達してしまうことがあります。 バッファに収まらないほど大きなメッセージ(数百メガバイト規模など)を転送するワークロードも、デフォルト値では合わない例です。

そうした場合は、+zdbblというVMフラグでバッファサイズを増やせます。値の単位はキロバイトです。

erl +zdbbl 192000

バッファが容量の上限に近づくと、ノードは分散ポートのビジー状態(busy_dist_port)を伝える警告をログに出力します。

2023-05-28 23:10:11.032 [warning] <0.431.0> busy_dist_port <0.324.0>

バッファサイズを増やすと、スループットの向上やレイテンシの低減につながることがあります。 こうした性能問題を見つけて対処するには、Erlangシステムを定期的に監視することが重要です。 etop:observerアプリケーションといったツールは、Erlangノードの負荷や性能について有用な情報を与えてくれます。 詳しくは監視の章で扱います。

busy dist portの根本原因を探るための他の対処としては、次のようなものが考えられます。

  1. ネットワークの問題:ネットワークが低速だったり不安定だったりすると、メッセージの送信が遅れる原因になります。ネットワークの性能を確認し、必要であればネットワークインフラの増強を検討してください。
  2. メッセージ量の多さ:Erlangノードが大量のメッセージを送っていると、分散ポートに負荷がかかりすぎることがあります。送信するメッセージの数を減らせないか、コードの見直しを検討してください。メッセージをまとめて送る、送信頻度を下げるといった方法が考えられます。通信し合うプロセスを同じノード上に配置するようにするのも一つの方法です。
  3. 長時間実行されるタスク:Erlangのプロセスが後続処理へ制御を譲らずに長時間実行され続けると、分散ポートをブロックしてしまうことがあります。他のプロセスがメッセージを送れるよう、プロセスが定期的に制御を譲るようにしてください。システム内に行儀の悪いNIFがない限り、通常はこれが問題になることはないはずです。
  4. Erlang VMのチューニング:Erlang VMをチューニングして負荷への対応を改善することもできます。スケジューラ数を増やす(+Sオプション)、IOポーリングスレッドを増やす(+Aオプション)、その他のVM設定を調整するといった方法があります。