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