ZooKeeperの内部構造
はじめに
このドキュメントには、ZooKeeperの内部構造に関する情報が含まれています。次のトピックについて説明します。
アトミックブロードキャスト
ZooKeeperの中心にあるのは、すべてのサーバーを同期させるアトミックメッセージングシステムです。
保証、プロパティ、および定義
ZooKeeperで使用されるメッセージングシステムによって提供される特定の保証は次のとおりです。
-
信頼性のある配信:メッセージ
m
が1つのサーバーによって配信された場合、メッセージm
は最終的にすべてのサーバーによって配信されます。 -
完全な順序:メッセージ
a
が1つのサーバーによってメッセージb
の前に配信された場合、メッセージa
はすべてのサーバーでb
の前に配信されます。 -
因果関係の順序:メッセージ
b
が、メッセージa
がb
の送信者によって配信された後に送信された場合、メッセージa
はb
の前に順序付けする必要があります。送信者がb
を送信した後にc
を送信する場合、c
はb
の後に順序付けする必要があります。
ZooKeeperメッセージングシステムは、効率的で信頼性が高く、実装と保守が容易である必要もあります。私たちはメッセージングを多用するため、システムが1秒あたり数千のリクエストを処理できるようにする必要があります。新しいメッセージを送信するために少なくともk + 1個の正しいサーバーを要求できますが、停電などの相関障害から回復できる必要があります。システムを実装したとき、時間とエンジニアリングリソースがほとんどなかったため、エンジニアがアクセスしやすく、実装が容易なプロトコルが必要でした。私たちのプロトコルはこれらの目標をすべて満たしていることがわかりました。
私たちのプロトコルは、サーバー間でポイントツーポイントのFIFOチャネルを構築できることを前提としています。同様のサービスは通常、メッセージの損失や順序の変更が可能なメッセージ配信を前提としていますが、FIFOチャネルの仮定は、TCPを通信に使用することを考えると非常に現実的です。具体的には、TCPの次のプロパティに依存しています。
-
順序付き配信:データは送信された順序と同じ順序で配信され、メッセージ
m
は、m
より前に送信されたすべてのメッセージが配信された後にのみ配信されます。(これの系は、メッセージm
が失われた場合、m
以降のすべてのメッセージが失われるということです。) -
クローズ後のメッセージなし:FIFOチャネルが閉じられると、そこからメッセージを受信できなくなります。
FLPは、障害が発生する可能性がある場合、非同期分散システムではコンセンサスを達成できないことを証明しました。障害が発生した場合にコンセンサスを達成できるようにするために、タイムアウトを使用します。ただし、正確性のためではなく、活性のために時間に依存しています。したがって、タイムアウトが機能しなくなった場合(たとえば、クロックが歪んでいる場合)、メッセージングシステムがハングする可能性がありますが、その保証に違反することはありません。
ZooKeeperメッセージングプロトコルを説明するときは、パケット、プロポーザル、およびメッセージについて説明します。
-
パケット:FIFOチャネルを介して送信されるバイトシーケンス。
-
プロポーザル:合意の単位。プロポーザルは、ZooKeeperサーバーのクォーラムとパケットを交換することによって合意されます。ほとんどのプロポーザルにはメッセージが含まれていますが、NEW_LEADERプロポーザルはメッセージを含まないプロポーザルの例です。
-
メッセージ:すべてのZooKeeperサーバーにアトミックにブロードキャストされるバイトシーケンス。メッセージはプロポーザルに入れられ、配信される前に合意されます。
上記のように、ZooKeeperはメッセージの完全な順序を保証し、プロポーザルの完全な順序も保証します。 ZooKeeperは、ZooKeeperトランザクションID(zxid)を使用して完全な順序を公開します。すべてのプロポーザルは、提案時にzxidがスタンプされ、完全な順序を正確に反映します。プロポーザルはすべてのZooKeeperサーバーに送信され、それらのクォーラムがプロポーザルを確認するとコミットされます。プロポーザルにメッセージが含まれている場合、メッセージはプロポーザルがコミットされると配信されます。確認は、サーバーがプロポーザルを永続ストレージに記録したことを意味します。私たちのクォーラムには、クォーラムの任意のペアが少なくとも1つの共通サーバーを持っている必要があるという要件があります。これを確実にするために、すべてのクォーラムのサイズが(n / 2 + 1)である必要があります。ここで、nはZooKeeperサービスを構成するサーバーの数です。
zxidには、エポックとカウンターの2つの部分があります。私たちの実装では、zxidは64ビットの数値です。上位32ビットをエポックに使用し、下位32ビットをカウンターに使用します。 zxidは2つの部分で構成されているため、zxidは数値としても、整数のペア(エポック、カウント)としても表すことができます。エポック番号は、リーダーシップの変更を表します。新しいリーダーが権力を握るたびに、独自の時代番号が付けられます。プロポーザルに一意のzxidを割り当てる簡単なアルゴリズムがあります。リーダーは、プロポーザルごとに一意のzxidを取得するために、zxidをインクリメントするだけです。リーダーシップの起動により、1人のリーダーのみが特定の時代を使用することが保証されるため、単純なアルゴリズムにより、すべてのプロポーザルに一意のIDが付与されることが保証されます。
ZooKeeperメッセージングは、次の2つのフェーズで構成されます。
-
リーダーの起動:このフェーズでは、リーダーがシステムの正しい状態を確立し、プロポーザルの作成を開始する準備をします。
-
アクティブメッセージング:このフェーズでは、リーダーは提案するメッセージを受け入れ、メッセージ配信を調整します。
ZooKeeperは、全体的なプロトコルです。個々のプロポーザルに焦点を当てるのではなく、プロポーザルのストリーム全体を全体として見ています。厳密な順序付けにより、これを効率的に行うことができ、プロトコルが大幅に簡素化されます。リーダーシップの起動は、この全体的な概念を具現化したものです。リーダーは、フォロワーのクォーラム(リーダーもフォロワーとしてカウントされます。いつでも自分自身に投票できます)がリーダーと同期しており、同じ状態になっている場合にのみアクティブになります。この状態は、リーダーがコミットされたと信じているすべてのプロポーザルと、リーダー、NEW_LEADERプロポーザルに従うためのプロポーザルで構成されています。(うまくいけばあなたは自分自身にこう考えているでしょう、リーダーがコミットされたと信じているプロポーザルのセットには、実際にコミットされたすべてのプロポーザルが含まれていますか?答えははいです。以下に、その理由を明確にします。)
リーダーの起動
リーダーの起動には、リーダー選出(FastLeaderElection
)が含まれます。 ZooKeeperメッセージングは、以下が当てはまる限り、リーダーを選出する正確な方法を気にしません。
- リーダーは、すべてのフォロワーの中で最高のzxidを見てきました。
- クォーラムのサーバーがリーダーに従うことを約束しました。
これらの2つの要件のうち、最初の要件であるフォロワーの中で最高のzxidのみが正しく動作するために保持する必要があります。 2番目の要件であるフォロワーのクォーラムは、高い確率で保持する必要があるだけです。 2番目の要件を再確認するので、リーダー選出中またはリーダー選出後に障害が発生してクォーラムが失われた場合は、リーダーの起動を放棄して別の選出を実行することで回復します。
リーダー選出後、単一のサーバーがリーダーとして指定され、フォロワーが接続するのを待ち始めます。残りのサーバーはリーダーに接続しようとします。リーダーは、不足しているプロポーザルを送信するか、フォロワーに欠落しているプロポーザルが多すぎる場合は、フォロワーに状態の完全なスナップショットを送信することで、フォロワーと同期します。
リーダーが見ていないプロポーザル、U
を持っているフォロワーが到着するという、隅のケースがあります。プロポーザルは順番に表示されるため、U
のプロポーザルには、リーダーが確認したzxidよりも高いzxidが設定されます。フォロワーは、リーダー選出後に到着した必要があります。そうでない場合、フォロワーは、より高いzxidを確認したことを考えると、リーダーに選出されていたでしょう。コミットされたプロポーザルはクォーラムのサーバーによって確認される必要があり、リーダーを選出したクォーラムのサーバーはU
を確認しなかったため、U
のプロポーザルはコミットされていないため、破棄できます。フォロワーがリーダーに接続すると、リーダーはフォロワーにU
を破棄するように指示します。
新しいリーダーは、確認した最高のzxidのエポックeを取得し、使用する次のzxidを(e + 1、0)に設定することで、新しいプロポーザルに使用を開始するzxidを確立します。リーダーがフォロワーと同期した後、NEW_LEADERプロポーザルを提案します。 NEW_LEADERプロポーザルがコミットされると、リーダーはアクティブになり、プロポーザルの受信と発行を開始します。
すべてが複雑に聞こえますが、リーダーの起動中の基本的な操作ルールは次のとおりです。
- フォロワーは、リーダーと同期した後、NEW_LEADERプロポーザルをACKします。
- フォロワーは、単一のサーバーからの特定のzxidを持つNEW_LEADERプロポーザルのみをACKします。
- 新しいリーダーは、クォーラムのフォロワーがACKした場合に、NEW_LEADERプロポーザルをコミットします。
- フォロワーは、NEW_LEADERプロポーザルがコミットされたときに、リーダーから受け取った状態をコミットします。
- 新しいリーダーは、NEW_LEADERプロポーザルがコミットされるまで、新しいプロポーザルを受け入れません。
リーダー選出が誤って終了した場合、リーダーにクォーラムがないため、NEW_LEADERプロポーザルはコミットされないため、問題はありません。この場合、リーダーと残りのフォロワーはタイムアウトし、リーダー選出に戻ります。
アクティブメッセージング
リーダーアクティベーションは、ほとんどの重労働を行います。リーダーが戴冠すると、彼は提案を次々と送り始めることができます。彼がリーダーである限り、他のリーダーはフォロワーの定足数を獲得できないため、出現することはありません。もし新しいリーダーが出現した場合、それはリーダーが定足数を失ったことを意味し、新しいリーダーは彼女のリーダーシップアクティベーション中に残された混乱をすべて片付けます。
ZooKeeperのメッセージングは、古典的な二相コミットに似た動作をします。
すべての通信チャネルはFIFO(先入れ先出し)であるため、すべてが順番に行われます。具体的には、次の運用制約が守られます。
- リーダーは、提案をすべてフォロワーに同じ順序で送信します。さらに、この順序は、リクエストが受信された順序に従います。FIFOチャネルを使用しているため、フォロワーも提案を順番に受信することになります。
- フォロワーは、受信した順序でメッセージを処理します。これは、メッセージが順番にACKされ、リーダーがFIFOチャネルによりフォロワーからACKを順番に受信することを意味します。また、メッセージ
m
が不揮発性ストレージに書き込まれた場合、m
の前に提案されたすべてのメッセージが不揮発性ストレージに書き込まれたことを意味します。 - リーダーは、フォロワーの定足数がメッセージをACKするとすぐに、すべてのフォロワーにCOMMITを発行します。メッセージは順番にACKされるため、COMMITはリーダーによって送信され、フォロワーによって順番に受信されます。
- COMMITは順番に処理されます。フォロワーは、提案がコミットされたときにその提案メッセージを配信します。
まとめ
以上が説明です。なぜこれが機能するのでしょうか?特に、新しいリーダーが信じている提案のセットに、実際にコミットされた提案が常に含まれているのはなぜでしょうか?まず、すべての提案には一意のzxidがあるため、他のプロトコルとは異なり、同じzxidに対して2つの異なる値が提案されることを心配する必要はありません。フォロワー(リーダーもフォロワーです)は、提案を順番に見て記録します。提案は順番にコミットされます。フォロワーは一度に1人のリーダーのみに従うため、アクティブなリーダーは一度に1人しかいません。新しいリーダーは、サーバーの定足数からの最高のzxidを確認しているため、前のエポックからコミットされたすべての提案を確認しています。新しいリーダーによって見られた前のエポックからの未コミットの提案は、アクティブになる前にそのリーダーによってコミットされます。
比較
これは単なるMulti-Paxosではないでしょうか?いいえ、Multi-Paxosは、単一のコーディネーターしかいないことを保証する方法が必要です。私たちはそのような保証に依存しません。代わりに、リーダーアクティベーションを使用して、リーダーシップの変更や、まだアクティブであると信じている古いリーダーから回復します。
これは単なるPaxosではないでしょうか?アクティブなメッセージングフェーズは、Paxosのフェーズ2によく似ています。実際、私たちにとって、アクティブなメッセージングは、アボートを処理する必要がない2相コミットのように見えます。アクティブなメッセージングは、提案をまたぐ順序付けの要件がある点で、両方とは異なります。すべてのパケットの厳密なFIFO順序を維持しないと、すべてが破綻します。また、リーダーアクティベーションフェーズは、両方とは異なります。特に、エポックを使用することで、未コミットの提案のブロックをスキップしたり、特定のzxidに対する重複した提案を心配したりする必要がなくなります。
整合性の保証
ZooKeeperの整合性の保証は、シーケンシャル整合性とリニアライザビリティの間にあります。このセクションでは、ZooKeeperが提供する正確な整合性保証について説明します。
ZooKeeperの書き込み操作はリニアライザブルです。つまり、各write
は、クライアントがリクエストを発行してから対応するレスポンスを受信するまでの間のいずれかの時点で、アトミックに有効になるように見えます。これは、ZooKeeper内のすべてのクライアントによって実行される書き込みを、これらの書き込みのリアルタイム順序を尊重する方法で完全に順序付けできることを意味します。ただし、書き込み操作がリニアライザブルであると単に述べるだけでは、読み取り操作についても言及しない限り意味がありません。
ZooKeeperの読み取り操作は、潜在的に古いデータを返す可能性があるため、リニアライザブルではありません。これは、ZooKeeperでのread
がクォーラム操作ではなく、サーバーがread
を実行しているクライアントにすぐに応答するためです。ZooKeeperは、読み取りのユースケースでは整合性よりもパフォーマンスを優先するため、これを行います。ただし、ZooKeeperでの読み取りはシーケンシャル整合性があります。これは、read
操作が、さらに各クライアントの操作の順序を尊重する何らかのシーケンシャルな順序で有効になるように見えるためです。この問題を回避するための一般的なパターンは、read
を発行する前にsync
を発行することです。これもまた、sync
が現在クォーラム操作ではないため、最新のデータを厳密に保証するものではありません。例として、TCP接続のタイムアウトがsyncLimit * tickTime
よりも小さい場合に発生する可能性のある、2つのサーバーが同時に自分がリーダーであると考えているシナリオを考えてみましょう。これは実際には発生する可能性は低いですが、厳密な理論的保証について議論する場合は留意する必要があります。このシナリオでは、sync
が古いデータを持つ「リーダー」によって提供される可能性があり、それによって次のread
も古くなる可能性があります。リニアライザビリティのより強力な保証は、read
の前に実際のクォーラム操作(write
など)が実行された場合に提供されます。
全体として、ZooKeeperの整合性保証は、シーケンシャル整合性とリニアライザビリティの間にある順序付きシーケンシャル整合性または正確にはOSC(U)
の概念によって正式に捉えられます。
クォーラム
アトミックブロードキャストとリーダー選出は、システムの整合性のあるビューを保証するために、クォーラムの概念を使用します。デフォルトでは、ZooKeeperはマジョリティクォーラムを使用します。これは、これらのプロトコルの1つで発生するすべての投票で、過半数の投票が必要であることを意味します。1つの例は、リーダー提案の承認です。リーダーは、サーバーの定足数から承認を受け取った後にのみコミットできます。
マジョリティの使用から本当に必要なプロパティを抽出すると、投票によって操作を検証するために使用されるプロセスのグループ(リーダー提案の承認など)が、少なくとも1つのサーバーでペアごとに交差することを保証する必要があることになります。マジョリティを使用することで、そのようなプロパティが保証されます。ただし、マジョリティとは異なるクォーラムを構築する方法もあります。たとえば、サーバーの投票に重みを割り当て、一部のサーバーの投票がより重要であると言うことができます。クォーラムを取得するには、すべての投票の重みの合計がすべての重みの合計の半分よりも大きくなるように、十分な投票を取得します。
重みを使用し、広域展開(コロケーション)で役立つ別の構造は、階層構造です。この構造では、サーバーを互いに素なグループに分割し、プロセスに重みを割り当てます。クォーラムを形成するには、グループGの過半数から十分なサーバーを保持する必要があり、Gの各グループgについて、gからの投票の合計がgの重みの合計の半分よりも大きくなります。興味深いことに、この構造により、より小さなクォーラムが可能になります。たとえば、9つのサーバーがあり、それらを3つのグループに分割し、各サーバーに重み1を割り当てると、サイズが4のクォーラムを形成できます。グループの過半数からのサーバーの過半数で構成されるプロセスの2つのサブセットは、必ず非空の交差を持つことに注意してください。コロケーションの過半数が、高い確率でサーバーの過半数を利用できると予想するのが妥当です。
ZooKeeperでは、ユーザーに、マジョリティクォーラム、重み、またはグループの階層を使用するようにサーバーを構成する機能を提供します。
ロギング
Zookeeperは、ロギングの抽象化レイヤーとしてslf4jを使用しています。ZooKeeperバージョン3.8.0以降、ロギングバックエンドとしてLogbackが選択されています。埋め込みサポートを向上させるために、最終的なロギング実装の選択をエンドユーザーに任せることを将来計画しています。したがって、コードでログステートメントを記述するには常にslf4j APIを使用しますが、ランタイムでどのようにログを記録するかをlogbackで構成します。slf4jにはFATALレベルがないことに注意してください。以前FATALレベルであったメッセージはERRORレベルに移動されました。ZooKeeperのlogbackの設定の詳細については、ZooKeeper管理者ガイドのロギングセクションを参照してください。
開発者ガイドライン
コード内でログステートメントを作成する場合は、slf4jマニュアルに従ってください。また、ログステートメントを作成するときは、パフォーマンスに関するFAQをお読みください。パッチレビュー担当者は以下を確認します。
適切なレベルでのロギング
slf4jにはいくつかのロギングレベルがあります。
適切なものを選択することが重要です。重要度の高い順から低い順に示します。
- ERRORレベルは、アプリケーションが引き続き実行できる可能性のあるエラーイベントを指定します。
- WARNレベルは、潜在的に有害な状況を指定します。
- INFOレベルは、アプリケーションの進捗状況を大まかに示す情報メッセージを指定します。
- DEBUGレベルは、アプリケーションのデバッグに最も役立つ詳細な情報イベントを指定します。
- TRACEレベルは、DEBUGよりも詳細な情報イベントを指定します。
ZooKeeperは通常、INFOレベルの重大度以上の(より重大な)ログメッセージがログに出力されるように本番環境で実行されます。
標準slf4jイディオムの使用
静的メッセージロギング
LOG.debug("process completed successfully!");
ただし、パラメーター化されたメッセージを作成する必要がある場合は、フォーマットアンカーを使用します。
LOG.debug("got {} messages in {} minutes",new Object[]{count,time});
名前付け
ロガーは、使用されているクラスの名前に従って命名する必要があります。
public class Foo {
private static final Logger LOG = LoggerFactory.getLogger(Foo.class);
....
public Foo() {
LOG.info("constructing Foo");
例外処理
try {
// code
} catch (XYZException e) {
// do this
LOG.error("Something bad happened", e);
// don't do this (generally)
// LOG.error(e);
// why? because "don't do" case hides the stack trace
// continue process here as you need... recover or (re)throw
}