ZooKeeper プログラマーガイド
ZooKeeper を使用する分散アプリケーションの開発
- はじめに
- ZooKeeper データモデル
- ZooKeeper セッション
- ZooKeeper ウォッチ
- ACL を使用した ZooKeeper アクセス制御
- プラグ可能な ZooKeeper 認証
- 整合性保証
- バインディング
- ビルディングブロック:ZooKeeper 操作ガイド
- 注意点:よくある問題とトラブルシューティング
はじめに
このドキュメントは、ZooKeeper の調整サービスを利用した分散アプリケーションを作成しようとする開発者向けのガイドです。概念的な情報と実践的な情報が含まれています。
このガイドの最初の4つのセクションでは、さまざまな ZooKeeper の概念に関するより高度な議論を提示します。これらは、ZooKeeper の動作方法と使用方法の両方の理解に必要です。ソースコードは含まれていませんが、分散コンピューティングに関連する問題に関する知識があることを前提としています。この最初のグループのセクションは次のとおりです。
次の4つのセクションでは、実践的なプログラミング情報を提供します。これらは次のとおりです。
このドキュメントは、付録で、ZooKeeper 関連のその他の有用な情報へのリンクを含めて締めくくられています。
このドキュメントのほとんどの情報は、スタンドアロンの参照資料としてアクセスできるように記述されています。ただし、最初の ZooKeeper アプリケーションを開始する前に、ZooKeeper データモデルとZooKeeper 基本操作に関する章を少なくとも読むことをお勧めします。
ZooKeeper データモデル
ZooKeeper は、分散ファイルシステムと非常によく似た階層型名前空間を持っています。唯一の違いは、名前空間内の各ノードに、子ノードだけでなく、データも関連付けることができることです。ファイルがディレクトリにもなるファイルシステムのようなものです。ノードへのパスは常に正規化された絶対的なスラッシュ区切りのパスとして表現されます。相対参照はありません。次の制約に従って、任意のUnicode文字をパスで使用できます。
- ヌル文字(\u0000)はパス名の一部にすることができません。(これは C バインディングで問題を引き起こします。)
- 次の文字は、表示がうまくいかないか、混乱するような方法でレンダリングされるため、使用できません:\u0001 - \u001F および \u007F
- \u009F。
- 次の文字は許可されていません:\ud800 - uF8FF、\uFFF0 - uFFFF。
- "." 文字は他の名前の一部として使用できますが、"." と ".." は単独でパスのノードを示すために使用できません。ZooKeeper は相対パスを使用しないためです。次のものは無効です:"/a/b/./c" または "/a/b/../c"。
- トークン "zookeeper" は予約されています。
Zノード
ZooKeeper ツリーのすべてのノードは、zノードと呼ばれます。Zノードは、データの変更、ACL の変更に関するバージョン番号を含む stat 構造体を保持します。stat 構造体にはタイムスタンプもあります。バージョン番号とタイムスタンプを組み合わせることで、ZooKeeper はキャッシュを検証し、更新を調整できます。zノードのデータが変更されるたびに、バージョン番号が増加します。たとえば、クライアントがデータを取得するたびに、データのバージョンも受信します。また、クライアントが更新または削除を実行する場合、変更する zノードのデータのバージョンを指定する必要があります。指定したバージョンがデータの実際のバージョンと一致しない場合、更新は失敗します。(この動作はオーバーライドできます。)
注記
分散アプリケーションエンジニアリングでは、ノードという単語は、一般的なホストマシン、サーバー、アンサンブルのメンバー、クライアントプロセスなどを指すことがあります。ZooKeeper のドキュメントでは、zノードはデータノードを指します。サーバーは ZooKeeper サービスを構成するマシンを指し、クォーラムピアはアンサンブルを構成するサーバーを指し、クライアントは ZooKeeper サービスを使用するホストまたはプロセスを指します。
Zノードは、プログラマーがアクセスする主要なエンティティです。ここに言及する価値のあるいくつかの特性があります。
ウォッチ
クライアントは zノードにウォッチを設定できます。その zノードへの変更はウォッチをトリガーし、その後ウォッチをクリアします。ウォッチがトリガーされると、ZooKeeper はクライアントに通知を送信します。ウォッチの詳細については、ZooKeeper ウォッチセクションを参照してください。
データアクセス
名前空間の各 zノードに格納されているデータは、アトミックに読み書きされます。読み取りは、zノードに関連付けられたすべてのデータバイトを取得し、書き込みはすべてのデータを置き換えます。各ノードには、誰が何ができるかを制限するアクセス制御リスト(ACL)があります。
ZooKeeper は、汎用データベースや大規模オブジェクトストアとして設計されていません。代わりに、調整データを管理します。このデータは、構成、ステータス情報、ランデブーなどの形式で提供できます。さまざまな形式の調整データの共通の特性は、比較的少ないことです。キロバイト単位で測定されます。ZooKeeper クライアントとサーバーの実装には、zノードのデータが 1MB 未満であることを確認するためのサニティチェックがありますが、データは平均してそれよりもはるかに少ないはずです。比較的大きなデータサイズを操作すると、一部の操作にかかる時間が他の操作よりもはるかに長くなり、より多くのデータをネットワーク経由でストレージメディアに移動するのに必要な追加時間のために、一部の操作のレイテンシに影響します。大規模なデータストレージが必要な場合は、NFS や HDFS などのバルクストレージシステムにデータを格納し、ZooKeeper にストレージ場所へのポインタを格納するのが一般的なパターンです。
一時ノード
ZooKeeper には、一時ノードの概念もあります。これらの zノードは、zノードを作成したセッションがアクティブである限り存在します。セッションが終了すると、zノードは削除されます。この動作のため、一時 zノードには子ノードを持つことができません。セッションの一時ノードのリストは、getEphemerals() API を使用して取得できます。
getEphemerals()
指定されたパスのセッションによって作成された一時ノードのリストを取得します。パスが空の場合、セッションのすべての一時ノードをリストします。ユースケース - サンプルユースケースとしては、重複データエントリチェックのためにセッションの一時ノードのリストを収集する必要があり、ノードがシーケンシャルに作成されるため、重複チェックのために名前がわからない場合があります。その場合、getEphemerals() API を使用してセッションのノードリストを取得できます。これは、サービス検出の典型的なユースケースです。
シーケンスノード — 一意の命名
zノードを作成するときは、ZooKeeper にパス末尾に単調増加カウンターを追加するように要求することもできます。このカウンターは親 zノードに固有です。カウンターの形式は %010d です。つまり、0(ゼロ)パディングによる 10 桁です(カウンターはこのようにフォーマットされることでソートが簡素化されます)、つまり "
コンテナノード
3.5.3 で追加
ZooKeeper には、コンテナ zノードの概念があります。コンテナ zノードは、リーダー、ロックなどのレシピに役立つ特殊な目的の zノードです。コンテナの最後の子が削除されると、コンテナは将来のある時点でサーバーによって削除される候補になります。
このプロパティを考えると、コンテナ zノード内に子を作成するときに KeeperException.NoNodeException が発生する準備をする必要があります。つまり、コンテナ zノード内に子 zノードを作成するときは、常に KeeperException.NoNodeException を確認し、発生した場合はコンテナ zノードを再作成します。
TTL ノード
3.5.3 で追加
PERSISTENT または PERSISTENT_SEQUENTIAL zノードを作成する場合は、オプションで zノードの TTL をミリ秒単位で設定できます。zノードが TTL 内で変更されず、子ノードがない場合、将来のある時点でサーバーによって削除される候補になります。
注:TTL ノードは、デフォルトで無効になっているため、システムプロパティを介して有効にする必要があります。管理者ガイドで詳細を確認してください。適切なシステムプロパティを設定せずに TTL ノードを作成しようとすると、サーバーは KeeperException.UnimplementedException をスローします。
ZooKeeperにおける時間
ZooKeeper は時間を複数の方法で追跡します。
- Zxid ZooKeeper 状態へのすべての変更には、zxid(ZooKeeper トランザクションID)という形式のスタンプが付けられます。これにより、ZooKeeper へのすべての変更の全順序が公開されます。各変更には一意の zxid があり、zxid1 が zxid2 より小さい場合、zxid1 は zxid2 より前に発生しました。
- バージョン番号 ノードへのすべての変更は、そのノードのバージョン番号のいずれかを増やす原因になります。3 つのバージョン番号は、version(zノードのデータの変更回数)、cversion(zノードの子の変更回数)、aversion(zノードの ACL の変更回数)です。
- ティック マルチサーバー ZooKeeper を使用する場合、サーバーはティックを使用して、ステータスアップロード、セッションタイムアウト、ピア間の接続タイムアウトなどのイベントのタイミングを定義します。ティック時間は、最小セッションタイムアウト(ティック時間の2倍)を通じて間接的に公開されるだけです。クライアントが最小セッションタイムアウトよりも短いセッションタイムアウトを要求した場合、サーバーはクライアントにセッションタイムアウトが実際に最小セッションタイムアウトであることを伝えます。
- リアルタイム ZooKeeper は、zノードの作成時と zノードの変更時にタイムスタンプを stat 構造体に入れる場合を除き、リアルタイムまたは時計時間をまったく使用しません。
ZooKeeper Stat 構造体
ZooKeeper の各 zノードの Stat 構造体は、次のフィールドで構成されています。
- czxid この zノードの作成の原因となった変更の zxid。
- mzxid この zノードを最後に変更した変更の zxid。
- pzxid この zノードの子を最後に変更した変更の zxid。
- ctime この zノードが作成されたときのエポックからのミリ秒単位の時間。
- mtime この zノードが最後に変更されたときのエポックからのミリ秒単位の時間。
- version この zノードのデータの変更回数。
- cversion この zノードの子の変更回数。
- aversion この zノードの ACL の変更回数。
- ephemeralOwner zノードが一時ノードである場合、この zノードの所有者のセッションID。一時ノードでない場合は 0 になります。
- dataLength この zノードのデータフィールドの長さ。
- numChildren この zノードの子の数。
ZooKeeper セッション
ZooKeeper クライアントは、言語バインディングを使用してサービスへのハンドルを作成することで、ZooKeeper サービスとのセッションを確立します。ハンドルが作成されると、最初はCONNECTING状態になり、クライアントライブラリはZooKeeperサービスを構成するサーバーのいずれかに接続を試みます。接続されると、CONNECTED状態に遷移します。通常の動作中は、クライアントハンドルはこれらの2つの状態のいずれかになります。セッションの期限切れや認証エラーなど、回復不可能なエラーが発生した場合、またはアプリケーションが明示的にハンドルを閉じると、ハンドルはCLOSED状態に遷移します。次の図は、ZooKeeper クライアントの状態遷移を示しています。
クライアントセッションを作成するには、アプリケーションコードは、ZooKeeperサーバーに対応するホスト:ポートのペアをコンマで区切ったリストを含む接続文字列を提供する必要があります(例:「127.0.0.1:4545」または「127.0.0.1:3000,127.0.0.1:3001,127.0.0.1:3002」)。ZooKeeperクライアントライブラリは任意のサーバーを選択し、それに接続しようとします。この接続が失敗した場合、またはクライアントが何らかの理由でサーバーから切断された場合、クライアントは接続が(再)確立されるまで、リスト内の次のサーバーを自動的に試行します。
3.2.0で追加:接続文字列にオプションの "chroot" サフィックスを追加することもできます。これにより、すべてのパスをこのルートを基準にして解釈しながら(unix の chroot コマンドと同様)、クライアントコマンドが実行されます。使用する場合、例は次のようになります。「127.0.0.1:4545/app/a」または「127.0.0.1:3000,127.0.0.1:3001,127.0.0.1:3002/app/a」。クライアントは "/app/a" にルート指定され、すべてのパスはこのルートを基準とします。つまり、"/foo/bar" を取得/設定するなどの操作は、(サーバーの観点から)"/app/a/foo/bar" で実行されます。この機能は、特定のZooKeeperサービスの各ユーザーが異なるルート指定される可能性のあるマルチテナント環境で特に役立ちます。これにより、各ユーザーがルートを "/" としてアプリケーションをコーディングできるため、再利用がはるかに簡単になり、実際の場所(/app/aなど)は展開時に決定できます。
クライアントがZooKeeperサービスへのハンドルを取得すると、ZooKeeperは64ビットの数値で表されるZooKeeperセッションを作成し、クライアントに割り当てます。クライアントが異なるZooKeeperサーバーに接続する場合、接続ハンドシェイクの一部としてセッションIDを送信します。セキュリティ対策として、サーバーはセッションIDのパスワードを作成し、ZooKeeperサーバーはそれを検証できます。パスワードは、クライアントがセッションを確立するときに、セッションIDとともにクライアントに送信されます。クライアントは、新しいサーバーとセッションを再確立するたびに、このパスワードをセッションIDとともに送信します。
ZooKeeperセッションを作成するためのZooKeeperクライアントライブラリ呼び出しのパラメーターの1つは、ミリ秒単位のセッションタイムアウトです。クライアントは要求されたタイムアウトを送信し、サーバーはクライアントに提供できるタイムアウトを返します。現在の実装では、タイムアウトはtickTime(サーバー構成で設定)の少なくとも2倍、最大20倍である必要があります。ZooKeeperクライアントAPIは、ネゴシエートされたタイムアウトへのアクセスを許可します。
クライアント(セッション)がZKサービングクラスタからパーティション化されると、セッション作成中に指定されたサーバーのリストの検索を開始します。最終的に、クライアントと少なくとも1つのサーバー間の接続が再確立されると、セッションは「接続済み」状態に再び遷移するか(セッションタイムアウト値内で再接続された場合)、または「期限切れ」状態に遷移します(セッションタイムアウト後に再接続された場合)。切断のために新しいセッションオブジェクト(cバインディングでは新しいZooKeeper.classまたはzookeeperハンドル)を作成することはお勧めしません。ZKクライアントライブラリは再接続を処理します。「群衆効果」などの処理をクライアントライブラリに組み込んだヒューリスティックがあります。セッションの期限切れが通知された場合にのみ、新しいセッションを作成してください(必須)。
セッションの期限切れは、クライアントではなく、ZooKeeperクラスタ自体によって管理されます。ZKクライアントがクラスタとセッションを確立すると、上記の詳細な「タイムアウト」値を提供します。この値は、クラスタがクライアントのセッションの期限切れを判断するために使用されます。期限切れは、クラスタが指定されたセッションタイムアウト期間内にクライアントから何も受信しない場合(つまり、ハートビートがない場合)に発生します。セッションの期限切れ時に、クラスタはそのセッションが所有するすべてのエフェメラルノードを削除し、変更を接続されているすべてのクライアントにすぐに通知します(それらのznodesを監視しているすべての人)。この時点で、期限切れセッションのクライアントはまだクラスタから切断されており、クラスタへの接続を再確立できるようになるまで、セッションの期限切れは通知されません。クライアントはTCP接続がクラスタと再確立されるまで切断された状態のままであり、その時点で期限切れセッションのウォッチャーは「セッション期限切れ」通知を受信します。
期限切れセッションのウォッチャーから見た、期限切れセッションの状態遷移の例
- '接続済み':セッションが確立され、クライアントがクラスタと通信しています(クライアント/サーバー間の通信が正常に動作しています)
- ....クライアントはクラスタからパーティション化されています
- '切断済み':クライアントはクラスタとの接続を失いました
- ....時間が経過し、「タイムアウト」期間後にクラスタがセッションの期限切れを発生させます。クライアントはクラスタから切断されているため、何も見えません
- ....時間が経過し、クライアントはクラスタとのネットワークレベルの接続を回復します
- '期限切れ':最終的にクライアントがクラスタに再接続し、期限切れが通知されます
ZooKeeperセッション確立呼び出しへのもう1つのパラメーターは、デフォルトのウォッチャーです。ウォッチャーは、クライアントで状態変更が発生したときに通知されます。たとえば、クライアントがサーバーとの接続を失った場合、またはクライアントのセッションが期限切れになった場合などです。このウォッチャーは、初期状態を切断済みと見なす必要があります(つまり、クライアントライブラリによってウォッチャーに状態変更イベントが送信される前)。新しい接続の場合、ウォッチャーに送信される最初のイベントは通常、セッション接続イベントです。
セッションは、クライアントによって送信された要求によって存続します。セッションがアイドル状態になり、セッションのタイムアウトになる期間が経過すると、クライアントはPING要求を送信してセッションを存続させます。このPING要求により、ZooKeeperサーバーはクライアントがまだアクティブであることを認識できるだけでなく、クライアントはZooKeeperサーバーへの接続がまだアクティブであることを確認することもできます。PINGのタイミングは、切断された接続を検出し、新しいサーバーに再接続するための妥当な時間を確保するのに十分なほど保守的です。
サーバーへの接続が正常に確立されると(接続済み)、同期操作または非同期操作が実行され、次のいずれかが当てはまる場合に、クライアントライブラリが接続喪失(cバインディングの結果コード、Javaの例外 - バインディング固有の詳細についてはAPIドキュメントを参照)を生成する基本的に2つのケースがあります。
- アプリケーションは、もはやアクティブ/有効ではないセッションで操作を呼び出します。
- ZooKeeperクライアントは、そのサーバーへの保留中の操作がある場合、つまり保留中の非同期呼び出しがある場合に、サーバーから切断します。
3.2.0で追加 - SessionMovedException。一般にクライアントには表示されない内部例外として、SessionMovedExceptionがあります。この例外は、異なるサーバーで再確立されたセッションの接続で要求が受信されたために発生します。このエラーの一般的な原因は、クライアントがサーバーに要求を送信しますが、ネットワークパケットが遅延するため、クライアントがタイムアウトし、新しいサーバーに接続することです。遅延したパケットが最初のサーバーに到着すると、古いサーバーはセッションが移動したことを検出し、クライアント接続を閉じます。クライアントは通常、これらの古い接続から読み取らないため、このエラーは見ません(古い接続は通常閉じられます)。この状態が見られる状況の1つは、2つのクライアントが保存されたセッションIDとパスワードを使用して同じ接続を再確立しようとした場合です。クライアントの1つが接続を再確立し、2番目のクライアントは切断されます(ペアが無期限に接続/セッションを再確立しようとします)。
サーバーリストの更新。コンマで区切ったホスト:ポートのペアの新しいリストを提供することにより、クライアントが接続文字列を更新できるようにします。各ペアはZooKeeperサーバーに対応します。この関数は確率的ロードバランシングアルゴリズムを呼び出し、新しいリスト内のサーバーごとに予想される均一な接続数を達成することを目的として、クライアントが現在のホストから切断される可能性があります。クライアントが接続されている現在のホストが新しいリストにない場合、この呼び出しは常に接続が切断される原因となります。それ以外の場合は、サーバーの数が増加したか減少したか、その程度によって決定されます。
たとえば、前の接続文字列に3つのホストが含まれており、現在リストにはこれらの3つのホストとさらに2つのホストが含まれている場合、3つのホストのそれぞれに接続されているクライアントの40%がロードバランスのために新しいホストのいずれかに移動します。このアルゴリズムにより、クライアントは確率0.4で現在のホストへの接続を切断し、この場合、ランダムに選択された2つの新しいホストのいずれかに接続します。
別の例 - 5つのホストがあり、リストを更新して2つのホストを削除するとします。残りの3つのホストに接続されているクライアントは接続されたままですが、削除された2つのホストに接続されているすべてのクライアントは、ランダムに選択された3つのホストのいずれかに移動する必要があります。接続が切断されると、クライアントは確率的アルゴリズムを使用して接続する新しいサーバーを選択する特別なモードに移動し、ラウンドロビンだけではありません。
最初の例では、各クライアントは確率0.4で切断することを決定しますが、決定がされると、新しいサーバーをランダムに選択して接続しようとします。新しいサーバーのいずれにも接続できない場合にのみ、古いサーバーに接続しようとします。サーバーが見つかった後、または新しいリスト内のすべてのサーバーを試行して接続に失敗した後、クライアントはconnectStringから任意のサーバーを選択して接続を試みる通常の動作モードに戻ります。それが失敗した場合、ラウンドロビンで異なるランダムサーバーを継続して試行します(上記のサーバーの初期選択に使用されるアルゴリズムを参照)。
ローカルセッション。3.5.0で追加され、主にZOOKEEPER-1147によって実装されました。
- 背景:ZooKeeperでは、セッションの作成とクローズはコストがかかります。クォーラムの確認が必要なため、数千ものクライアント接続を処理する必要がある場合、ZooKeeperアンサンブルのボトルネックになります。そのため、3.5.0以降、クォーラム確認を必要としない新しいタイプのセッションであるローカルセッションを導入しました。この機能は、localSessionsEnabledを有効にすることで使用できます。
localSessionsUpgradingEnabledが無効になっている場合
-
ローカルセッションはエフェメラルノードを作成できません
-
ローカルセッションが失われると、ユーザーはセッションID/パスワードを使用して再確立することはできません。セッションとそのウォッチは完全に失われます。注:TCP接続の喪失は、必ずしもセッションの喪失を意味するものではありません。セッションタイムアウト前に同じZooKeeperサーバーとの接続を再確立できれば、クライアントは続行できます(別のサーバーに移動することはできません)。
-
ローカルセッションが接続されると、セッション情報は接続先のZooKeeperサーバーのみに保持されます。リーダーは、そのようなセッションの作成を認識せず、ディスクに状態が書き込まれません。
-
ピング、期限切れ、その他のセッション状態の維持は、現在のセッションが接続されているサーバーによって処理されます。
localSessionsUpgradingEnabled が有効な場合
-
ローカルセッションは、グローバルセッションに自動的にアップグレードできます。
-
新しいセッションが作成されると、ラップされたLocalSessionTrackerにローカルに保存されます。その後、必要に応じてグローバルセッションにアップグレードできます(例:エフェメラルノードの作成)。アップグレードが要求されると、セッションIDを維持したまま、ローカルコレクションからセッションが削除されます。
-
現在、エフェメラルノードの作成という操作のみが、ローカルからグローバルへのセッションアップグレードを必要としています。これは、エフェメラルノードの作成がグローバルセッションに大きく依存しているためです。ローカルセッションがグローバルセッションにアップグレードせずにエフェメラルノードを作成できると、異なるノード間でデータの不整合が発生します。また、リーダーは、クローズ/期限切れ時にエフェメラルノードをクリーンアップするために、セッションの寿命を知る必要があります。そのためには、ローカルセッションが特定のサーバーに結び付けられているため、グローバルセッションが必要です。
-
セッションはアップグレード中にローカルセッションとグローバルセッションの両方になることができますが、アップグレード操作を2つのスレッドで同時に呼び出すことはできません。
-
ZooKeeperServer(スタンドアロン)はSessionTrackerImplを使用します。LeaderZookeeperはLeaderSessionTrackerを使用し、SessionTrackerImpl(グローバル)とLocalSessionTracker(有効な場合)を保持します。FollowerZooKeeperServerとObserverZooKeeperServerはLearnerSessionTrackerを使用し、LocalSessionTrackerを保持します。セッションに関するクラスのUMLグラフ
+----------------+ +--------------------+ +---------------------+ | | --> | | ----> | LocalSessionTracker | | SessionTracker | | SessionTrackerImpl | +---------------------+ | | | | +-----------------------+ | | | | +-------------------------> | LeaderSessionTracker | +----------------+ +--------------------+ | +-----------------------+ | | | | | | | +---------------------------+ +---------> | | | UpgradeableSessionTracker | | | | | ------------------------+ +---------------------------+ | | | v +-----------------------+ | LearnerSessionTracker | +-----------------------+
-
Q&A
- ローカルセッションのアップグレードを無効にするための設定オプションがある理由は何ですか?
- 非常に多くのクライアントを処理する必要がある大規模な展開では、オブザーバーを介して接続するクライアントはローカルセッションのみであることがわかっています。そのため、これは、誰かが誤って多数のエフェメラルノードとグローバルセッションを作成するのを防ぐための安全策のようなものです。
-
セッションはいつ作成されますか?
- 現在の実装では、ConnectRequestの処理時と、createSessionリクエストがFinalRequestProcessorに到達したときに、ローカルセッションの作成を試みます。
-
セッションの作成がサーバーAで送信され、クライアントが別のサーバーBに切断し、そこで再度送信されて切断され、サーバーAに再接続された場合、どうなるでしょうか?
- クライアントがBに再接続すると、そのsessionIdはBのローカルセッショントラッカーに存在しません。そのため、Bは検証パケットを送信します。Aによって発行されたCreateSessionが検証パケットの到着前にコミットされた場合、クライアントは接続できます。それ以外の場合は、クォーラムがこのセッションをまだ認識していないため、クライアントはセッション期限切れになります。クライアントがAに再度接続しようとすると、セッションは既にローカルセッショントラッカーから削除されています。そのため、Aはリーダーに検証パケットを送信する必要があります。結果は、リクエストのタイミングに応じてBと同じになります。
ZooKeeper ウォッチ
ZooKeeperのすべての読み取り操作(getData()、getChildren()、exists())には、副作用としてウォッチを設定するオプションがあります。以下は、ZooKeeperによるウォッチの定義です。ウォッチイベントとは、ウォッチを設定したクライアントに送信されるワンタイムトリガーであり、ウォッチが設定されたデータが変更されたときに発生します。このウォッチの定義では、考慮すべき3つの重要なポイントがあります。
- ワンタイムトリガー データが変更されると、クライアントに1つのウォッチイベントが送信されます。たとえば、クライアントがgetData("/znode1", true)を実行し、後で/znode1のデータが変更または削除された場合、クライアントは/znode1のウォッチイベントを受け取ります。/znode1が再度変更されても、クライアントが新しいウォッチを設定する別の読み取りを実行しない限り、ウォッチイベントは送信されません。
- クライアントに送信 これは、イベントがクライアントに向かっていることを意味しますが、変更操作を開始したクライアントに成功した戻りコードが到達する前に、クライアントに到達しない可能性があります。ウォッチャーには、非同期的にウォッチが送信されます。ZooKeeperは順序保証を提供します。クライアントは、最初にウォッチイベントを確認するまで、ウォッチを設定した変更を確認することはありません。ネットワーク遅延やその他の要因により、異なるクライアントが異なる時間に更新からのウォッチと戻りコードを確認することがあります。重要なのは、異なるクライアントによって確認されるすべてのものが一貫した順序を持つことです。
- ウォッチが設定されたデータ これは、ノードが変更されるさまざまな方法を参照しています。ZooKeeperは、データウォッチと子ウォッチの2つのウォッチリストを維持していると考えることが役立ちます。getData()とexists()はデータウォッチを設定します。getChildren()は子ウォッチを設定します。あるいは、返されるデータの種類に応じてウォッチが設定されていると考えても役立ちます。getData()とexists()はノードのデータに関する情報を返し、getChildren()は子のリストを返します。したがって、setData()は、設定されているznodeのデータウォッチをトリガーします(設定が成功した場合)。成功したcreate()は、作成されているznodeのデータウォッチと親znodeの子ウォッチをトリガーします。成功したdelete()は、削除されているznodeのデータウォッチと子ウォッチ(子がいなくなるため)、および親znodeの子ウォッチの両方をトリガーします。
ウォッチは、クライアントが接続されているZooKeeperサーバーでローカルに維持されます。これにより、ウォッチの設定、維持、およびディスパッチが軽量になります。クライアントが新しいサーバーに接続すると、セッションイベントに対してウォッチがトリガーされます。サーバーから切断中は、ウォッチは受信されません。クライアントが再接続すると、以前に登録されたウォッチは再登録され、必要に応じてトリガーされます。一般的に、これはすべて透過的に行われます。ウォッチを見逃す可能性のあるケースが1つあります。まだ作成されていないznodeの存在に関するウォッチは、切断中にznodeが作成および削除された場合に見逃されます。
3.6.0の新機能:クライアントは、トリガーされたときに削除されず、登録されたznodeとすべての子znodeに対して再帰的に変更をトリガーする、znodeに対する永続的な再帰的なウォッチを設定することもできます。
ウォッチのセマンティクス
ZooKeeperの状態を読み取る3つの呼び出し(exists、getData、getChildren)を使用してウォッチを設定できます。次のリストは、ウォッチがトリガーできるイベントと、それらを有効にする呼び出しを詳しく説明しています。
- 作成イベント:existsへの呼び出しで有効になります。
- 削除イベント:exists、getData、getChildrenへの呼び出しで有効になります。
- 変更イベント:existsとgetDataへの呼び出しで有効になります。
- 子イベント:getChildrenへの呼び出しで有効になります。
永続的な再帰的なウォッチ
3.6.0の新機能:上記で説明した標準的なウォッチのバリエーションがあり、トリガーされたときに削除されないウォッチを設定できます。さらに、これらのウォッチは、NodeCreated、NodeDeleted、NodeDataChangedというイベントタイプをトリガーし、オプションで、ウォッチが登録されているznodeから始まるすべてのznodeに対して再帰的にトリガーします。永続的な再帰的なウォッチでは、NodeChildrenChangedイベントは冗長であるためトリガーされません。
永続的なウォッチは、addWatch()メソッドを使用して設定されます。(ワンタイムトリガーを除く)トリガーのセマンティクスと保証は、標準的なウォッチと同じです。イベントに関する唯一の例外は、再帰的な永続的なウォッチャーが子変更イベントをトリガーしないことです。これは冗長であるためです。永続的なウォッチは、ウォッチャートタイプWatcherType.Anyを使用してremoveWatches()で削除されます。
ウォッチの削除
removeWatchesを呼び出して、znodeに登録されているウォッチを削除できます。また、ローカルフラグをtrueに設定することにより、ZooKeeperクライアントは、サーバー接続がない場合でもローカルでウォッチを削除できます。次のリストは、ウォッチの削除が成功した後、トリガーされるイベントを詳しく説明しています。
- 子削除イベント:getChildrenを呼び出して追加されたウォッチャー。
- データ削除イベント:existsまたはgetDataを呼び出して追加されたウォッチャー。
- 永続削除イベント:永続的なウォッチを追加する呼び出しで追加されたウォッチャー。
ZooKeeper がウォッチについて保証すること
ウォッチに関して、ZooKeeperはこれらの保証を維持します。
-
ウォッチは、他のイベント、他のウォッチ、および非同期応答に関して順序付けられます。ZooKeeperクライアントライブラリは、すべてが順序どおりにディスパッチされることを保証します。
-
クライアントは、監視しているznodeに対応する新しいデータを確認する前に、そのznodeのウォッチイベントを確認します。
-
ZooKeeperからのウォッチイベントの順序は、ZooKeeperサービスによって確認された更新の順序に対応します。
ウォッチに関する留意事項
-
標準的なウォッチはワンタイムトリガーです。ウォッチイベントを取得し、今後の変更を通知する必要がある場合は、別のウォッチを設定する必要があります。
-
標準的なウォッチはワンタイムトリガーであり、イベントの取得とウォッチを取得するための新しいリクエストの送信の間に遅延があるため、ZooKeeperでノードに発生するすべての変更を確実に確認することはできません。znodeがイベントの取得とウォッチの再設定の間に複数回変更される可能性があるケースを処理する準備をしておいてください。(気にしないかもしれませんが、少なくともそれが起こる可能性があることを認識してください。)
-
ウォッチオブジェクト、または関数/コンテキストペアは、特定の通知に対して1回だけトリガーされます。たとえば、同じウォッチオブジェクトが同じファイルのexistsとgetData呼び出しに登録され、そのファイルが削除された場合、ウォッチオブジェクトはファイルの削除通知で1回だけ呼び出されます。
-
サーバーから切断すると(たとえば、サーバーが失敗した場合)、接続が再確立されるまで、ウォッチは取得されません。このため、セッションイベントは、すべての保留中のウォッチハンドラーに送信されます。安全モードに入るためにセッションイベントを使用してください。切断中はイベントを受信しないため、プロセスはそのモードで保守的に動作する必要があります。
ACL を使用した ZooKeeper アクセス制御
ZooKeeperは、ACLを使用して、そのznode(ZooKeeperデータツリーのデータノード)へのアクセスを制御します。ACLの実装は、UNIXファイルアクセス権限と非常によく似ています。ノードに対するさまざまな操作を許可/拒否するための権限ビットと、ビットが適用される範囲を使用します。標準的なUNIX権限とは異なり、ZooKeeperノードは、ユーザー(ファイルの所有者)、グループ、およびワールド(その他)の3つの標準的な範囲によって制限されません。ZooKeeperには、znodeの所有者の概念はありません。代わりに、ACLは、それらのIDに関連付けられたIDと権限のセットを指定します。
また、ACLは特定のznodeのみに関係することに注意してください。特に、子には適用されません。たとえば、/appがip:172.16.16.1のみによって読み取り可能で、/app/statusがワールド読み取り可能である場合、誰でも/app/statusを読み取ることができます。ACLは再帰的ではありません。
ZooKeeperは、プラグ可能な認証スキームをサポートしています。IDは、scheme:expressionという形式で指定されます。ここで、schemeはIDに対応する認証スキームです。有効な式のセットは、スキームによって定義されます。たとえば、ip:172.16.16.1は、ipスキームを使用してアドレス172.16.16.1を持つホストのIDであるのに対し、digest:bob:passwordは、digestスキームを使用して名前がbobのユーザーのIDです。
クライアントがZooKeeperに接続して自身を認証すると、ZooKeeperはクライアントに対応するすべてのIDをクライアントの接続に関連付けます。クライアントがノードにアクセスしようとすると、これらのIDはznodeのACLに対してチェックされます。ACLは、(scheme:expression, perms)のペアで構成されます。expressionの形式は、スキームに固有です。たとえば、ペア(ip:19.22.0.0/16, READ)は、19.22で始まるIPアドレスを持つすべてのクライアントにREAD権限を与えます。
ACL 権限
ZooKeeperは、次の権限をサポートしています。
- CREATE:子ノードを作成できます。
- READ:ノードからデータを取得し、その子ノードを一覧表示できます。
- WRITE:ノードのデータを設定できます。
- DELETE:子ノードを削除できます。
- ADMIN:権限を設定できます。
より詳細なアクセス制御のために、WRITE パーミッションからCREATE と DELETE パーミッションが分割されました。CREATE と DELETE の使用例は以下の通りです。
ZooKeeper ノードに対して A が SET 操作を実行できるようにしたいが、子ノードのCREATE や DELETE は許可したくない場合。
CREATE のみ、DELETE なし: クライアントは親ディレクトリに ZooKeeper ノードを作成することでリクエストを作成します。すべてのクライアントが追加できるようにしたいが、削除できるのはリクエストプロセッサのみの場合。(ファイルの APPEND パーミッションに似ています。)
また、ZooKeeper にファイル所有者の概念がないため、ADMIN パーミッションが存在します。ある意味、ADMIN パーミッションはエンティティを所有者として指定します。ZooKeeper は LOOKUP パーミッション(ディレクトリをリスト表示できなくても LOOKUP を許可するディレクトリの execute パーミッションビット)をサポートしていません。すべてのユーザーは暗黙的に LOOKUP パーミッションを持っています。これにより、ノードの状態を確認できますが、それ以上はできません。(問題は、存在しないノードに対して `zoo_exists()` を呼び出したい場合、確認するためのパーミッションがないことです。)
ADMIN パーミッションは ACL においても特別な役割を果たします。znode の ACL を取得するには、ユーザーに READ または ADMIN パーミッションが必要です。ただし、ADMIN パーミッションがない場合、ダイジェストハッシュ値はマスクされます。
組み込み ACL スキーム
ZooKeeper には次の組み込みスキームがあります。
- world スキームには、すべてのユーザーを表す単一の ID、anyone があります。
- auth は特別なスキームで、指定された式を無視し、代わりに現在のユーザー、クレデンシャル、およびスキームを使用します。SASL 認証のような user や DIGEST 認証のような user:password を含む、指定された式は、ZooKeeper サーバーが ACL を永続化するときに無視されます。ただし、ACL は scheme:expression:perms の形式と一致する必要があるため、式は ACL に指定する必要があります。このスキームは、ユーザーが znode を作成し、その znode へのアクセスをそのユーザーのみに制限するという一般的なユースケースのため、便宜上提供されています。認証されたユーザーがいない場合、auth スキームを使用して ACL を設定しようとすると失敗します。
- digest は username:password 文字列を使用して MD5 ハッシュを生成し、それを ACL ID ID として使用します。認証は、username:password をクリアテキストで送信することで行われます。ACL で使用される場合、式は username:base64 エンコードされた SHA1 パスワード ダイジェスト になります。
- ip は、クライアントホストの IP アドレスを ACL ID ID として使用します。ACL 式は addr/bits の形式で、addr の最上位 bits ビットは、クライアントホスト IP の最上位 bits ビットと照合されます。
- x509 は、クライアントの X500 プリンシパルを ACL ID ID として使用します。ACL 式は、クライアントの正確な X500 プリンシパル名です。セキュアポートを使用する場合、クライアントは自動的に認証され、x509 スキームの認証情報が設定されます。
ZooKeeper C クライアントAPI
ZooKeeper C ライブラリでは、次の定数が提供されています。
- const int ZOO_PERM_READ; // ノードの値を読み取り、その子ノードをリスト表示できます。
- const int ZOO_PERM_WRITE;// ノードの値を設定できます。
- const int ZOO_PERM_CREATE; // 子ノードを作成できます。
- const int ZOO_PERM_DELETE;// 子ノードを削除できます。
- const int ZOO_PERM_ADMIN; // `set_acl()` を実行できます。
- const int ZOO_PERM_ALL;// 上記のすべてのフラグを OR 結合したもの。
標準的な ACL ID は次のとおりです。
- struct Id ZOO_ANYONE_ID_UNSAFE; // (‘world’,’anyone’)
- struct Id ZOO_AUTH_IDS;// (‘auth’,’’)
ZOO_AUTH_IDS の空の ID 文字列は、「作成者の ID」と解釈する必要があります。
ZooKeeper クライアントには、3 つの標準 ACL が付属しています。
- struct ACL_vector ZOO_OPEN_ACL_UNSAFE; //(ZOO_PERM_ALL,ZOO_ANYONE_ID_UNSAFE)
- struct ACL_vector ZOO_READ_ACL_UNSAFE;// (ZOO_PERM_READ, ZOO_ANYONE_ID_UNSAFE)
- struct ACL_vector ZOO_CREATOR_ALL_ACL; //(ZOO_PERM_ALL,ZOO_AUTH_IDS)
ZOO_OPEN_ACL_UNSAFE は完全にオープンな ACL です。すべてのアプリケーションがノードに対して任意の操作を実行し、その子ノードを作成、リスト表示、削除できます。ZOO_READ_ACL_UNSAFE は、すべてのアプリケーションに対する読み取り専用のアクセス権です。CREATE_ALL_ACL は、ノードの作成者にすべての権限を付与します。作成者は、この ACL を使用してノードを作成する前に、サーバーによって認証されている必要があります(例:「digest」スキームを使用)。
次の ZooKeeper 操作は ACL を処理します。
- int zoo_add_auth (zhandle_t *zh,const char* scheme,const char* cert, int certLen, void_completion_t completion, const void *data);
アプリケーションは、`zoo_add_auth` 関数を使用して、サーバーに対して自身を認証します。アプリケーションが異なるスキームや ID を使用して認証する必要がある場合、この関数を複数回呼び出すことができます。
- int zoo_create (zhandle_t *zh, const char *path, const char *value,int valuelen, const struct ACL_vector *acl, int flags,char *realpath, int max_realpath_len);
`zoo_create(...)` 操作は新しいノードを作成します。`acl` パラメーターは、ノードに関連付けられた ACL のリストです。親ノードには、CREATE パーミッションビットが設定されている必要があります。
- int zoo_get_acl (zhandle_t *zh, const char *path,struct ACL_vector *acl, struct Stat *stat);
この操作は、ノードの ACL 情報を返します。ノードには、READ または ADMIN パーミッションが設定されている必要があります。ADMIN パーミッションがない場合、ダイジェストハッシュ値はマスクされます。
- int zoo_set_acl (zhandle_t *zh, const char *path, int version,const struct ACL_vector *acl);
この関数は、ノードの ACL リストを新しいリストに置き換えます。ノードには、ADMIN パーミッションが設定されている必要があります。
以下は、「foo」スキームを使用して自身を認証し、作成のみの権限を持つ一時ノード「/xyz」を作成する上記の API を使用するサンプルコードです。
注記
これは、ZooKeeper ACL との対話方法を具体的に示すことを目的とした非常に単純な例です。C クライアント実装の例については、.../trunk/zookeeper-client/zookeeper-client-c/src/cli.c を参照してください。
#include <string.h>
#include <errno.h>
#include "zookeeper.h"
static zhandle_t *zh;
/**
* In this example this method gets the cert for your
* environment -- you must provide
*/
char *foo_get_cert_once(char* id) { return 0; }
/** Watcher function -- empty for this example, not something you should
* do in real code */
void watcher(zhandle_t *zzh, int type, int state, const char *path,
void *watcherCtx) {}
int main(int argc, char argv) {
char buffer[512];
char p[2048];
char *cert=0;
char appId[64];
strcpy(appId, "example.foo_test");
cert = foo_get_cert_once(appId);
if(cert!=0) {
fprintf(stderr,
"Certificate for appid [%s] is [%s]\n",appId,cert);
strncpy(p,cert, sizeof(p)-1);
free(cert);
} else {
fprintf(stderr, "Certificate for appid [%s] not found\n",appId);
strcpy(p, "dummy");
}
zoo_set_debug_level(ZOO_LOG_LEVEL_DEBUG);
zh = zookeeper_init("localhost:3181", watcher, 10000, 0, 0, 0);
if (!zh) {
return errno;
}
if(zoo_add_auth(zh,"foo",p,strlen(p),0,0)!=ZOK)
return 2;
struct ACL CREATE_ONLY_ACL[] = {{ZOO_PERM_CREATE, ZOO_AUTH_IDS}};
struct ACL_vector CREATE_ONLY = {1, CREATE_ONLY_ACL};
int rc = zoo_create(zh,"/xyz","value", 5, &CREATE_ONLY, ZOO_EPHEMERAL,
buffer, sizeof(buffer)-1);
/** this operation will fail with a ZNOAUTH error */
int buflen= sizeof(buffer);
struct Stat stat;
rc = zoo_get(zh, "/xyz", 0, buffer, &buflen, &stat);
if (rc) {
fprintf(stderr, "Error %d for %s\n", rc, __LINE__);
}
zookeeper_close(zh);
return 0;
}
プラグ可能な ZooKeeper 認証
ZooKeeper は、さまざまな認証スキームを持つさまざまな環境で実行されるため、完全にプラグ可能な認証フレームワークがあります。組み込みの認証スキームでさえ、このプラグ可能な認証フレームワークを使用しています。
認証フレームワークの動作を理解するには、まず2つの主要な認証操作を理解する必要があります。フレームワークはまずクライアントを認証する必要があります。これは通常、クライアントがサーバーに接続するとすぐに実行され、クライアントから送信された情報またはクライアントに関する情報を検証し、接続に関連付けることで構成されます。フレームワークによって処理される2番目の操作は、クライアントに対応するACLのエントリを見つけることです。ACLエントリは`
public interface AuthenticationProvider {
String getScheme();
KeeperException.Code handleAuthentication(ServerCnxn cnxn, byte authData[]);
boolean isValid(String id);
boolean matches(String id, String aclExpr);
boolean isAuthenticated();
}
最初のメソッド `getScheme` は、プラグインを識別する文字列を返します。複数の認証方法をサポートしているため、認証クレデンシャルまたは `idspec` には常に `scheme:` が接頭辞として付けられます。ZooKeeper サーバーは、認証プラグインによって返されるスキームを使用して、スキームが適用される ID を決定します。
`handleAuthentication` は、クライアントが接続に関連付ける認証情報を送信するときに呼び出されます。クライアントは、情報が対応するスキームを指定します。ZooKeeper サーバーは、クライアントによって渡されたスキームと一致する `getScheme` を持つ認証プラグインに情報を渡します。`handleAuthentication` の実装者は、情報が悪意があると判断した場合、通常はエラーを返します。または、`cnxn.getAuthInfo().add(new Id(getScheme(), data))` を使用して接続に情報を関連付けます。
認証プラグインは、ACL の設定と使用の両方に関係しています。znode の ACL が設定されると、ZooKeeper サーバーはエントリの id 部分を `isValid(String id)` メソッドに渡します。id が正しい形式かどうかを確認するのはプラグイン次第です。たとえば、`ip:172.16.0.0/16` は有効な id ですが、`ip:host.com` は無効です。新しい ACL に "auth" エントリが含まれている場合、`isAuthenticated` を使用して、接続に関連付けられているこのスキームの認証情報を ACL に追加する必要があるかどうかを確認します。一部のスキームは auth に含めるべきではありません。たとえば、クライアントの IP アドレスは、auth が指定されている場合、ACL に追加する必要がある ID としては考慮されません。
ACL をチェックする場合、ZooKeeper は `matches(String id, String aclExpr)` を呼び出します。クライアントの認証情報を関連する ACL エントリと照合する必要があります。クライアントに適用されるエントリを見つけるために、ZooKeeper サーバーは各エントリのスキームを見つけ、そのスキームに対してクライアントからの認証情報がある場合、`matches(String id, String aclExpr)` は、`id` を以前に `handleAuthentication` によって接続に追加された認証情報に設定し、`aclExpr` を ACL エントリの id に設定して呼び出されます。認証プラグインは独自のロジックと照合スキームを使用して、`id` が `aclExpr` に含まれているかどうかを判断します。
組み込みの認証プラグインには、`ip` と `digest` の 2 つがあります。システムプロパティを使用して追加のプラグインを追加できます。起動時に、ZooKeeper サーバーは「zookeeper.authProvider.」で始まるシステムプロパティを探し、これらのプロパティの値を認証プラグインのクラス名として解釈します。これらのプロパティは `-Dzookeeeper.authProvider.X=com.f.MyAuth` を使用して設定するか、サーバー設定ファイルに次のようなエントリを追加することで設定できます。
authProvider.1=com.f.MyAuth
authProvider.2=com.f.MyAuth2
プロパティのサフィックスが一意であることを確認する必要があります。`-Dzookeeeper.authProvider.X=com.f.MyAuth -Dzookeeper.authProvider.X=com.f.MyAuth2` のような重複がある場合、1 つしか使用されません。また、すべてのサーバーで同じプラグインを定義する必要があります。そうしないと、プラグインによって提供される認証スキームを使用するクライアントは、一部のサーバーへの接続に問題が発生します。
3.6.0 で追加:プラグ可能な認証には、代替の抽象化が利用可能です。追加の引数を提供します。
public abstract class ServerAuthenticationProvider implements AuthenticationProvider {
public abstract KeeperException.Code handleAuthentication(ServerObjs serverObjs, byte authData[]);
public abstract boolean matches(ServerObjs serverObjs, MatchValues matchValues);
}
`AuthenticationProvider` を実装する代わりに、`ServerAuthenticationProvider` を拡張します。その場合、`handleAuthentication()` と `matches()` メソッドは追加のパラメーター(`ServerObjs` と `MatchValues` を介して)を受け取ります。
- ZooKeeperServer ZooKeeperServer インスタンス
- ServerCnxn 現在の接続
- path 操作対象の ZNode パス(使用されない場合は null)
- perm 操作値または 0
- setAcls `setAcl()` メソッドが操作されている場合、設定されている ACL のリスト
整合性保証
ZooKeeper は高性能でスケーラブルなサービスです。読み取り操作と書き込み操作の両方とも高速に設計されていますが、読み取り操作の方が書き込み操作よりも高速です。これは、読み取りの場合、ZooKeeper は古いデータを提供できるためであり、これは ZooKeeper の一貫性保証によるものです。
-
逐次一貫性:クライアントからの更新は、送信された順序で適用されます。
-
原子性:更新は成功するか失敗するかのどちらかです。部分的な結果は存在しません。
-
単一システムイメージ:クライアントが接続するサーバーに関係なく、クライアントはサービスの同じビューを表示します。つまり、クライアントが同じセッションを持つ別のサーバーにフェイルオーバーした場合でも、クライアントは古いシステムビューを見ることはありません。
-
信頼性:更新が適用されると、クライアントが更新を上書きするまで、その時点から永続的に保持されます。この保証には2つの系があります。
- クライアントが成功の戻りコードを取得した場合、更新は適用されます。一部のエラー(通信エラー、タイムアウトなど)では、クライアントは更新が適用されたかどうかを知りません。エラーを最小限に抑えるための措置を講じていますが、保証は成功した戻りコードでのみ存在します。(これは、Paxos の単調性条件と呼ばれます。)
- クライアントが読み取りリクエストまたは成功した更新を通じて見た更新は、サーバー障害からの復旧時にロールバックされることはありません。
-
タイムリーネス:クライアントのシステムビューは、一定の時間範囲内(数十秒程度)で最新の状態であることが保証されます。システムの変更は、この時間範囲内でクライアントに反映されるか、またはクライアントはサービス停止を検知します。
これらの整合性保証を利用することで、リーダー選出、バリア、キュー、読み取り/書き込み取り消し可能なロックなどの高レベル関数をZooKeeperクライアントのみで簡単に構築できます(ZooKeeperへの追加は不要です)。詳細はレシピとソリューションを参照してください。
注記
開発者は、ZooKeeperが実際には提供していない別の保証を誤って想定することがあります。それは、同時整合性のあるクロスクライアントビューです。ZooKeeperは、あらゆる時点で2つの異なるクライアントがZooKeeperデータの同一のビューを持つことを保証しません。ネットワーク遅延などの要因により、あるクライアントが更新を実行する前に、別のクライアントが変更の通知を受け取る場合があります。クライアントAとクライアントBのシナリオを考えてみましょう。クライアントAがzノード/aの値を0から1に設定し、その後クライアントBに/aの読み取りを指示した場合、クライアントBは接続しているサーバーによっては古い値0を読み取る可能性があります。クライアントAとクライアントBが同じ値を読み取る必要がある場合、クライアントBは読み取りを実行する前にZooKeeper APIメソッドからsync()メソッドを呼び出す必要があります。したがって、ZooKeeper自体は、すべてのサーバー間で変更が同期的に発生することを保証しませんが、ZooKeeperプリミティブを使用して、有用なクライアント同期を提供する高レベル関数を構築できます。(詳細については、ZooKeeperレシピを参照してください。)
バインディング
ZooKeeperクライアントライブラリは、JavaとCの2つの言語で提供されています。以降のセクションでは、これらについて説明します。
Java バインディング
ZooKeeper Javaバインディングを構成するパッケージは、org.apache.zookeeperとorg.apache.zookeeper.dataの2つです。ZooKeeperを構成する残りのパッケージは、内部的に使用されるか、サーバー実装の一部です。org.apache.zookeeper.dataパッケージは、単純なコンテナとして使用される生成されたクラスで構成されています。
ZooKeeper Javaクライアントで使用される主なクラスは、ZooKeeperクラスです。その2つのコンストラクタは、オプションのセッションIDとパスワードだけが異なります。ZooKeeperは、プロセスのインスタンス間でセッションの復旧をサポートします。Javaプログラムは、セッションIDとパスワードを安定したストレージに保存し、再起動して、プログラムの以前のインスタンスで使用されていたセッションを復旧できます。
ZooKeeperオブジェクトが作成されると、IOスレッドとイベントスレッドの2つのスレッドも作成されます。すべてのIOはIOスレッド(Java NIOを使用)で行われます。すべてのイベントコールバックはイベントスレッドで行われます。ZooKeeperサーバーへの再接続やハートビートの維持などのセッションの維持は、IOスレッドで行われます。同期メソッドの応答もIOスレッドで処理されます。非同期メソッドとウォッチイベントへのすべての応答は、イベントスレッドで処理されます。この設計からわかることがいくつかあります。
- 非同期呼び出しとウォッチャーコールバックの完了はすべて、一度に1つずつ順番に行われます。呼び出し元は任意の処理を実行できますが、その間は他のコールバックは処理されません。
- コールバックは、IOスレッドの処理や同期呼び出しの処理をブロックしません。
- 同期呼び出しは、正しい順序で返されない場合があります。たとえば、クライアントが次の処理を行うとします。watchをtrueに設定してノード/aの非同期読み取りを行い、その後、読み取りの完了コールバックで/aの同期読み取りを行います。(必ずしも良い方法ではありませんが、違法でもなく、簡単な例になります。)非同期読み取りと同期読み取りの間に/aに変更があると、クライアントライブラリは同期読み取りの応答の前に/aが変更されたというウォッチイベントを受け取りますが、完了コールバックがイベントキューをブロックするため、同期読み取りはウォッチイベントが処理される前に/aの新しい値を返します。
最後に、シャットダウンに関連するルールは簡単です。ZooKeeperオブジェクトが閉じられたか、致命的なイベント(SESSION_EXPIREDおよびAUTH_FAILED)を受け取ると、ZooKeeperオブジェクトは無効になります。クローズ時に、2つのスレッドはシャットダウンし、zookeeperハンドルへのそれ以降のアクセスは未定義の動作となり、回避する必要があります。
クライアント設定パラメータ
次のリストには、Javaクライアントの構成プロパティが含まれています。これらのプロパティは、Javaシステムプロパティを使用して設定できます。サーバープロパティについては、管理ガイドのサーバー構成セクションを確認してください。ZooKeeper Wikiには、ZooKeeper SSLサポートとZooKeeperのSASL認証に関する便利なページもあります。
-
zookeeper.sasl.client:SASL認証を無効にするには、値をfalseに設定します。デフォルトはtrueです。
-
zookeeper.sasl.clientconfig:JAASログインファイルのコンテキストキーを指定します。デフォルトは "Client"です。
-
zookeeper.server.principal:Kerberos認証が有効になっている場合、ZooKeeperサーバーに接続する際にクライアントが認証に使用するサーバープリンシパルを指定します。この構成が提供されている場合、ZooKeeperクライアントは、サーバープリンシパルの決定に以下のパラメータを使用しません:zookeeper.sasl.client.username、zookeeper.sasl.client.canonicalize.hostname、zookeeper.server.realm 注:この設定パラメータは、ZooKeeper 3.5.7以降、3.6.0以降でのみ機能します。
-
zookeeper.sasl.client.username:従来、プリンシパルは、プライマリ、インスタンス、レルムの3つの部分に分かれています。一般的なKerberos V5プリンシパルの形式はprimary/instance@REALMです。zookeeper.sasl.client.usernameは、サーバープリンシパルのプライマリ部分を指定します。デフォルトは "zookeeper"です。インスタンス部分はサーバーIPから派生します。最後に、サーバーのプリンシパルはusername/IP@realmになります。ここで、usernameはzookeeper.sasl.client.usernameの値、IPはサーバーIP、realmはzookeeper.server.realmの値です。
-
zookeeper.sasl.client.canonicalize.hostname:zookeeper.server.principalパラメータが提供されていないと想定して、ZooKeeperクライアントはZooKeeperサーバープリンシパルの「インスタンス」(ホスト)部分を決定しようとします。まず、ZooKeeperサーバー接続文字列として提供されたホスト名を取得します。次に、アドレスに属する完全修飾ドメイン名を取得することで、アドレスを「正規化」しようとします。この「正規化」を無効にするには、次のように設定します:zookeeper.sasl.client.canonicalize.hostname=false
-
zookeeper.server.realm:サーバープリンシパルのレルム部分。デフォルトでは、クライアントプリンシパルのレルムです。
-
zookeeper.disableAutoWatchReset:このスイッチは、自動ウォッチリセットが有効になっているかどうかを制御します。クライアントはデフォルトでセッション再接続中にウォッチを自動的にリセットします。このオプションを使用すると、zookeeper.disableAutoWatchResetをtrueに設定することで、クライアントがこの動作をオフにすることができます。
-
zookeeper.client.secure:3.5.5の新機能:サーバーのセキュアクライアントポートに接続する場合は、クライアントでこのプロパティをtrueに設定する必要があります。これにより、指定された資格情報を使用してSSLを使用してサーバーに接続します。Nettyクライアントが必要です。
-
zookeeper.clientCnxnSocket:使用するClientCnxnSocketを指定します。可能な値はorg.apache.zookeeper.ClientCnxnSocketNIOとorg.apache.zookeeper.ClientCnxnSocketNettyです。デフォルトはorg.apache.zookeeper.ClientCnxnSocketNIOです。サーバーのセキュアクライアントポートに接続する場合は、クライアントでこのプロパティをorg.apache.zookeeper.ClientCnxnSocketNettyに設定する必要があります。
-
zookeeper.ssl.keyStore.locationとzookeeper.ssl.keyStore.password:3.5.5の新機能:SSL接続に使用されるローカル資格情報を含むJKSへのファイルパスと、ファイルをロック解除するためのパスワードを指定します。
-
zookeeper.ssl.keyStore.passwordPath:3.8.0の新機能:キーストアパスワードを含むファイルパスを指定します。
-
zookeeper.ssl.trustStore.locationとzookeeper.ssl.trustStore.password:3.5.5の新機能:SSL接続に使用されるリモート資格情報を含むJKSへのファイルパスと、ファイルをロック解除するためのパスワードを指定します。
-
zookeeper.ssl.trustStore.passwordPath:3.8.0の新機能:トラストストアパスワードを含むファイルパスを指定します。
-
zookeeper.ssl.keyStore.typeとzookeeper.ssl.trustStore.type:3.5.5の新機能:ZooKeeperサーバーへのTLS接続を確立するために使用されるキー/トラストストアファイルのファイル形式を指定します。値:JKS、PEM、PKCS12、またはnull(ファイル名で検出)。デフォルト:null。3.6.3、3.7.0の新機能:BCFKS形式が追加されました。
-
jute.maxbuffer:クライアント側では、サーバーから受信するデータの最大サイズを指定します。デフォルトは0xfffff(1048575)バイト、つまり1M未満です。これは実際にはサニティチェックです。ZooKeeperサーバーは、キロバイト単位のデータの保存と送信を行うように設計されています。受信データの長さがこの値を超えると、IOExceptionが発生します。クライアント側のこの値はサーバー側と同じにする必要があります(クライアント側でSystem.setProperty("jute.maxbuffer", "xxxx")を設定すると機能します)。そうでない場合、問題が発生します。
-
zookeeper.kinit:kinitバイナリのパスを指定します。デフォルトは"/usr/bin/kinit"です。
C バインディング
Cバインディングには、シングルスレッドとマルチスレッドのライブラリがあります。マルチスレッドライブラリは最も簡単に使用でき、Java APIと最も似ています。このライブラリは、接続の維持とコールバックの処理のために、IOスレッドとイベントディスパッチスレッドを作成します。シングルスレッドライブラリを使用すると、マルチスレッドライブラリで使用されているイベントループを公開することで、ZooKeeperをイベント駆動型アプリケーションで使用できます。
パッケージには、zookeeper_stとzookeeper_mtの2つの共有ライブラリが含まれています。前者は、アプリケーションのイベントループに統合するための非同期APIとコールバックのみを提供します。このライブラリが存在する唯一の理由は、pthreadライブラリが利用できないか不安定なプラットフォーム(つまりFreeBSD 4.x)をサポートするためです。それ以外の場合は、同期APIと非同期APIの両方をサポートしているため、アプリケーション開発者はzookeeper_mtをリンクする必要があります。
インストール
Apacheリポジトリからチェックアウトしたクライアントをビルドする場合は、以下に概説されている手順に従ってください。Apacheからダウンロードしたプロジェクトソースパッケージからビルドする場合は、手順3に進んでください。
- zookeeper-juteディレクトリ(.../trunk/zookeeper-jute)で
mvn compile
を実行します。これにより、.../trunk/zookeeper-client/zookeeper-client-cの下に "generated"という名前のディレクトリが作成されます。 - ディレクトリを`.../trunk/zookeeper-client/zookeeper-client-c`に変更し、`autoreconf -if`を実行して**autoconf**、**automake**、**libtool**をブートストラップします。**autoconf バージョン2.59以上**がインストールされていることを確認してください。ステップ**4**に進みます。
- プロジェクトソースパッケージからビルドする場合は、ソースターボールを解凍/展開し、`zookeeper-x.x.x/zookeeper-client/zookeeper-client-c`ディレクトリに移動します。
- `./configure
`を実行してMakefileを生成します。このステップで役立つ、**configure**ユーティリティがサポートするオプションの一部を以下に示します。
- `--enable-debug` 最適化を有効にし、デバッグ情報コンパイラオプションを有効にします。(デフォルトでは無効です。)
- `--without-syncapi` Sync APIサポートを無効にします。zookeeper_mtライブラリはビルドされません。(デフォルトでは有効です。)
- `--disable-static` 静的ライブラリをビルドしません。(デフォルトでは有効です。)
- `--disable-shared` 共有ライブラリをビルドしません。(デフォルトでは有効です。)
注記
**configure**の実行に関する一般的な情報はINSTALLを参照してください。1. ライブラリをビルドしてインストールするには、`make`または`make install`を実行します。1. ZooKeeper APIのDoxygenドキュメントを生成するには、`make doxygen-doc`を実行します。すべてのドキュメントは、docsという名前の新しいサブフォルダに配置されます。デフォルトでは、このコマンドはHTMLのみを生成します。他のドキュメント形式に関する情報は、`./configure --help`を実行してください。
独自の C クライアントのビルド
アプリケーションでZooKeeper C APIを使用できるようにするには、以下のことを覚えておく必要があります。
- ZooKeeperヘッダーを含める: `#include
` - マルチスレッドクライアントをビルドする場合は、`-DTHREADED`コンパイラフラグを使用してライブラリのマルチスレッドバージョンを有効にしてコンパイルし、`zookeeper_mt`ライブラリにリンクします。シングルスレッドクライアントをビルドする場合は、`-DTHREADED`を使用せずにコンパイルし、`zookeeper_st`ライブラリにリンクするようにしてください。
注記
Cクライアント実装の例については、`.../trunk/zookeeper-client/zookeeper-client-c/src/cli.c`を参照してください。
ビルディングブロック:ZooKeeper 操作ガイド
このセクションでは、開発者がZooKeeperサーバーに対して実行できるすべての操作について概説します。これは、このマニュアルの以前の概念に関する章よりも低レベルの情報ですが、ZooKeeper APIリファレンスよりも高レベルの情報です。以下のトピックを網羅しています。
エラー処理
JavaとCの両方のクライアントバインディングはエラーを報告する場合があります。Javaクライアントバインディングは、KeeperExceptionをスローすることでこれを行います。例外に対してcode()を呼び出すと、特定のエラーコードが返されます。Cクライアントバインディングは、enum ZOO_ERRORSで定義されているエラーコードを返します。APIコールバックは、両方の言語バインディングの結果コードを示します。考えられるエラーとその意味の詳細については、APIドキュメント(Javaの場合はjavadoc、Cの場合はdoxygen)を参照してください。
ZooKeeper への接続
始める前に、クライアントの開発を開始できるように、実行中のZookeeperサーバーをセットアップする必要があります。Cクライアントバインディングでは、Cで記述された簡単な例を使用して、マルチスレッドライブラリ(zookeeper_mt)を使用します。Zookeeperサーバーとの接続を確立するには、次のシグネチャを持つC API - `zookeeper_init`を使用します。
int zookeeper_init(const char *host, watcher_fn fn, int recv_timeout, const clientid_t *clientid, void *context, int flags);
-
**`host`**: ホスト:ポート形式のZookeeperサーバーへの接続文字列。複数のサーバーがある場合は、ホスト:ポートのペアを指定した後にコンマを使用します。例:「127.0.0.1:2181,127.0.0.1:3001,127.0.0.1:3002」
-
**`fn`**: 通知がトリガーされたときにイベントを処理するウォッチャー関数。
-
**`recv_timeout`**: セッションの有効期限(ミリ秒)。
-
**`clientid`**: 新しいセッションの場合は0を指定できます。以前にセッションがすでに確立されている場合は、そのクライアントIDを提供でき、以前のセッションに再接続します。
-
**`context`**: `zkhandle_t`ハンドラーに関連付けることができるコンテキストオブジェクト。使用しない場合は、0に設定できます。
-
**`flags`**: 開始時には0のままにすることができます。
接続が成功した後に「Zookeeperに接続しました」と出力するか、そうでない場合はエラーメッセージを出力するクライアントを実装します。このコードを`zkClient.cc`と呼びます。
#include <stdio.h>
#include <zookeeper/zookeeper.h>
#include <errno.h>
using namespace std;
// Keeping track of the connection state
static int connected = 0;
static int expired = 0;
// *zkHandler handles the connection with Zookeeper
static zhandle_t *zkHandler;
// watcher function would process events
void watcher(zhandle_t *zkH, int type, int state, const char *path, void *watcherCtx)
{
if (type == ZOO_SESSION_EVENT) {
// state refers to states of zookeeper connection.
// To keep it simple, we would demonstrate these 3: ZOO_EXPIRED_SESSION_STATE, ZOO_CONNECTED_STATE, ZOO_NOTCONNECTED_STATE
// If you are using ACL, you should be aware of an authentication failure state - ZOO_AUTH_FAILED_STATE
if (state == ZOO_CONNECTED_STATE) {
connected = 1;
} else if (state == ZOO_NOTCONNECTED_STATE ) {
connected = 0;
} else if (state == ZOO_EXPIRED_SESSION_STATE) {
expired = 1;
connected = 0;
zookeeper_close(zkH);
}
}
}
int main(){
zoo_set_debug_level(ZOO_LOG_LEVEL_DEBUG);
// zookeeper_init returns the handler upon a successful connection, null otherwise
zkHandler = zookeeper_init("localhost:2181", watcher, 10000, 0, 0, 0);
if (!zkHandler) {
return errno;
}else{
printf("Connection established with Zookeeper. \n");
}
// Close Zookeeper connection
zookeeper_close(zkHandler);
return 0;
}
前述のマルチスレッドライブラリを使用してコードをコンパイルします。
`> g++ -Iinclude/ zkClient.cpp -lzookeeper_mt -o Client`
クライアントを実行します。
`> ./Client`
出力から、接続が成功した場合は「Zookeeperに接続しました」とZookeeperのデバッグメッセージが表示されます。
注意点:よくある問題とトラブルシューティング
これでZooKeeperについて理解できました。高速でシンプルで、アプリケーションは動作しますが、待ってください…何かが間違っています。ZooKeeperユーザーが陥りがちな落とし穴をいくつか紹介します。
- ウォッチを使用している場合は、接続されたウォッチイベントを確認する必要があります。ZooKeeperクライアントがサーバーから切断されると、再接続するまで変更の通知は受信されません。zノードの存在を監視している場合、切断中にzノードが作成および削除されると、そのイベントを見逃します。
- ZooKeeperサーバーの障害をテストする必要があります。ZooKeeperサービスは、過半数のサーバーがアクティブであれば障害を生き残ることができます。問題は、アプリケーションがそれを処理できるかどうかです。現実世界では、クライアントのZooKeeperへの接続が切断される可能性があります。(ZooKeeperサーバーの障害とネットワークパーティションは、接続が失われる一般的な理由です。)ZooKeeperクライアントライブラリは、接続の復旧と発生した問題の通知を処理しますが、状態と失敗した未処理のリクエストを確実に復旧する必要があります。本番環境ではなくテストラボで正しく動作しているかどうかを確認してください。複数のサーバーで構成されたZooKeeperサービスでテストし、再起動を実行します。
- クライアントで使用されるZooKeeperサーバーのリストは、各ZooKeeperサーバーが持つZooKeeperサーバーのリストと一致する必要があります。クライアントリストが実際のZooKeeperサーバーリストのサブセットである場合は、最適ではありませんが動作する場合がありますが、クライアントがZooKeeperクラスタにないZooKeeperサーバーをリストアップする場合は動作しません。
- トランザクションログの配置場所には注意してください。ZooKeeperで最もパフォーマンスクリティカルな部分はトランザクションログです。ZooKeeperは、レスポンスを返す前に、メディアにトランザクションを同期する必要があります。専用のトランザクションログデバイスは、一貫した良好なパフォーマンスの鍵となります。ビジーなデバイスにログを配置すると、パフォーマンスに悪影響を与えます。ストレージデバイスが1つしかない場合は、トレースファイルをNFSに配置し、snapshotCountを増やします。問題は解決しませんが、軽減できます。
- Javaの最大ヒープサイズを正しく設定してください。*スワップを回避する*ことが非常に重要です。不要にディスクにアクセスすると、パフォーマンスが著しく低下する可能性が非常に高くなります。ZooKeeperではすべてが順序付けられているため、1つのリクエストがディスクにアクセスすると、キューに入れられている他のすべてのリクエストもディスクにアクセスします。スワップを回避するには、ヒープサイズを、OSとキャッシュに必要な量を引いた物理メモリの量に設定してみてください。構成に対して最適なヒープサイズを決定する最良の方法は、*負荷テストを実行する*ことです。何らかの理由で実行できない場合は、見積もりを控えめにし、マシンにスワップが発生する可能性のある制限をはるかに下回る数値を選択してください。たとえば、4GBのマシンでは、3GBのヒープは保守的な見積もりです。
その他の情報のリンク
正式なドキュメント以外にも、ZooKeeper開発者向けの他の情報源がいくつかあります。
-
APIリファレンス: ZooKeeper APIの完全なリファレンス
-
Hadoop Summit 2008でのZooKeeperトーク: Yahoo! ResearchのBenjamin ReedによるZooKeeperの紹介ビデオ
-
バリアとキューのチュートリアル: Flavio Junqueiraによる優れたJavaチュートリアル。ZooKeeperを使用して、単純なバリアとプロデューサーコンシューマーキューを実装しています。
-
ZooKeeper - 信頼性が高く、スケーラブルな分散調整システム: Todd Hoffによる記事(2008年7月15日)
-
ZooKeeperレシピ: ZooKeeperを使用したさまざまな同期ソリューションの実装に関する擬似レベルの議論:イベントハンドル、キュー、ロック、および2フェーズコミット。