DUICUO

ZooKeeper のコアコンセプトを理解するための包括的なガイド

ZooKeeperの第一印象

HadoopプロジェクトのオープンソースサブプロジェクトであるZookeeperは、分散データ整合性ソリューションの典型です。分散アプリケーション向けに、高性能、高可用性、そして厳密に順序付けられたアクセス制御を備えた分散コーディネーションサービスを提供することに特化しています。

1. ZooKeeperデータモデル

ZooKeeper はファイルシステムのようなデータ構造を維持しており、各サブディレクトリ (/WeChat、/WeChat/Official Account) は znode と呼ばれます。ファイルシステムと同様に、znode は簡単に追加・削除でき、単一の znode の配下の子 znode も追加・削除できます。ファイルシステムとの違いは、znode はデータを保存できる点です (厳密に言えば、znode は必ずデータを保存する必要があります。デフォルトは空文字列です)。

ZooKeeper はディレクトリ ノード構造を使用するため、ノードを取得または作成するときにはノードが「/」で始まる必要があります。そうでない場合、ノードを取得するときに「パスは / 文字で始まる必要があります」というエラーが発生します。

  1. [zk: localhost:2181(CONNECTED) 13] テストを取得 
  2. コマンドが失敗しました: java.lang.IllegalArgumentException: パスは / 文字で始まる必要があります

ルートノード名は「/XXX」である必要があり、子ノードを作成するときは、ルートノードディレクトリ「/XXX/CCC」または「/XXX/AAA」を含める必要があります。

たとえば、下の画像の「Programmer Insider」ノードを取得するには、完全なパスを連結する必要があります: get /WeChat/Official Account/Programmer Insider

/WeChat/公式アカウント/プログラマー向けヒントを入手

ここに画像の説明を挿入

znodeはバイトレベルまたはキロバイトレベルのデータを格納するために使用され、格納できるデータの最大量は1MBです(注:ノードのデータ量には、ノード自体に格納されているデータだけでなく、すべての子ノードの名前も含まれます。子ノードの名前はバイトに変換する必要があるため、znodeの子ノードの数は無制限ではありません)。ノードのストレージサイズは手動で変更できますが、通常は推奨されません。

2. znodeノード属性

znodeはデータを保存できるだけでなく、いくつかの特別な属性も持っています。次に、/testノードを作成し、その様々な属性の意味を分析します。

  1. [zk: localhost:2181(接続済み) 6] /test を取得 
  2. 456  
  3. cZxid = 0x59ac //  
  4. ctime = 2020年3月30日月曜日15:20:08 CST  
  5. mZxid = 0x59ad    
  6. mtime = 2020年3月30日月曜日15:22:25 CST  
  7. pZxid = 0x59ac    
  8. 変換= 0    
  9. データバージョン= 2    
  10. aclバージョン= 0    
  11. 一時所有者= 0x0  
  12. データ長= 3    
  13. 子供の人数= 0    
ノード属性注釈
cZxid このデータ ノードが作成された時点のトランザクション ID。
mZxid 最新のトランザクション ID が変更されたときに、データ ノードが変更されました。
pZxid 現在のノードの親ノードのトランザクションID
ctime データノードは [time] に作成されました。
mtime このデータノードの最終更新時刻
データバージョン現在のノードバージョン番号(変更ごとに1ずつ増加)
cバージョン子ノードのバージョン番号 (子ノードが変更された回数。変更ごとに 1 ずつ増加します)。
aclバージョン現在のノードの ACL バージョン番号 (ノードの ACL 権限が変更されるたびに値は 1 ずつ増加します)。
一時的な所有者一時ノード識別子: 現在のノードが一時ノードである場合、作成者のセッション ID (sessionId) が格納されます。それ以外の場合、値は 0 になります。
データ長現在のノードに保存されているデータの長さ
子供の人数現在のノードの子ノードの数

znode には多くの属性があることがわかりますが、最も重要なのは zxid、version、acl です。

ジド:

znode の状態が変化すると、ノードは zxid 形式のタイムスタンプを受け取ります。このタイムスタンプはグローバルに順序付けられ、znode が作成または更新されるたびに新しいタイムスタンプが生成されます。zxid1 の値が zxid2 の値より小さい場合、zxid2 の変更は zxid1 より後に発生したことになります。各 znode には、cZxid(ノード作成時刻)、mZxid(ノード変更時刻、子ノードとは無関係)、pZxid(このノードまたはその子ノードの最終作成時刻または最終変更時刻、孫ノードとは無関係)の 3 つの zxid 属性があります。

zxid 属性は主に ZooKeeper クラスターで使用されます。これについては、後ほどクラスターについて説明するときに詳しく説明します。

バージョン:

znode プロパティには、dataversion (データ バージョン番号)、cversion (子ノード バージョン番号)、aclversion (ノードの ACL 権限バージョン番号) の 3 つのバージョン番号があります。

znode 内のデータは複数のバージョンを持つことができます。ノードに複数のバージョンのデータが含まれている場合、そのノードのデータをクエリするにはバージョン番号を含める必要があります。znode 内のデータを変更するたびに、ノードの `dataVersion` 番号が増加します。クライアントが znode を要求すると、ノードデータとバージョン番号の両方が返されます。また、`dataVersion` が -1 の場合、操作中にバージョンは無視されます。ノードの権限を設定すると、`aclVersion` 番号が増加します。ACL 権限制御については後述します。

これを検証するために、/testノードのデータに変更を加え、dataVersionの変化を観察しました。dataVersion属性が3に変更され、バージョン番号が増加したことがわかりました。

  1. [zk: localhost:2181(接続済み) 10] /test 8888 を設定 
  2. cZxid = 0x59ac    
  3. ctime = 2020年3月30日月曜日15:20:08 CST  
  4. mZxid = 0x59b6    
  5. mtime = 2020年3月30日月曜日16:58:08 CST  
  6. pZxid = 0x59ac    
  7. 変換= 0    
  8. データバージョン= 3    
  9. aclバージョン= 0    
  10. 一時所有者= 0x0    
  11. データ長= 4    
  12. 子供の人数= 0  

3. znodeの種類

ZooKeeper には 4 種類の znode があり、クライアントを使用してノードを作成するときにタイプを指定する必要があります。

  1. zookeeper.create("/public account/programmer insider", "".getBytes(), Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
  • PERSISTENT - 永続ディレクトリノード: クライアントがノードを作成し、ZooKeeper から切断した後も、ノードは永続化され、クライアントが再接続しても存在し続けます。
  • PERSISTENT_SEQUENTIAL - 永続的シーケンシャルノード:クライアントがノードを作成し、ZooKeeperから切断した後も、ノードは永続化されます。クライアントが再接続しても、ノードは依然として存在します。ZooKeeperはノード名にシーケンシャル番号を割り当てます(例:/lock/0000000001、/lock/0000000002、/lock/0000000003)。
  • EPHEMERAL - 一時ディレクトリ ノード: このノードは、クライアントが ZooKeeper から切断された後に削除されます。
  • EPHEMERAL_SEQUENTIAL - 一時シーケンシャルノード: このノードは、クライアントがZooKeeperから切断されると削除されます。ノード名には、/lock/0000000001、/lock/0000000002、/lock/0000000003のようなシーケンシャル番号が割り当てられます。

ノードACL権限制御

ACLはアクセス制御リスト(Access Control List)の略です。これは、znodeノードのアクセス制御問題を解決するために使用されます。ZooKeeperのアクセス制御はznodeレベルに基づいているため、ノード間の権限は継承されないことに注意してください。つまり、子ノードは親ノードの権限を継承しません。

ZooKeeper では、ACL 権限を設定するための形式は、<schema>:<id>:<acl> の 3 つの部分で構成されます。

スキーマ: 認証方法を表す

  • world: 誰でもアクセスできることを示します。
  • 認証: 認証されたユーザーのみがアクセスできます。
  • ダイジェスト: ユーザー名とパスワードを認証 ID として使用して MD5 ハッシュ値を生成します。
  • host/ip: 認証にクライアントのホスト IP アドレスを使用します。

id: 選択したスキーマに応じて異なる、ID を識別するために使用される権限の範囲。

acl: ノードに割り当てる権限を指定します。ノード権限には、作成、削除、書き込み、読み取り、管理者権限があり、これらを総称してcdwraと呼びます。

1. ワールド: 誰でもアクセスできることを示します。

getAcl コマンドを使用して、権限が設定されていない znode ノードのデフォルトの権限を確認してみましょう。

  1. [zk: localhost:2181(接続済み) 12] getAcl /test  
  2. 「世界」、誰 
  3. cdrwa

ACL属性のないノードが表示されています。デフォルトのスキーマは「world」、スコープは「anyone」、ノード権限は「cdwra」で、誰でもアクセスできることを意味します。

では、スキーマがワールドではないノードにワールド権限を設定するにはどうすればよいでしょうか?

  1. setAcl /test world:anyone:crdwa

2. 認証: 認証されたユーザーのみがアクセスできます。

スキーマは認証認可を使用して、認証されたユーザーのみがアクセスできるようにします。そのため、最初のステップは認証されたユーザーを追加し、それらのユーザーに対してACL権限を設定することです。

addauthダイジェストテスト:パスワード(プレーンテキスト)

認証されたユーザーのパスワードはプレーンテキストであることに注意してください。

  1. [zk: localhost:2181(CONNECTED) 2] addauth digest user:user //username:password  
  2. [zk: localhost:2181(接続済み) 5] setAcl /test auth:user:crdwa  
  3. [zk: localhost:2181(接続済み) 6] getAcl /test  
  4. 「ダイジェスト」、ユーザー:ben+k/ 3JomjGj4mfd4fYsfM6p0A =  
  5. cdrwa

実際、このように設定することで、このノードをすべての認証済みユーザーに公開します。`setAcl /test auth:user:crdwa` は `setAcl /test auth::crdwa` と同等です。

3. ダイジェスト: ユーザー名: パスワード認証方法

ユーザー名:パスワード認証は特定の単一のユーザーを対象としており、この方法では最初に認証されたユーザーを追加する必要はありません。

ZooKeeper クライアントを使用してコード内で ACL を設定する場合、パスワードは平文で保存されます。ただし、zk.cli などのクライアントを使用する場合は、SHA1 と base64 を使用してパスワードを処理する必要があります。

  1. setAcl <パス>ダイジェスト: <ユーザー> : <パスワード暗号文 > : <acl>    
  2. setAcl /test ダイジェスト:user:jalRr+knv/ 6L2uXdenC93dEDNuE =:crdwa

では、パスワードはどのように暗号化されるのでしょうか?いくつかの方法があります。

シェルコマンドによる暗号化

  1. echo -n <ユーザー> : <パスワード> | openssl dgst -binary -sha1 | openssl base64

組み込みの ZooKeeper ライブラリ org.apache.zookeeper.server.auth.DigestAuthenticationProvider を使用して生成します。

  1. java -cp /zookeeper-3.4.13/zookeeper-3.4.13.jar:/zookeeper-3.4.13/lib/slf4j-api-1.7.25.jar \  
  2. org.apache.zookeeper.server.auth.DigestAuthenticationProvider \  
  3. ルート:ルート 
  4. ルート:root- >ルート:qiTlqPLK7XM2ht3HMn02qRpkKIE =

4. host/ip: 認証にクライアントのホスト IP アドレスを使用します。

この方法は理解しやすく、特定の IP アドレス、または IP アドレスの範囲を承認します。

  1. [zk: localhost:2181(接続済み) 3] setAcl /test0000000014 ip:127.0.0.1:crdwa  
  2. cZxid = 0x59ac    
  3. ctime = 2020年3月30日月曜日15:20:08 CST  
  4. mZxid = 0x59b6    
  5. mtime = 2020年3月30日月曜日16:58:08 CST  
  6. pZxid = 0x59ac    
  7. 変換= 0    
  8. データバージョン= 3    
  9. aclVersion = 3 // このバージョン番号は常に更新されています。  
  10. 一時所有者= 0x0    
  11. データ長= 4    
  12. 子供の人数= 0  

動物園の飼育員の魂の監視者

冒頭で述べたように、ZooKeeperはレジストリセンターとして機能し、Dubboにサービス登録と検出機能を提供します。しかし、ZooKeeperがなぜサービス登録と検出を実現できるのか疑問に思ったことはありませんか?そこで、ZooKeeperの中核機能であるWatcherについて説明します。

1. ウォッチャーとは何ですか?

ウォッチャーはZooKeeperの中核機能です。クライアント側のウォッチャーは、ノードとその子ノードのデータの変化を監視できます。これらの状態が変化すると、ZooKeeperサーバーはそのノードにウォッチャーを設定したすべてのクライアントに通知します。これにより、各クライアントは監視対象のノードの状態変化を迅速に検知し、対応する論理アクションを実行できます。

ウォッチャーについて簡単に紹介したので、ZooKeeper がサービス登録と検出をどのように実装しているかを分析してみましょう。ZooKeeper のサービス登録と検出は、主に znode データモデルとウォッチャーメカニズムを利用しています。一般的なプロセスは以下のとおりです。

ここに画像の説明を挿入

  • サービス登録: サービスプロバイダーは起動時に、ZooKeeperサーバーにサービス情報を登録します。これはノードの作成を意味します。例えば、ユーザーはcom.xxx.user.registerというサービスを登録し、関連データ(サービスプロバイダーのIPアドレス、ポートなど)をノードに保存します。
  • サービスディスカバリ: サービスコンシューマーは起動時に、設定された依存サービス情報に基づいてZooKeeperサーバーから登録済みサービス情報を取得し、監視機能を設定します。登録済みサービス情報を取得した後、サービスプロバイダー情報をローカルにキャッシュし、サービスを呼び出します。
  • サービス通知: サービスプロバイダが何らかの理由でダウンし、サービスの提供を停止すると、クライアントはZooKeeperサーバーとの接続を失い、ZooKeeperサーバー上の当該サービスプロバイダに対応するサービスノード(例:ユーザー登録サービス com.xxx.user.register)が削除されます。その後、ZooKeeperサーバーは、サービス com.xxx.user.register を登録し、ノードの削除をリッスンする監視を設定しているすべてのコンシューマーユーザーに非同期的に通知を送信します。コンシューマーは受信した通知に基づいて最新のサービスリストを取得し、ローカルキャッシュ内のサービスリストを更新します。

上記のプロセスは、ZooKeeper がサービスを登録および検出する機能の背後にある一般的な原則です。

2. ウォッチャー型

Znodeには、DataWatchとDataWatchesという2種類のウォッチを設定できます。DataWatchは、znode上のデータの変更に基づいてトリガーされます。トリガー条件は、getData()、exists()、setData()、create()です。

もう1つのタイプは、子ノードウォッチです。これは、znodeの子ノードが変更されたときにトリガーされるウォッチイベントです。トリガー条件はgetChildren()とcreate()です。

delete() メソッドが呼び出されてznodeが削除されると、データウォッチと子ウォッチの両方が同時にトリガーされます。削除されたノードに親ノードがある場合、親ノードが子ウォッチをトリガーします。

3. ウォッチャーの特徴

ノードの監視機能は1回限りのイベントです。クライアントが特定のノードに監視機能を設定し、そのノードのデータの変更をクライアントに通知すると、そのノードに対するクライアントの監視機能は無効になります。

このノードの監視を継続したい場合は、クライアントのリスナーコールバックでノードの監視イベントを再度Trueに設定する必要があります。そうしないと、クライアントはノードの変更通知を一度しか受信しません。

ZooKeeperによって実装された機能

サービス登録と検出機能はZooKeeperの氷山の一角に過ぎません。分散ロック、キュー、構成センターといった一連の機能も実装できます。ここでは原理のみを分析します。具体的な実装については、オンラインで詳細情報をご覧ください。

1. 分散ロック

ZooKeeper は、ウォッチャー機構と znode の順序付きノードに基づいており、本質的に分散ロックとして機能する可能性があります。まず、親ノード `/test/lock` がロックとして作成されます。このノードは永続ノード(PERSISTENT 型)が望ましいです。このロックを取得しようとする各クライアントは、親ノード `/test/lock` の下に、一時的なシーケンシャルな子ノードを作成します。

シーケンス番号は増分的に増加するため、最も小さいシーケンス番号を持つノードがロックを取得するように規定されています。例えば、クライアントがロックを取得すると、/test/lock ノードの下に /test/lock/seq-00000001 というノードが作成されます。このノードはシーケンス番号が最も小さいため、最初にロックを取得し、他のノードはロック取得の通知を待機します。/test/lock/seq-00000001 の処理が完了すると、ノードが削除され、ロックが解放されます。

では、ノード /test/lock/seq-00000002 はロックに関して誰から通知を受け取りたいのでしょうか?

ここでは、/test/lock/seq-00000002 ノードが /test/lock/seq-00000001 ノードをリッスンしています。/test/lock/seq-00000001 ノードが削除されると、/test/lock/seq-00000002 ノードに通知が送られ、自身が最小ノードかどうかを再度確認します。最小ノードであればロックを取得でき、そうでない場合は通知を待機し続けます。

同様に、ノード /test/lock/seq-00000003 はノード /test/lock/seq-00000002 をリッスンします。常に次のノードが前のノードをリッスンするようにし、すべてのノードが最小のノードをリッスンしないようにします。これにより、不要なリッスンを防ぎ、大量の無効な通知が生成されて「herd effect(群衆効果)」が生じるのを防ぎます。

Redis 分散ロックと比較すると、ZooKeeper 分散ロックは、多数のノードの作成および削除操作を処理する際のパフォーマンスが低いため、あまりお勧めできません。

2. 分散キュー

ZooKeeper での分散キューの実装も非常にシンプルです。znode の自然な「先入先出」(FIFO)原理を活用することで、最後に作成されたノードが常に最大になり、シーケンス番号が最も小さいノードが常にキューから削除されます。

3. 構成管理

現在、多くのオープンソースプロジェクトが設定の維持にZooKeeperを使用しています。例えば、メッセージキューKafkaはブローカー情報の維持にZooKeeperを使用し、Dubboはサービス設定情報を管理しています。この仕組みはウォッチャーメカニズムに基づいています。例えば、いくつかの設定を保存するために「/config」ノードが作成されます。クライアントはこのノードをリッスンし、「/config」ノードの設定情報が変更されると、すべてのクライアントに新しい設定情報を取得するように通知されます。

4. ネーミングサービス

ZooKeeper のネーミングサービス(サービス登録・検出とも呼ばれます)は、主に指定された名前に基づいて、リソースまたはサービスのアドレスやサービスプロバイダなどの情報を取得します。znode ノードの特性とウォッチャー機構を活用することで、サービス情報を動的に登録・取得するための設定センターとして機能し、サービス名とそれに対応するサーバーリスト情報を一元管理します。これにより、バックエンドサーバーの状態(オンライン、オフライン、ダウンタイム)をほぼリアルタイムで把握できます。