DUICUO

残念ながら、ZooKeeper を明確に説明できる記事はありません。

[51CTO.com からのオリジナル記事] インターネット時代は情報爆発の時代であり、情報の高い同時性により分散システムの広範な応用が促進されました。

[[283428]]

Pexelsからの画像

分散システム ソリューションとして、ZooKeeper は、データの公開/サブスクリプション、負荷分散、ネーミング サービス、クラスター管理など、さまざまな分散シナリオで広く使用されています。

そのため、ZooKeeperは分散システムにおいて重要な役割を果たします。今日は、簡単な例を通してその実装原理を見ていきます。

簡単な例から始めましょう。

分散システムでは、複数のアプリケーションが同じ設定を読み取る状況がよく発生します。例えば、アプリケーションAとBの両方が設定Cの内容を読み取ります。設定Cの内容が変更されると、AとBの両方に通知されます。

一般的なアプローチとしては、クロック周波数に基づいてCにAとBの変更を問い合わせるか、オブザーバーパターンを使用してCの変更をリッスンし、変更を検出した場合にのみAとBを更新するというものがあります。では、ZooKeeperはこのシナリオをどのように調整するのでしょうか?

ZooKeeper は、C の値を保存するために、ZServer という ZooKeeper サーバーを作成します。アプリケーション A と B に対してそれぞれ ClientA と ClientB の 2 つのクライアントが生成されます。

これら2つのクライアントはZooKeeperサーバーに接続し、そこに保存されているC値を取得します。ZooKeeperサーバー内でC値が保存されている場所はZNodeと呼ばれます。

ClientA と ClientB は ZooKeeper サーバーを通じて C の値を取得します。

Zノード

上記の例では、クライアント ClientA と ClientB は C の内容を読み取る必要があります。この C は ZooKeeper の ZNode にリーフ ノードとして保存されます。

通常、効率性を向上させるため、ZNode はメモリに保存されます。ZNode のデータモデルはツリー(ZNode ツリー)です。

上の図からわかるように、ツリー内の各ノードにはデータを保存でき、各ノードにはリーフノードを保存できます。

ZooKeeperクライアントは、アクセスパスとして「/」を使用してデータにアクセスします。例えば、変数Cはパス「/RootNode/C」を介してアクセスできます。

クライアント呼び出しを容易にするために、ZooKeeper はいくつかのコマンドを公開しています。

Znodeコマンドへのアクセス

ストレージ メディアとしての ZNode は、永続ノードと一時ノードに分けられます。

  • 永続ノード (PERSISTENT) は、作成されると、削除されない限り、ZooKeeper サーバー上に存在します。
  • エフェメラルノードとは、ライフサイクルがクライアントセッションに結び付けられたデータノードです。クライアントセッションが失われた場合、ノードは自動的に削除されます。

エフェメラル ノードをリソースと見なすと、クライアントとサーバーがセッションを確立してエフェメラル ノードを生成すると、クライアントがサーバーとの接続を失うと、ノード リソースは ZNode から削除されます。

シーケンシャルZNodeには、一意の単調増加する整数が割り当てられます。例えば、複数のクライアントがサーバー上の/tasksノードをリクエストした場合、クライアントがノードをリクエストした順序に従って、/tasks/taskノードに番号が追加されます。

3 つのクライアントがノード リソースを要求すると、/tasks の下に 3 つの連続したノード (/tasks/task1、/tasks/task2、/tasks/task3) が作成されます。

シーケンシャル ノードは、複数のクライアントが連携して動作するときに特定の順序で実行されるため、分散トランザクションを処理するときに非常に役立ちます。

上記の 2 種類のノードをシーケンシャル ノードと組み合わせると、永続ノード、永続シーケンシャル ノード、エフェメラル ノード、エフェメラル シーケンシャル ノードの 4 種類のノードが生成されます。

ウォッチャー

前述の通り、ZooKeeperはデータを保存するためにZNodeを使用し、Cの値はそこに保存されます。Cが更新された場合、2つのクライアント(ClientAとClientB)はどのように通知を受け取るのでしょうか?

ZooKeeperクライアントは、指定されたノード(/RootNote/C)にウォッチャーを登録します。ZNode上のCが更新されると、サーバーはClientAとClientBに通知します。

これは次の 3 つのステップで実現できます。

  • クライアント登録ウォッチャー
  • Watcherのサーバー側処理
  • クライアント側コールバックウォッチャー

ウォッチャーの登録、処理、およびコールバック。

① クライアント登録ウォッチャー

ZooKeeper クライアントは Watcher のインスタンスを作成します。

一方、このウォッチャーはクライアント上でローカルに保存され、サーバー側の会話のウォッチャーとして引き続き機能します。

クライアントは、getData、getChildren、および exist メソッドを使用して、Watchers をサーバーに登録できます。

クライアント側ウォッチャー登録図

また、クライアントが登録のために Watcher をサーバーに送信すると、送信される Watcher はローカルの ZKWatchManager に保存されることにも注意してください。

これを実行する利点は、サーバーが登録の成功を確認すると、ウォッチャーの詳細をクライアントに送り返す必要がなくなることです。

クライアントは、ローカルの ZKWatchManager から Watch 情報を取得し、サーバーの応答を受信した後、それを処理するだけで済みます。

② Watcherのサーバー側処理

サーバーはクライアントからのリクエストを受信すると、それを処理のために FinalRequestProcessor に渡します。このプロセスは、ZNode から対応するデータを取得し、Watch を WatchManager に追加します。

こうすることで、このノード上のデータが次に変更されたときに、Watch を登録したクライアントに通知されます。


サーバー側の監視プロセス

③ クライアント側コールバックウォッチャー

クライアントはウォッチャー登録に応答した後、WatcherEventを送信します。クライアントには、このメッセージを受信するための対応するコールバック関数があります。

これは readResponse メソッドを使用して均一に処理されます。


SendThread はサーバーから通知を受信すると、EventThread.queueEvent を介して EventThread にイベントを送信します。

前述のように、Watcher の具体的な内容は、クライアント登録時にすでに ZKWatchManager に保存されています。

したがって、EventRead は EventType を使用して、どの Watcher に応答したか (データが変更されたか) を判断できます。

次に、特定の Watch が ZKWatchManager から取得され、waitingEvent キューに配置されて処理を待機します。

最後に、EventThread の processEvent メソッドがデータ更新応答を順番に処理します。

バージョン

Watcherメカニズムの紹介が終わったので、ZNodeのバージョンについての説明に戻りましょう。クライアント(ClientD)がCの値を変更しようとすると、他の2つのクライアントは通知を受け取り、後続のビジネスロジックを続行します。

分散システムでは、クライアント D が C に対して書き込み操作を実行しているときに、別のクライアント E も C に対して書き込み操作を実行しているという状況が発生する可能性があります。これらの 2 つのクライアントは C リソースを競合するため、通常、このような状況では C をロックする必要があります。

2 つのクライアントがリソースをめぐって競合しています。

そのため、ZNodeバージョンの概念が導入されました。バージョンは、分散データに対するアトミック操作を保証するために使用されます。

ZNodeのバージョン情報は、ZNodeのStatオブジェクトに保存されます。Statオブジェクトには以下の3つの種類があります。

この例では、「データ ノード コンテンツのバージョン番号」、つまり Version のみに焦点を当てています。

ClientD と ClientE が C に対して実行する書き込み操作を単一のトランザクションと見なすと、書き込み操作を実行する前に、各トランザクションはノード上の値、つまりノードに保存されているデータとノードのバージョン番号を取得します。

楽観的ロックを例に挙げると、データの書き込みは、データの読み取り、書き込みの検証、そしてデータの書き込みの3つの段階に分かれています。例えば、Cのデータは1で、バージョンは0です。

この時点で、ClientDとClientEの両方がこの情報を取得しています。ClientDが​​最初に書き込み操作を実行すると仮定すると、書き込み検証中に、以前に取得したバージョンとノード上のバージョンが同じ(どちらも1)であることが判明するため、ClientDは直接データの書き込みを実行します。

書き込み操作後、バージョン値が1から2に変更されました。クライアントEが書き込み検証を実行すると、自身のバージョン=1がノードの現在のバージョン=2と異なることが分かります。そのため、書き込み操作は失敗し、バージョンとノードデータを再度取得して、再度書き込みを試みます。

上記の方法に加えて、ZNodeの順序付けされた性質も活用できます。C言語では、複数の順序付けされた子ノードを作成できます。クライアントがデータを書き込む準備ができると、一時的に順序付けされたノードが作成されます。

ノードはFIFOアルゴリズムに従って順序付けられ、最初に書き込みを要求したクライアントがノードの前にリストされます。各ノードにはシーケンス番号が付与され、ノードへの後続の要求はシーケンス番号に基づいて順番に順序付けられます。

ClientD と ClientE はそれぞれ子 ZNode を確立します。

各クライアントがCに対して変更操作を実行する際、自身のシーケンス番号よりも小さいシーケンス番号を持つノードが存在するかどうかを確認する必要があります。存在する場合、クライアントは待機状態になります。

シーケンス番号が小さいノードがすべて操作を完了した後にのみ、ノードは変更操作を実行します。これにより、トランザクションの順次処理が保証されます。

セッション

バージョンの概念を説明したので、例はClientABからClientDEへと拡張されました。これらのクライアントはZooKeeperサーバーと通信してデータの読み取りや変更を行います。

クライアントとサーバー間のこの接続をセッションと呼びます。ZooKeeper セッションには、「接続中」、「接続済み」、「再接続中」、「再接続済み」、「終了」という状態があります。

さらに、サーバー側では専用のプロセスがこれらを管理します。クライアントが初期化されると、設定に従って自動的にサーバーに接続し、セッションを確立します。クライアントがサーバーに接続すると、セッションは「接続中」状態になります。

接続が確立されると、クライアントは「接続済み」状態になります。遅延や短時間の接続切断が発生した場合、クライアントは自動的に再接続し、「再接続中」および「再接続済み」状態になります。

長時間のタイムアウトが発生した場合、またはクライアントがサーバーから切断された場合、ZooKeeper はセッションとそのセッションによって作成された一時データ ノードをすべてクリーンアップし、クライアントとの接続を閉じます。

  • セッションはセッション エンティティとしてクライアント セッションを表し、次の 4 つの属性が含まれます。
  • SessionID は、セッションをグローバルに一意に識別するために使用されます。
  • TimeOutはセッションタイムアウトイベントです。クライアントがセッションインスタンスを作成すると、セッションタイムアウト期間が設定されます。
  • TickTimeは次のセッションのタイムアウトポイントです。これは後述の「バケット化戦略」セクションで使用されます。

isClosing は、サーバーがセッションがタイムアウトしたことを検出した場合にセッションを閉じるプロパティです。

セッションはクライアントとサーバー間の接続であるため、SessionTracker はサーバー側でセッションを管理します。

SessionTracker のタスクの一つは、タイムアウトしたセッションをクリアすることです。ここで「バケット化戦略」が役立ちます。

各セッションは生成時に定義されたタイムアウト期間を持つため、セッションの有効期限は現在の時刻にタイムアウト期間を加算することで計算できます。

SessionTracker はセッション タイムアウトをリアルタイムで監視しないため、一定の時間間隔で監視します。

つまり、SessionTrackerのチェック期間がまだ到来していない場合、SessionTrackerは期限切れのセッションをクリアしません。そのため、セッションタイムアウトの計算式(TickTime計算式とも呼ばれます)が導入されています。

TickTime = ((現在の時刻 + セッション有効期限) / チェック間隔 + 1) * チェック間隔。

この値を計算した後、SessionTracker は対応するセッションを対応するタイムラインに配置します。次に、SessionTracker は対応する TickTime でセッションが期限切れになっているかどうかを確認します。

セッションの次の有効期限を計算します。

クライアントがサーバーに接続するたびにアクティベーション操作が実行され、定期的にクライアントからサーバーにハートビート チェックが送信されます。

サーバーはアクティベーションまたはハートビート検出を受信すると、セッションの有効期限を再計算し、「バケット戦略」に従って再調整して、セッションを「古いブロック」から「新しいブロック」に移動します。

有効期限を再計算し、「バケット戦略」を調整します。

タイムアウトしたセッションの場合、SessionTracker は次のクリーンアップ タスクも実行します。

  • セッション ステータスを「closed」としてマークします。つまり、isClosing を True に設定します。
  • 「セッション終了」要求を開始して、終了操作をクラスター全体で有効にします。
  • クリーンアップする必要がある一時ノードを収集します。
  • 「ノード削除」のトランザクション変更を追加します。
  • 一時ノードを削除
  • セッションを削除
  • クライアントとサーバー間の接続を閉じます。

セッションが閉じられると、クライアントはサーバーからデータを取得/書き込むことができなくなります。

サービスグル​​ープ(リーダー、フォロワー、オブザーバー)

前のセクションでは、クライアントがセッションを通じてサーバーとの接続を維持する方法と、サーバーがクライアント セッションを管理する方法について説明しました。

さらに考えてみましょう。非常に多くのサーバーが単一のZooKeeperサーバーに依存しています。サービスがダウンすると、クライアントは動作を停止します。

ZooKeeperサービスの信頼性を向上させるため、サーバークラスターの概念が導入されました。単一のサーバーではなく複数のサーバーに拡張されたため、1つのサーバーに障害が発生しても他のサーバーが引き継ぐことができます。

ZooKeeper サーバー クラスター

これは良さそうですが、新たな問題が発生します。複数の ZooKeeper サーバーがある場合、クライアントはどのサーバーにリクエストを送信すればよいのでしょうか?サーバーはどのようにデータを同期するのでしょうか?1 つのサービスがダウンした場合、他のサーバーはどのように引き継ぐのでしょうか?ここで、「リーダー」と「フォロワー」という 2 つの概念を導入します。

リーダーサーバーは、トランザクションリクエスト(書き込み操作)の唯一のスケジューラおよびプロセッサであり、クラスター内のトランザクションの連続処理を保証します。また、クラスター内のサーバーのスケジューラでもあります。

クラスタ全体のリーダーです。他のサーバーは、トランザクション要求をこのノードに転送し、調整と処理を行います。

フォロワーサーバーは、非トランザクションリクエスト(読み取り操作)を処理し、トランザクションリクエストをリーダーサーバーに転送します。また、リーダー選出のための投票やトランザクションリクエスト提案への投票にも参加します。

リーダーはクラスターのリーダーですが、このリーダーはどのように選出されるのでしょうか? ZooKeeper には調停メカニズムがあり、リーダーは多数決の原則に従ってサーバーによって選出されます。

したがって、クラスター内のサーバーの数は、一般的に1、3、5などの奇数になります。これはあくまで提案です。選出と調停には具体的なアルゴリズムがありますので、見ていきましょう。

多くのサーバーが起動すると、どのサーバーがリーダーであるかがわからないため、すべてのサーバーが Looking 状態になり、ネットワーク内のリーダーを検索している状態になります。

検索プロセスは投票プロセスでもあります。各サーバーは、自身のサーバーIDとトランザクションIDを投票情報としてネットワーク内の他のサーバーに送信します。この投票情報には、(ServerID, ZXID)が含まれます。

このうち、ServerID はサーバーの登録 ID であり、サーバーの起動順に自動的に増加し、後から起動したサーバーの ServerID の方が大きくなります。また、ZXID はサーバーのトランザクション ID であり、トランザクションの数に応じて自動的に増加し、後からコミットしたトランザクションの ZXID も大きくなります。

VOTE メッセージを受信すると、他のサーバーはそれを自身の VOTE メッセージ (ServerID、ZXID) と比較します。

受信した VOTE (ServerID、ZXID) の ZXID が自分の ZXID より大きい場合は、受信した VOTE と一致するように自分の VOTE を変更します。

ZXID が同じ場合は、ServerID が比較され、大きい方の ServerID が VOTE の ServerID として使用され、他のサーバーに転送されます。

簡単に言うと、トランザクションID(ZXID)が自身のトランザクションID(ZXID)より大きい場合、そのサーバーに投票が行われます。トランザクションIDが同じ場合は、ServerIDが大きい方のサーバーに投票が行われます。

具体的な例を挙げると、サーバーは3つあり、それぞれの投票値は次のとおりです。

  • S1(1、6)
  • S2 (2, 5)
  • S3 (3, 5)

3台のサーバーはそれぞれ他の2台のサーバーにVOTEを送信します。VOTEを受信したS2とS3は、ZXIDが6であるS1からのVOTEが自身のZXIDより大きいことに気づきます。そのため、S2とS3はVOTEを(1, 6)に変更して送信します。これにより、S1がリーダーと呼ばれます。

リーダー選挙の例

同様に、リーダーである S1 が何らかの理由でクラッシュしたり、長時間にわたって要求に応答できなかったりすると、他のサーバーは Looking 状態に入り、次のリーダーを見つけるための投票仲裁モードを開始します。

新しいリーダーになると、ブロードキャストを介して ZNode 上のデータを他のフォロワーに同期します。

リーダーが確立されると、サーバークラスター全体に、クライアントのトランザクション要求を処理できるリーダーが誕生します。クライアントからの要求はクラスター内の任意のサーバーに送信でき、各サーバーはトランザクション要求をリーダーに転送します。

リーダーは、ZNode にデータを書き込む前に、そのデータを ZooKeeper の他のフォロワーにブロードキャストします。

ここでのブロードキャストは、Paxosプロトコルの実装であるZAB(Atomic Broadcast Protocol)を使用します。簡単に言えば、これは2フェーズコミットです。

PS: 分散トランザクションに精通している人は、2 フェーズ コミットと 3 フェーズ コミットについて知っているはずです。

ここで、ZooKeeper は次のように 2 部構成のコミットを実装します。

  • リーダーはすべてのフォロワーに提案を送信します。
  • フォロワーは PROPOSAL を受信すると、リーダーに ACK メッセージを返します。これは、PROPOSAL を受信して​​準備ができていることを示します。
  • リーダーは、フォロワーの半数以上(リーダー自身を含む)から ACK を受信すると、フォロワーに COMMIT を通知するメッセージを送信します。
  • COMMIT を受信すると、Follower は作業を開始し、データを ZNode に書き込みます。

ZABブロードキャストプロトコル

クラスターを率いるリーダーが選出されると、リーダーはクライアントからのリクエストを受け取った後、フォロワーの作業を調整することもできます。

では、多数のクライアントが存在し、特にそれらのクライアントすべてが読み取り操作を実行している場合、ZooKeeper サーバーはどのようにして多数のリクエストを処理するのでしょうか。ここで、オブザーバーの概念が登場します。

オブザーバーとフォロワーは基本的に同じです。非トランザクションリクエスト(読み取り操作)の場合、ノード内の情報を直接返すことができます(データはリーダーから同期されます)。

トランザクション要求(書き込み操作)は、リーダーに引き渡され、統一的に処理されます。オブザーバーは、多数のクライアントからの読み取り要求を処理するために存在します。

オブザーバーとフォロワーの違いは、オブザーバーはリーダーを選出するための仲裁投票に参加しないことです。

オブザーバーはリーダーとフォロワーのファミリーに加わります。

要約

この記事では、簡単な例を使用して ZooKeeper の主な機能と実装の原則を説明し、最後に要約します。

ZooKeeperは、分散システムの調整と管理において重要な役割を果たします。分散システムでは、その性質上、複数の物理ホストまたはネットワークに分散されたアプリケーションが関与します。

これらが連携して動作できるようにするため、ZooKeeper の ZNode は統合的な調整の重要な部分となります。クライアントはクライアントを介してサーバーの ZNode に接続し、ZNode データの変更をリッスンします。

一方、ZNode は永続性、一時的および順次的なプロパティ、およびバージョン管理をサポートしており、分散トランザクションとロック機能を有効にします。

ZooKeeperClient からサーバーへの各書き込み操作がトランザクションと見なされる場合、ZooKeeper サーバーは多数のトランザクションを維持し、「バケット化戦略」を通じてそれらを管理して、クライアントとサーバー間の調整された作業を確実に実行します。

サーバーの信頼性を向上させるため、ZooKeeper はサーバークラスターの概念を導入しました。仲裁メカニズムによってリーダーが選出され、他のフォロワーを誘導します。

すべてのトランザクションはリーダーによって処理され、2フェーズコミットを介して他のサーバーにブロードキャストされます。非トランザクションリクエストの処理効率を向上させるため、ZooKeeperにはオブザーバーが組み込まれています。

ZooKeeper には上記で説明したもの以外にも多くの機能が含まれており、スペースの制限により、ここですべてを網羅することはできません。

皆様にご理解いただきやすいよう、この記事ではいくつかの原則を簡略化してご説明いたしました。次回は、これらの原則についてより深くご説明できる機会をいただければ幸いです。

著者:崔浩

自己紹介:開発とアーキテクチャに関する16年間の経験があり、以前はHPの武漢デリバリーセンターで技術エキスパート、要件アナリスト、プロジェクトマネージャーを務め、その後はスタートアップ企業でテクニカル/プロダクトマネージャーを務めました。学習能力が高く、知識を共有することに熱心です。現在は、技術アーキテクチャと研究開発管理に注力しています。

[これは51CTOからのオリジナル記事です。提携サイトへの転載の際は、原著者と出典を51CTO.comと明記してください。]