Orleans は、組み込みのメンバーシップ プロトコル (クラスター メンバーシップとも呼ばれる) を介したクラスター管理を提供 します。 このプロトコルの目的は、すべてのサイロ (Orleans サーバー) が現在稼働しているサイロのセットに合意し、障害が発生したサイロを検出し、新しいサイロのクラスターへの参加を許可できるようにすることです。
このプロトコルは、外部サービスに依存して IMembershipTable の抽象化を提供します。
IMembershipTable は、2 つの目的で使用されるフラットで耐久性のあるテーブルです。 最初に、サイロが互いを見つけ合い、 Orleans クライアントがサイロを見つけるためのランデブー ポイントとして機能します。 次に、現在のメンバーシップ ビュー (生きているサイロの一覧) を格納し、このビューで合意を調整するのに役立ちます。
現在、 IMembershipTableには、 Azure Table Storage、 Azure Cosmos DB、ADO.NET (PostgreSQL、MySQL/MariaDB、SQL Server、Oracle)、 Apache ZooKeeper、 Consul IO、 AWS DynamoDB、 MongoDB、 Redis、 Apache Cassandra、および開発用のメモリ内実装に基づいて実装されています。
各サイロは、 IMembershipTableに加えて、障害が発生したサイロを検出し、一連の生きているサイロに関する合意に達する、完全に分散されたピアツーピア メンバーシップ プロトコルに参加します。
Orleansのメンバーシップ プロトコルの内部実装については、以下で説明します。
メンバーシップ プロトコル
すべてのサイロは、起動時に、 IMembershipTableの実装を使用して、既知の共有テーブルに自身のエントリを追加します。 Orleans では、テーブル内の一意のキーとしてサイロ ID (
ip:port:epoch) とサービスデプロイ ID (クラスター ID) の組み合わせを使用します。 エポックは、このサイロが開始されたときの時間 (ティック単位) であり、特定のip:port:epochデプロイ内でOrleansが一意であることを確認します。サイロは、アプリケーション プローブを介して直接互いを監視します ("生きています"
heartbeats)。 プローブは、通常の通信に使用されるのと同じ TCP ソケット経由でサイロからサイロに直接メッセージとして送信されます。 これにより、プローブは実際のネットワークの問題とサーバーの正常性と完全に関連付けられます。 すべてのサイロは、構成可能な他のサイロ群を探査します。 サイロは、他のサイロの ID に対して一貫ハッシュを計算し、すべての ID の仮想リングを形成して、そのリング上で X 個の後続サイロを選択することによって、プローブする対象を選択します。 (これは 一貫性のあるハッシュ と呼ばれるよく知られた分散手法であり、 コード DHT などの多くの分散ハッシュ テーブルで広く使用されています)。サイロ S が監視中のサーバー P から Y 回のプローブ応答を受信しない場合、S は P に対する疑いを、タイムスタンプ付きで
IMembershipTableの P の行に記録することで疑念を持ちます。P が K 秒以内に Z 個以上の疑いを受けた場合、S は P が死んでいることを P の行に記録し、現在のメンバーシップテーブルのスナップショットを他のすべてのサイロにブロードキャストします。 サイロはテーブルを定期的に更新するため、スナップショットは、すべてのサイロが新しいメンバーシップ ビューについて学習するのにかかる時間を短縮するための最適化です。
詳細については、以下を参照してください。
疑いは、P に対応する行の特殊な列で、
IMembershipTableに書き込まれます。S が P を疑うと、"at time TTT S suspected P" と書き込まれます。1つの疑いは、Pが死んだと宣言するのに十分ではありません。 P デッドを宣言するには、構成可能な時間枠 T (通常は 3 分) 内に異なるサイロからの Z 疑いが必要です。 疑いは、
IMembershipTableによって提供されるオプティミスティック同時実行制御を使って書き込まれます。疑っているサイロ S は、P の行を読み取ります。
Sが最後の容疑者である場合 (疑い列に記録されているように、期間 T 内に既に Z-1 の容疑者が存在している)、S は P が死んだと宣言することにします。 この場合、S は、疑っているサイロのリストに自分自身を追加し、P の状態列に P が非稼働であることも書き込みます。それ以外の場合、S が最後の容疑者でない場合、S は単に容疑者の列に追加します。
どちらの場合も、書き戻しでは、前に読み取ったバージョン番号または ETag が使用され、この行に対する更新がシリアル化されます。 バージョン/ETag の不一致が原因で書き込みが失敗した場合、S は再試行します (P が既に配信不能とマークされていない限り、再度読み取って書き込みを試みます)。
大まかに言えば、この一連の "読み取り、ローカル変更、書き戻し" はトランザクションです。 ただし、ストレージ トランザクションは必ずしも使用されるとは限りません。 "トランザクション" コードはサーバー上でローカルに実行され、
IMembershipTableによって提供されるオプティミスティック コンカレンシーによって分離とアトミック性が保証されます。
すべてのサイロは、そのデプロイに対するメンバーシップ テーブル全体を定期的に読み取ります。 このようにして、サイロは新しいサイロが参加することや、他のサイロが死んだと宣言されていることについて学習します。
スナップショット ブロードキャスト: 定期的なテーブル読み取りの頻度を減らすために、サイロがテーブルに書き込むたびに (疑い、新しい結合など)、現在のテーブル状態のスナップショットを他のすべてのサイロに送信します。 メンバーシップ テーブルは一貫性があり、単調にバージョン管理されているため、各更新プログラムでは、安全に共有できる一意のバージョン管理されたスナップショットが生成されます。 これにより、定期的な読み取りサイクルを待たずにメンバーシップの変更を即時に反映できます。 スナップショットの配布が失敗した場合でも、定期的な読み取りはフォールバック メカニズムとして維持されます。
順序付きメンバーシップ ビュー: メンバーシップ プロトコルを使用すると、すべてのメンバーシップ構成がグローバルに完全に順序付けられます。 この順序付けには、次の 2 つの主な利点があります。
接続の保証: 新しいサイロがクラスターに参加するときは、他のすべてのアクティブなサイロへの双方向接続を検証する必要があります。 既存のサイロが応答しない場合 (ネットワーク接続の問題を示している可能性があります)、新しいサイロへの参加は許可されません。 これにより、起動時にクラスター内のすべてのサイロ間の完全な接続が保証されます。 ディザスター リカバリー シナリオでの例外については、以下の
IAmAliveに関するメモを参照してください。一貫性のあるディレクトリ更新: 分散グレイン ディレクトリなどの上位レベルのプロトコルは、メンバーシップの一貫性のある単調なビューを持つすべてのサイロに依存します。 これにより、重複するグレイン アクティブ化をよりスマートに解決できます。 詳細については、 Grain ディレクトリ のドキュメントを参照してください。
実装の詳細:
IMembershipTableでは、変化のグローバルな全順序を保証するためにアトミックアップデートが必要です。- 実装では、テーブル エントリ (サイロの一覧) とバージョン番号の両方をアトミックに更新する必要があります。
- これを実現するには、データベース トランザクション (SQL Server など) または ETag を使用したアトミックな比較とスワップ操作 (Azure Table Storage と同様)。
- 具体的なメカニズムは、基になるストレージ システムの機能によって異なります。
テーブル内の特別なメンバーシップ バージョン行は、変更を追跡します。
- テーブルへのすべての書き込み (疑い、死の宣言、結合) は、このバージョン番号をインクリメントします。
- すべての書き込みはこの行を介してアトミック更新によってシリアル化されます。
- 単調に増加するバージョンにより、すべてのメンバーシップ変更の合計順序が保証されます。
サイロ S がサイロ P の状態を更新する場合:
- S は最初に最新のテーブルの状態を読み取ります。
- 1 つのアトミック操作では、P の行の両方が更新され、バージョン番号がインクリメントされます。
- アトミック更新が失敗した場合 (たとえば、同時変更により)、操作は指数バックオフで再試行されます。
スケーラビリティに関する考慮事項:
バージョン行を介してすべての書き込みをシリアル化すると、競合の増加によるスケーラビリティに影響する可能性があります。 このプロトコルは、最大 200 個のサイロを含む運用環境で効果的であることが証明されていますが、数千のサイロを超える課題に直面する可能性があります。 非常に大規模なデプロイでは、メンバーシップの更新がボトルネックになった場合でも、Orleans の他の部分 (メッセージング、グレイン ディレクトリ、ホスティング) はスケーラブルなままです。
既定の構成: 既定の構成は、Azure での運用環境の使用中に手動で調整されています。 既定では、すべてのサイロは他の 3 つのサイロによって監視され、2 つの疑いはサイロの死を宣言するのに十分であり、疑いは過去 3 分からのみ考慮されます (それ以外の場合は古くなっています)。 プローブは 10 秒ごとに送信され、サイロを疑うために 3 つのプローブを見逃す必要があります。
自己監視: 障害検出器は、Hashicorpの Lifeguardの 研究(ペーパー、講演、ブログ)からのアイデアを組み込み、クラスターの大部分が部分的な障害を経験する致命的なイベントの間にクラスターの安定性を向上させます。
LocalSiloHealthMonitorコンポーネントは、複数のヒューリスティックを使用して各サイロの正常性をスコア付けします。- メンバーシップ テーブルのアクティブな状態
- 他のサイロからの疑いなし
- 最近成功したプローブ応答
- 受信した最近のプローブ リクエスト
- スレッド プールの応答性 (1 秒以内に実行される作業項目)
- タイマー精密度 (スケジュールから3秒以内に作動する)
サイロの正常性スコアは、プローブのタイムアウトに影響します。異常なサイロ (スコア 1 から 8) では、正常なサイロ (スコア 0) と比較してタイムアウトが増加しています。 これには、次の 2 つの利点があります。
- ネットワークまたはシステムに負荷がかかっているときにプローブが成功する時間が長くなります。
- 健康なサイロを誤って排除する前に、不健全なサイロを排除するための投票がより確実に行われます。
これは、スレッド プールの枯渇などのシナリオで特に重要です。低速ノードでは、応答を十分に迅速に処理できないために正常なノードが誤って疑われる可能性があります。
間接的なプローブ: Lifeguard に着想を得た別の機能で、異常またはパーティション分割されたサイロが正常なサイロの死を誤って宣言する可能性を減らすことで、障害検出の精度を向上させます。 監視サイロがターゲット サイロの停止を宣言する投票を行う前に、ターゲット サイロに対するプローブ試行が 2 回残っている場合、間接プローブが使用されます。
- 監視サイロは、別のサイロを仲介者としてランダムに選択し、ターゲットをプローブするように要求します。
- 仲介者は、ターゲット サイロに接続しようとします。
- ターゲットがタイムアウト期間内に応答しなかった場合、中間者は否定確認を送信します。
- 監視サイロが仲介者から否定的な確認を受け取り、仲介者が (前述の自己監視を通じて) 正常であると宣言した場合、監視サイロはターゲットの死者を宣言する投票を行います。
- 2 つの必要な投票の既定の構成では、間接プローブからの否定受信確認は両方の投票としてカウントされるため、複数のパースペクティブで失敗が確認された場合に、デッド サイロの宣言が高速化されます。
完全な障害検出の強制: テーブルでサイロが死亡したと宣言されると、実際には死亡していなくても(たとえば、一時的にパーティションが分割されていたり、ハートビートメッセージが失われたりした場合でも)、誰もがそれを死んだと見なします。 誰もがそれとの通信を止めます。 サイロは、(テーブルから新しい状態を読み取ることによって) それが死んだことを知ると、そのプロセスを終了します。 そのため、新しいプロセスとしてサイロを再起動するには、インフラストラクチャを配置する必要があります (開始時に新しいエポック番号が生成されます)。 Azure でホストされている場合、これは自動的に行われます。 それ以外の場合は、障害発生時に自動再起動するように構成された Windows サービスや Kubernetes のデプロイなど、別のインフラストラクチャが必要です。
テーブルにしばらくアクセスできない場合はどうなりますか。
ストレージ サービスが停止している、利用できない、または通信の問題が発生した場合、 Orleans プロトコルはサイロの停止を誤って宣言しません。 運用サイロは問題なく動作し続けます。 ただし、 Orleans はサイロの死を宣言できず (ミスしたプローブを介してデッド サイロを検出した場合、この事実をテーブルに書き込むことができず)、新しいサイロの参加を許可することはできません。 そのため、完全性は損なわれますが、精度は損なわれません。テーブルからパーティション分割しても、 Orleans が誤ってサイロの死を宣言することはありません。 また、部分的なネットワーク パーティション (一部のサイロがテーブルにアクセスでき、他のサイロではアクセスできない) では、 Orleans がサイロの死を宣言する可能性がありますが、他のすべてのサイロがそれについて学習するには時間がかかります。 検出が遅れる可能性がありますが、テーブルが利用できないため、 Orleans 間違ってサイロを強制終了することはありません。
診断とディザスター リカバリーのために
IAmAlive執筆する:サイロ間で送信されるハートビートに加えて、各サイロでは、テーブル行の "I Am Alive" タイムスタンプが定期的に更新されます。 これは、次の 2 つの目的に役立ちます。
診断: システム管理者は、クラスターの稼働状態を確認し、サイロが最後にアクティブだった時期を判断する簡単な方法を提供します。 タイムスタンプは、既定では 30 秒ごとに更新されます。
ディザスター リカバリー: サイロが複数の期間 (
NumMissedTableIAmAliveLimit、既定値: 3 を使用して構成) タイムスタンプを更新していない場合、新しいサイロは起動時の接続チェック中にそれを無視します。 これにより、適切なクリーンアップなしでサイロがクラッシュしたシナリオからクラスターを復旧できます。
メンバーシップ テーブル
前述のように、 IMembershipTable はサイロが互いに見つけ合い、 Orleans クライアントがサイロを見つけるためのランデブー ポイントとして機能します。 また、メンバーシップ ビューで契約を調整するのにも役立ちます。 メイン Orleans リポジトリには、Azure Table Storage、Azure Cosmos DB、PostgreSQL、MySQL/MariaDB、SQL Server、Apache ZooKeeper、Consul IO、Apache Cassandra、MongoDB、Redis、AWS DynamoDB、開発用のメモリ内実装など、多くのシステムの実装が含まれています。
Azure Table Storage: この実装では、Azure デプロイ ID がパーティション キーとして機能し、サイロ ID (
ip:port:epoch) が行キーとして機能します。 協力して、サイロごとに一意のキーを保証します。 コンカレンシー制御では、 Azure Table ETag に基づくオプティミスティック コンカレンシー制御が使用されます。 テーブルからデータが読み取されるたびに、各読み取り行の ETag が格納され、書き戻そうとするときに使用されます。 Azure Table Service では、書き込みごとに ETag が自動的に割り当てられ、チェックされます。 複数行トランザクションの場合、 Azure Table によって提供されるバッチ トランザクション のサポートが利用され、同じパーティション キーを持つ行に対してシリアル化可能なトランザクションが保証されます。SQL Server: この実装では、構成されたデプロイ ID によって、デプロイと、どのサイロがどのデプロイに属しているかを区別します。 サイロ ID は、適切なテーブルと列の
deploymentID, ip, port, epochの組み合わせとして定義されます。 リレーショナル バックエンドでは、Azure Table 実装での ETag の使用と同様に、オプティミスティック コンカレンシー制御とトランザクションが使用されます。 リレーショナル実装では、データベース エンジンが ETag を生成することを想定しています。 SQL Server 2000 の場合、生成された ETag はNEWID()の呼び出しから取得されます。 SQL Server 2005 以降では、 ROWVERSION が使用されます。 Orleans は、リレーショナル ETag を非透過的なVARBINARY(16)タグとして読み書きし、base64 でエンコードされた文字列としてメモリに格納します。 Orleans では、統計データの挿入に現在使用されているUNION ALL(DUALを含む Oracle の場合) を使用した複数行挿入がサポートされています。 SQL Server の正確な実装と根拠は、 CreateOrleansTables_SqlServer.sqlで入手できます。Apache ZooKeeper: この実装では、構成されたデプロイ ID がルート ノードとして使用され、サイロ ID (
ip:port@epoch) がその子ノードとして使用されます。 共に、サイロごとにユニークなルートを提供する。 コンカレンシー制御では、 ノードのバージョン に基づくオプティミスティック コンカレンシー制御が使用されます。 デプロイ ルート ノードからデータが読み取されるたびに、すべての読み取り子サイロ ノードのバージョンが格納され、書き戻そうとするときに使用されます。 ノードのデータが変更されるたびに、ZooKeeper サービスによってバージョン番号がアトミックに増加します。 複数行トランザクションの場合、 複数の方法 が使用され、同じ親デプロイ ID ノードを持つサイロ ノード経由でシリアル化可能なトランザクションが保証されます。Consul IO: Consul のキー/値ストアは、メンバーシップ テーブルの実装に使用されました。 詳細については、「 Consul Deployment」 を参照してください。
AWS DynamoDB: この実装では、クラスターデプロイ ID がパーティション キーとして使用され、サイロ ID (
ip-port-generation) が RangeKey として使用され、レコードが一意になります。 オプティミスティック コンカレンシーは、DynamoDB で条件付き書き込みを行うことで、ETag属性を使用して実現されます。 実装ロジックは、Azure Table Storage とよく似ています。Apache Cassandra: この実装では、サービス ID とクラスター ID の複合がパーティション キーとして機能し、行キーとしてサイロ ID (
ip:port:epoch) が機能します。 それによって、サイロごとに一意の行が保証されます。 コンカレンシー制御では、Lightweight Transaction を使用した静的列バージョンに基づくオプティミスティック コンカレンシー制御が使用されます。 このバージョン列はパーティション/クラスター内のすべての行で共有され、各クラスターのメンバーシップ テーブルに一貫した増分バージョン番号が提供されます。 この実装では、複数行トランザクションはありません。開発セットアップ用のメモリ内エミュレーション: この実装には特別なシステム グレインが使用されます。 このグレインは、開発セットアップにのみ使われる、指定されたプライマリ サイロに存在します。 実際の運用環境では、プライマリ サイロ は必要ありません。
設計の原理
自然な疑問として、クラスター メンバーシップの実装に完全に Apache ZooKeeper や etcd に依存し、ZooKeeper の一時ノードを使用したグループ メンバーシップのすぐに使用できるサポートを活用しないのはなぜでしょうか? メンバーシップ プロトコルを実装する理由 主に次の 3 つの理由がありました。
クラウドでのデプロイとホスティング:
Zookeeper はホステッド サービスではありません。 つまり、クラウド環境では、 Orleans お客様は ZK クラスターのインスタンスをデプロイ、実行、および管理する必要があります。 これは、顧客に強制されなかった不必要な負担です。 Azure Table を使用することで、 Orleans はホストされたマネージド サービスに依存し、顧客の生活がはるかにシンプルになります。 基本的に、クラウドでは、インフラストラクチャではなく、プラットフォームとしてクラウドを使用します。 一方、オンプレミスで実行してサーバーを管理する場合、
IMembershipTableの実装として ZK に依存することは実行可能なオプションです。直接的な障害検出:
エフェメラル ノードで ZK のグループ メンバーシップを使用すると、 Orleans サーバー (ZK クライアント) と ZK サーバーの間でエラー検出が発生します。 これは、 Orleans サーバー間の実際のネットワークの問題と必ずしも関連しているわけではありません。 障害検出は、通信のクラスター内の状態を正確に反映することを望んでいた。 具体的には、この設計では、 Orleans サイロが
IMembershipTableと通信できない場合、それは死んでいるとは見なされず、作業を続けることができます。 これに対し、エフェメラル ノードを含む ZK グループ メンバーシップを使用した場合、ZK サーバーからの切断により、 Orleans サイロ (ZK クライアント) が停止状態と宣言される一方で、有効で完全に機能している可能性があります。移植性と柔軟性:
Orleansの理念の一部として、Orleansは特定のテクノロジへの強い依存を強制するのではなく、異なるコンポーネントを異なる実装で簡単に切り替えることができる柔軟な設計を提供します。 これはまさに、
IMembershipTable抽象化が果たす目的です。
メンバーシップ プロトコルのプロパティ
任意の数の障害を処理できる:
このアルゴリズムは、クラスターの完全な再起動を含め、任意の数のエラー (f<=n) を処理できます。 これは、クォーラム (通常は多数派) を必要とする "従来の" Paxos ベースのソリューションとは対照的です。 運用環境では、サイロの半分以上がダウンしているシナリオが示されています。 このシステムは機能したままですが、Paxos ベースのメンバーシップは進行できません。
テーブルへのトラフィックが非常に軽い:
実際の探針は、テーブルではなく、直接サーバー間に送信されます。 テーブルを介してプローブをルーティングすると、大量のトラフィックが生成され、障害検出の観点から精度が低下します。サイロがテーブルに到達できない場合は、"I am alive" ハートビートの記述が間違いになり、他のユーザーはそれをデッドと宣言します。
正確性と完全性のバランスを調整できる:
完璧さと精度の両方を兼ね備えた障害検出はできませんが、通常は精度(ライブサイロを誤って死亡と宣言しないこと)と完全性(死亡したサイロをできるだけ早く死亡と宣言すること)の間でトレードオフを行う能力が望まれます。 デッドプローブとミスプローブを宣言するための設定可能な投票は、これら2つの側面を調整することを可能にします。 詳しくは、エール大学のコンピューター サイエンスの障害検出機能に関するページをご覧ください。
スケーリング:
このプロトコルは、おそらく数万台のサーバーを処理できます。 これは、数十ノードを超えてスケーリングしないことが知られているグループ通信プロトコルなどの従来のPaxosベースのソリューションとは対照的です。
診断:
テーブルは、診断とトラブルシューティングにも非常に便利です。 システム管理者は、テーブル内の現在の生きているサイロの一覧を瞬時に見つけ出し、殺されたすべてのサイロと疑いの履歴を確認できます。 これは、問題を診断するときに特に役立ちます。
IMembershipTableの実装に信頼性の高い永続的ストレージが必要な理由:永続的ストレージは、2 つの目的で
IMembershipTableに使用されます。 最初に、サイロが互いを見つけ合い、 Orleans クライアントがサイロを見つけるためのランデブー ポイントとして機能します。 次に、信頼性の高いストレージは、メンバーシップ ビューで契約を調整するのに役立ちます。 障害検出はサイロ間で直接ピアツーピアで行われますが、メンバーシップ ビューは信頼できるストレージに格納され、このストレージによって提供されるコンカレンシー制御メカニズムは、誰が生きているか、誰が死んでいるかについての合意に達するために使用されます。 ある意味では、このプロトコルは、分散コンセンサスの困難な問題をクラウドにアウトソーシングします。 そうすることで、基になるクラウド プラットフォームの完全な機能が利用され、真にサービスとしてのプラットフォーム (PaaS) として使用されます。直接
IAmAliveは、診断専用にテーブルに書き込みます。サイロ間で送信されるハートビートに加えて、各サイロでは、テーブル行の "I Am Alive" 列も定期的に更新されます。 この "I Am Alive" 列は 、手動のトラブルシューティングと診断 にのみ使用され、メンバーシップ プロトコル自体では使用されません。 通常は、はるかに低い頻度 (5 分に 1 回) で記述され、システム管理者がクラスターの稼働状態を確認したり、サイロが最後に生きていた時期を簡単に確認したりするための非常に便利なツールとして機能します。
謝辞
このプロトコルの最初のバージョンの設計と実装に対する Alex Kogan の貢献に関する確認。 この作業は、2011 年夏の Microsoft Research での夏期インターンシップの一環として行われました。
ZooKeeper ベースのIMembershipTableの実装は Shay Hazor によって行われ、SQL IMembershipTableの実装は Veikko Eeva によって行われ、AWS DynamoDB IMembershipTableの実装は Gutemberg Ribeiro によって行われ、Consul ベースのIMembershipTableの実装は Paul North によって行われ、最後に Apache Cassandra IMembershipTableの実装は OrleansCassandraUtils によってから適応されました。
.NET