Apache > Hadoop > ZooKeeper
 

ZooKeeper プログラマーズガイド

ZooKeeper を使った分散アプリケーションの開発

はじめに

このドキュメントは、ZooKeeper のコーディネーションサービスを利用した分散アプリケーションを作成したいと考えている開発者向けのガイドです。このガイドには、コンセプトに関する情報と、実際のプログラミングに関する情報の両方が含まれています。

このガイドの最初の 4 つのセクションでは、ZooKeeper のさまざまな概念について高い次元から説明します。ZooKeeper のしくみと ZooKeeper の使い方を理解するには、これらのセクションで説明するコンセプトについて知っておく必要があります。ソースコードは登場しませんが、分散コンピューティングに関連したさまざまな問題について知識があることを前提にしています。これらのセクションは、次のとおりです。

続く 4 つのセクションでは、実際のプログラミングに関する情報を取り上げます。これらのセクションは、次のとおりです。

このガイドの最後の付録では、ZooKeeper に関連したその他の有益な情報へのリンクを掲載しています。

このガイドのほとんどの情報は、リファレンスとしても利用できるように記述されています。ただし、初めての ZooKeeper アプリケーションを作成する前に、少なくとも「ZooKeeper のデータモデル」と「ZooKeeper の基本操作に関する部分は読んでおいた方がよいでしょう。また、「プログラムの構造と簡単な例[準備中] も、ZooKeeper クライアントアプリケーションの基本構造を理解するのに役立つはずです。

ZooKeeper のデータモデル

ZooKeeper は、分散ファイルシステムによく似た階層的な名前空間を持っています。唯一の違いは、名前空間内の各ノードは、そのノードに関連付けられたデータのほかに、複数の子を持つことができるという点です。たとえて言えば、ファイルが同時にディレクトリにもなれるファイルシステムのようなものです。ノードへのパスは常に、スラッシュで区切られた正規の絶対パスとして表現されます。相対参照はありません。パスには任意の Unicode 文字を使用できますが、次の制限があります。

  • null 文字 (\u0000) をパス名に含めることはできません (C バインディングを使うときに問題が生じます)。

  • 次の文字は、うまく表示されないか、表示されたときに問題を生じやすいので、使用できません: \u0001 - \u0019 および \u007F - \u009F。

  • 次の文字は使用できません: \ud800 - uF8FFF、 \uFFF0 - uFFFF、 \uXFFFE - \uXFFFF (X は 1 - E の数字)、\uF0000 - \uFFFFF。

  • "." 文字はほかの名前の一部としてなら使用できますが、ノードやパスを示すのに "." と ".." を単独で使用することはできません。したがって、次のようなパスは無効です: "/a/b/./c"、"/a/b/../c"。

  • "zookeeper" というトークンは予約されています。

ZNode

ZooKeeper ツリーの各ノードのことを znode といいます。znode が保持する Stat 構造体には、データ変更と ACL 変更のバージョン番号が含まれています。Stat 構造体にはタイムスタンプもあります。ZooKeeper では、バージョン番号とタイムスタンプを利用することで、キャッシュの有効性検証と更新のコーディネートを可能にしています。znode のデータが変更されると、そのたびにバージョン番号は増えていきます。たとえば、クライアントがデータを取得するとき、クライアントはデータのほかにデータのバージョンも一緒に受け取ります。クライアントは、更新や削除を実行するときに、変更対象となる znode のデータのバージョン番号も提示しなければなりません。クライアントが提示したバージョンが、対象となるデータの実際のバージョンと一致しない場合、更新は失敗します。(この動作はオーバーライドできます。詳細については、... を参照してください。)[準備中...]

分散アプリケーション分野では、ノードという語は、通常のホストマシン、サーバー、アンサンブルのメンバー、クライアントプロセスなど、さまざまなものを意味します。ZooKeeper のドキュメントでは、znode はデータノードを意味します。また、サーバーは、ZooKeeper サービスを構成するマシンを意味し、クォーラムピアはアンサンブルを構成するサーバーを意味します。クライアントは、ZooKeeper サービスを利用するホストまたはプロセスを意味します。

znode は、プログラミング時の主なアクセス対象となる重要なエンティティです。znode にはさまざまな特徴がありますが、そのうち重要なものを以下で説明していきます。

ウォッチ

クライアントは znode にウォッチを設定できます。該当する znode が変更されると、ウォッチがトリガされ、その後ウォッチはクリアされます。ウォッチがトリガされると、ZooKeeper はクライアントに通知を送信します。ウォッチの詳細については、後の「ZooKeeper のウォッチ」を参照してください。

データアクセス

1 つの名前空間内の各 znode に格納されたデータの読み書きはアトミックに行うことができます。読み取りでは、1 つの znode に関連付けられているすべてのデータバイトが取得され、書き込みではすべてのデータが置き換えられます。各ノードはアクセス制御リスト (ACL) を持っており、このアクセス制御リストで、誰がどんな操作を行えるかが制限されます。

ZooKeeper は、汎用データベースやラージオブジェクト (LOB) ストアとなることを目的に設計されたものではありません。ZooKeeper が管理するのは、コーディネーションデータです。具体的には、設定、ステータス情報、ランデブーなどです。さまざまな形式があるなかで、これらのコーディネーションデータに共通する特徴は、データサイズが比較的小さく、KB 単位であるということです。ZooKeeper クライアントとサーバーの実装では、znode の持つデータが 1MB 未満であることを保証するための健全性チェックを行っていますが、平均的な実際のデータサイズはそれよりはるかに小さいでしょう。比較的大きなデータサイズを対象とした操作では、データをネットワークやストレージメディアに移動するための時間が余計にかかるために、操作の一部が遅くなったり、遅延に影響が生じたりします。ラージデータのストレージが必要な場合には、データの格納先を NFS や HDFS などのバルクストレージシステムにし、これらのストレージの場所へのポインタを ZooKeeper に格納するのが一般的な対処方法です。

エフェメラルノード

ZooKeeper には、エフェメラルノード (ephemeral node: 一時ノード) という概念もあります。これらの znode は、znode を作成したセッションがアクティブになっている間だけ存在します。セッションが終了すると、znode は削除されます。こうしたふるまいをするために、エフェメラルノードは子を持つことができません。

シーケンスノード -- 一意の名前付け

znode を作成するときは、単調増加するカウンタをパスの最後に追加するよう、ZooKeeper に要求することもできます。このカウンタは、親 znode に対して一意です。カウンタの形式は %010d、すなわち先頭部分が 0 で埋められた 10桁の数字です (カウンタがこのような形式になっているのは、並べ替えを簡単にするためです)。たとえば、"<path>0000000001" のようになります。この機能の使用例については、「キューのレシピ」を参照してください。注意: 連番を格納するのに使われるカウンタの実体は、親ノードが保持する signed int (4 バイト) なので、2147483647 を超えるとカウンタはオーバーフローします (ノード名が "<path>-2147483647" になってしまいます)。

ZooKeeper での時間

ZooKeeper では、さまざまな方法で時間を追跡します。

  • zxid

    ZooKeeper の状態に対するあらゆる変更には、zxid (ZooKeeper Transaction Id) 形式のスタンプが付けられます。これによって、ZooKeeper に対するすべての変更の順序がわかるようになっています。変更はそれぞれ固有の zxid を持っており、もし zxid1 が zxid2 より小さければ、zxid1 は zxid2 より前に起こったことになります。

  • バージョン番号

    ノードに対するあらゆる変更は、そのノードが持つ複数のバージョン番号のいずれかを増やします。ノードが持つバージョン番号には、version (znode のデータの変更回数)、cversion (znode の子の変更回数)、および aversion (ノードの ACL の変更回数) の 3 つがあります。

  • tick

    ZooKeeper サービスが複数のサーバーから構成される場合、サーバーは tick を使って、ステータスアップロードやセッションタイムアウト、ピア間の接続タイムアウトといったイベントのタイミングを決定します。tick の時間は、最小セッションタイムアウト (tick 時間の 2 倍) という形で、間接的にのみ公開されます。クライアントが最小セッションタイムアウトより短いセッションタイムアウトを要求すると、サーバーはクライアントに対し、セッションタイムアウトは実際には最小セッションタイムアウトであることを知らせます。

  • 実時間

    ZooKeeper は、znode の作成時と znode の修正時に Stat 構造体にタイムスタンプを格納する場合を除いて、実時間、すなわちクロック時間を使用しません。

ZooKeeper の Stat 構造体

ZooKeeper の各 znode の Stat 構造体は、次のフィールドから構成されています。

  • czxid

    この znode を作成した変更の zxid です。

  • mzxid

    この znode を最後に修正した変更の zxid です。

  • ctime

    この znode が作成された時点での epoch からの経過時間 (ミリ秒単位) です。

  • mtime

    この znode が最後に修正された時点での epoch からの経過時間 (ミリ秒単位) です。

  • version

    この znode のデータの変更回数です。

  • cversion

    この znode の子の変更回数です。

  • aversion

    この znode の ACL の変更回数です。

  • ephemeralOwner

    この znode がエフェメラルノードである場合、znode のオーナーのセッション id です。この znode がエフェメラルノードでない場合、値は 0 です。

  • dataLength

    この znode のデータフィールドの長さです。

  • numChildren

    この znode の子の数です。

ZooKeeper のセッション

ZooKeeper クライアントは、言語バインディングを使って ZooKeeper サービスへのハンドルを作成することで、ZooKeeper サービスとセッションを確立します。作成されたハンドルは、CONNECTING 状態からスタートし、クライアントライブラリが ZooKeeper サービスを構成するサーバーのいずれかに接続を試み、接続した時点で CONNECTED 状態に切り替わります。正常な操作が行われている間は、この 2 つの状態のうちいずれかの状態になります。セッションの有効期限切れや認証失敗など、回復不能なエラーが発生した場合、またはアプリケーションが明示的にハンドルをクローズした場合には、ハンドルは CLOSED 状態に移行します。次の図は、ZooKeeper クライアントが取り得る状態遷移を表したものです。

アプリケーションコードでは、クライアントセッションを作成するために、ZooKeeper サーバーを表す host:port のペアをカンマで区切って並べた接続文字列を用意する必要があります ("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 セッション id を作成し、これをクライアントに割り当てます。クライアントが別の ZooKeeper サーバーに接続する場合、クライアントは接続ハンドシェイクの一部として、このセッション id を送信します。セキュリティ上の措置として、サーバーはセッション id のパスワードを作成しますが、このパスワードは任意のサーバーが検証できます。クライアントがセッションを確立すると、このパスワードがセッション id とともにクライアントに送信されます。クライアントは、新しいサーバーとの間でセッションを再確立するときは常に、このパスワードをセッション id と組み合わせて送信します。

ZooKeeper セッションを作成するための ZooKeeper クライアントライブラリの呼び出しに渡すパラメータの 1 つに、セッションタイムアウト (ミリ秒単位) があります。クライアントは要求されたタイムアウトを送信しますが、サーバーがレスポンスの中で返すのは、サーバーがクライアントに与えることができるタイムアウトです。現在の実装では、タイムアウトは tickTime (サーバーの設定ファイルで指定) の最低 2 倍で、最大 20 倍まででなければならないことになっています。ZooKeeper クライアント API を使えば、ネゴシエートされたタイムアウトを取得できます。

クライアント (セッション) が、ZooKeeper サービスを提供するクラスタから切り離されると、クライアントは、セッション作成中に指定されたリスト内のサーバーの探索を開始します。最終的にクライアントといずれかのサーバーとの間で接続が再確立されると、セッションは、再び "connected" 状態に遷移するか (セッションタイムアウト値の範囲内で再接続した場合)、または "expired" 状態に遷移します (セッションタイムアウト後に再接続した場合)。セッションの切断時に新しいセッションオブジェクト (新しい ZooKeeper.class、または C バインディングの場合は新しい ZooKeeper ハンドル) を作成することは推奨されません。ZooKeeper クライアントライブラリが、プログラマに代わって再接続を処理するからです。具体的には、クライアントライブラリにヒューリスティックを組み込んであり、「殺到現象 (herd effect)」などの問題に対処できるようにしています。新しいセッションの作成は、セッションの有効期限切れが通知された場合だけにとどめてください (必須)。

セッションの有効期限切れは、クライアントではなく、ZooKeeper クラスタ自体によって管理されます。ZooKeeper クライアントがクラスタとセッションを確立すると、クライアントは、すでに述べたように、「タイムアウト」値を提示します。クラスタ側ではこの値を使って、クライアントのセッションの有効期限がいつ切れたかを決定します。指定されたタイムアウト期間内にクライアントからの反応がない (すなわちハートビートがない) と、有効期限切れになります。セッションが有効期限切れになると、クラスタは、そのセッションによって所有されているすべてのエフェメラルノードを削除し、この変更を現在接続中のすべてのクライアント (該当する znode をウォッチしているクライアント) にただちに通知します。この時点では、セッションが有効期限切れになったクライアントはまだクラスタから切断された状態なので、セッションの有効期限切れは、このクライアントがクラスタへの接続を再確立できるまで (ないし再確立できない限り)、通知されません。このクライアントは、クラスタとの間で TCP 接続が再確立されるまで切断状態のままですが、接続が再確立されると、有効期限切れセッションのウォッチャーが "session expired" 通知を受け取ります。

有効期限切れセッションのウォッチャーから見た有効期限切れセッションの状態遷移例

  1. 'connected' : セッションが確立され、クライアントはクラスタと通信しています (クライアント/サーバー通信は正常に行われています)。

  2. .... クライアントがクラスタから切り離されました。

  3. 'disconnected' : クライアントはクラスタとの接続を失っています。

  4. .... 時間が経過していきます。「タイムアウト」期間の経過後、クラスタはセッションを有効期限切れにします。クラスタから切断されたままのクライアントには、何も見えません。

  5. .... 時間が経過し、クライアントはクラスタとの間でネットワークレベルの接続を回復します。

  6. 'expired' : 最終的にクライアントはクラスタに接続し、有効期限切れを通知されます。

ZooKeeper セッション確立呼び出しのもう 1 つパラメータに、デフォルトウォッチャーがあります。ウォッチャーは、クライアントに何らかの状態の変化が起きたときに、通知を受けます。たとえば、クライアントがサーバーへの接続を失うと、クライアントに通知が送られ、クライアントのセッションが有効期限切れになった場合なども、同様に通知が送られます。ウォッチャーでは、初期状態を「切断状態」(すなわち、クライアントライブラリから何らかの状態変更イベントがウォッチャーに送信される前の状態) とみなす必要があります。新しく接続する場合、ウォッチャーに送信される最初のイベントは、一般的にはセッション接続イベントです。

セッションは、クライアントから送信されるリクエストによって維持されます。セッションが一定期間アイドル状態で、この期間がセッションをタイムアウトさせる時間に相当する場合、クライアントはセッションを維持するために PING リクエストを送信します。この PING リクエストによって、ZooKeeper サーバーはクライアントが依然アクティブであることを知ることができますが、クライアントの側も、自分から ZooKeeper への接続が依然アクティブであることを知ることができます。PING のタイミングについては、デッドコネクションを検出して新しいサーバーに再接続するための時間を確保できるよう、十分に余裕を取ってあります。

サーバーへの接続の確立が成功 (connected) した後、クライアントライブラリが connectionloss を生成するケースは基本的に 2 つあります (connectinloss は C バインディングでのリザルトコードです。Java では例外になります。バインディング固有の情報については API ドキュメントを参照してください)。具体的には、同期操作または非同期操作が行われ、次のいずれかに該当する場合です。

  1. すでに維持されていない、または有効ではないセッションに対して、アプリケーションが操作を呼び出した。

  2. サーバーに対する操作のうち保留中のものがある、すなわち非同期呼び出しが保留中になっているときに、ZooKeeper クライアントがサーバーから切断された。

3.2.0 で追加 -- SessionMovedException。クライアントからは一般に見えない、SessionMovedException と呼ばれる内部例外があります。この例外は、以前と異なるサーバーで再確立されたセッションの接続上で、リクエストを受信した場合に発生します。通常、このエラーは、クライアントがサーバーにリクエストを送信したもののネットワークパケットが遅延し、結果としてクライアントがタイムアウトして新しいサーバーに接続したときに発生します。遅延したパケットが最初のサーバーに到着したとき、このサーバーはセッションが移動したことを検出し、クライアントからの接続をクローズします。一般に、クライアントからはこのエラーは見えません。なぜなら、古い接続に対する読み取り操作は行わないためです (古い接続は通常はクローズされます)。こうしたエラーが見える状況の 1 つとして、2 つのクライアントが、保存されたセッション id とパスワードを使って同一の接続を再確立しようとする場合があります。この場合、クライアントのいずれか一方が接続を再確立し、もう一方のクライアントは切断されます (このため、2 つのクライアントはいつまでも接続とセッションの再確立を試みることになります)。

ZooKeeper のウォッチ

ZooKeeper のすべての読み取り操作、すなわち getData()getChildren()、および exists() では、副次的機能としてウォッチを設定できます。ZooKeeper でのウォッチの定義は次のとおりです。ウォッチイベントとは、ワンタイムのトリガで、そのウォッチを設定したクライアントに送信され、そのウォッチが設定されたデータが変更されたときに発生します。このウォッチの定義には、3 つの重要なポイントがあります。順に説明していきましょう。

  • ワンタイムのトリガ

    ウォッチイベントは、データが変更されたときにクライアントに送信されます。たとえば、クライアントが 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 が作成されて削除されると、取りこぼされることになります。

ウォッチに関して 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 のオーナーという概念がありません。その代わりに、ZooKeeper の ACL では、id とその id に関連付けられたパーミッションとを組み合わせて指定します。

また、ACL は特定の znode だけに適用されます。具体的には、子には適用されません。たとえば、/app が ip:172.16.16.1 からのみ読み取り可能で、/app/status が誰からも読み取り可能である場合、/app/status は誰でも読み取ることができます。ACL は再帰的には適用されません。

ZooKeeper は、プラガブル認証スキームをサポートしています。id は、scheme:id という形式で指定します。ここで、scheme は、id に対応する認証スキームです。たとえば、ip:172.16.16.1 は、172.16.16.1 というアドレスのホストを表す 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: パーミッションを設定できます。

CREATE パーミッションと DELETE パーミッションは、より細かいアクセス制御を可能にするために、WRITE パーミッションとは別に用意されました。CREATEDELETE を使うのは次のような場合です。

A に対し、ZooKeeper ノードへのデータの設定は行えるようにし、子の CREATE または DELETE は行えないようにしたい場合。

DELETE なしの CREATE: クライアントは、親ディレクトリに ZooKeeper ノードを作成することによって、リクエストを作成します。すべてのクライアントが追加を行えるようにし、リクエストプロセッサだけが削除を行えるようにしたい場合 (ファイルに対する APPEND パーミッションのようなものです)。

なお、ADMIN パーミッションが用意されているのは、ZooKeeper にファイルオーナーの概念がないためです。ある意味では、ADMIN パーミッションは、その対象エンティティをオーナーに指定するものだといえます。ZooKeeper は LOOKUP パーミッション (ディレクトリの内容を表示できなくても LOOKUP を可能にする、ディレクトリに対する実行可能パーミッション) をサポートしていません。誰もが暗黙的に LOOKUP パーミッションを持っています。そのため、ノードを stat することはできますが、それ以上のことは何もできません。(問題は、存在しないノードに対して zoo_exists() を呼び出したい場合、チェックできるパーミッションがないことです。)

ビルトイン ACL スキーム

ZooKeeeper には、次のようなビルトインスキームがあります。

  • world は単一の id、anyone を持ちます。任意のユーザーを表します。

  • auth は id を使用しません。認証済みの任意のユーザーを表します。

  • digest は、username:password 文字列を使って MD5 ハッシュを生成し、これを ACL ID として使います。認証は、username:password を平文で送信することによって行なわれます。ACL で使われる場合の記法は、username:base64 エンコードされた SHA1 パスワード digest になります。

  • ip は、クライアントホスト IP を ACL ID として使います。ACL の記法は addr/bits になります。addr のうち bits 分の最上位ビットが、クライアントホスト IP の bits 分の最上位ビットと一致するかどうかが調べられます。

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 に完全に開放された ACL ID で、あらゆるアプリケーションがノードに対して任意の操作を実行でき、子の作成、表示、削除を行うことができます。ZOO_READ_ACL_UNSAFE は、あらゆるアプリケーションに読み取りアクセスを与えます。CREATE_ALL_ACL は、ノードの作成者にすべてのパーミッションを与えます。作成者が ACL を使ってノードを作成するには、その作成者がサーバーによって (たとえば “digest” スキームを使って) 認証済みでなければなりません。

ACL を処理する ZooKeeper 操作には、次のものがあります。

  • 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 を使って認証を行いたい場合、zoo_add_auth 関数を複数回呼び出すことができます。

  • 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 情報を返します。

  • int zoo_set_acl (zhandle_t *zh, const char *path, int version,const struct ACL_vector *acl);

この関数は、ノードの ACL リストを新しいリストで置き換えます。ノードには ADMIN パーミッションがセットされていなければなりません。

次に示すのは、上の API の使用例です。“foo” スキームを使って自身を認証し、エフェメラルノード “/xyz” を create-only パーミッションで作成します。

この使用例は、ZooKeeper ACL の操作方法を示すことを目的とした非常に簡単な例です。適切な C クライアントの実装例については、.../trunk/src/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 はさまざまな環境でさまざなスキームとともに実行されるため、完全にプラガブルな認証フレームワークを持っています。ZooKeeper のビルトイン認証スキームも、プラガブル認証フレームワークを使っています。

認証フレームワークの動作を理解するには、まず 2 つの主な認証操作について理解する必要があります。認証フレームワークはまずクライアントを認証しなければなりません。通常、クライアントの認証は、クライアントがサーバーに接続するとすぐに行われ、クライアントから送信された情報またはクライアントについて収集された情報の有効性を検証して、これを接続に関連付ける処理が行われます。認証フレームワークが次に行う操作は、ACL の中からクライアントに対応するエントリを探すことです。ACL エントリは、<idspec, permissions> というペア形式です。idspec は、接続に関連付けられた認証情報との単純な文字列一致であることもあれば、認証情報に照らして評価する式であることもあります。一致操作は認証プラグイン側で実装します。認証プラグインが実装しなければならないインタフェースを次に示します。

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 メソッドは、プラグインを識別する文字列を返します。ZooKeeper では複数の認証方式をサポートしているので、認証クレデンシャル、すなわち idspec には常にプリフィックス scheme: が付きます。ZooKeeper サーバーは、認証プラグインから返されるスキームを使って、スキームが適用されるのはどの id かを決定します。

handleAuthentication は、接続に関連付ける認証情報をクライアントが送信したときに呼び出されます。クライアントは、認証情報に対応するスキームを指定します。ZooKeeper サーバーは、認証プラグインに認証情報を渡します。認証情報を渡す先のプラグインは、そのプラグインの getScheme が、クライアントから渡されたスキームと一致するプラグインです。通常、handleAuthentication の実装は、認証情報が不適切であると判断した場合はエラーを返し、それ以外の場合には cnxn.getAuthInfo().add(new Id(getScheme(), data)) を使って、認証情報を接続に関連付けます。

認証プラグインは、ACL の設定と使用の両方に関与します。ACL が znode に設定される場合、ZooKeeper サーバーは ACL エントリの id 部分を isValid(String id) メソッドに渡します。id が正しい形式であることの確認は、プラグイン側で行います。たとえば、ip:172.16.0.0/16 は有効な id ですが、ip:host.com は無効です。新しい ACL が "auth" エントリを含んでいる場合には、接続に関連付けられたそのスキームの認証情報を ACL に追加すべきかどうかを判断するために、isAuthenticated が使われます。"auth" に含めるべきでないスキームもあります。たとえば、クライアントの IP アドレスは、"auth" が指定されている場合には、ACL に追加すべき id とはみなされません。

ZooKeeper は、ACL をチェックするときに matches(String id, String aclExpr) を呼び出します。ZooKeeper は、クライアントの認証情報が、該当 ACL エントリに一致するかどうかを調べる必要があります。クライアントに適用されるエントリを見つけるために、ZooKeeper サーバーは各エントリのスキームを探し、該当するクライアントの認証情報がそのスキームに存在するかどうかを調べます。このとき、handleAuthentication によって以前に接続に追加された認証情報を id にセットし、ACL エントリの id を aclExpr にセットして、matches(String id, String aclExpr) が呼び出されます。認証プラグインは独自のロジックと一致方式を用いて、idaclExpr に含まれているかどうかを決定します。

ビルトイン認証プラグインには、ipdigest の 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 のように重複がある場合、2 つのうち 1 つだけが使われます。また、すべてのサーバーが同じ定義済みのプラグインを持っていなければなりません。そうでない場合、プラグインで提供される認証スキームを使用するクライアントは、一部のサーバーに接続するときに問題を抱えることになります。

一貫性の保証

ZooKeeper は、パフォーマンスの高いスケーラブルなサービスです。読み取りと書き込みの両方の操作が高速に行えるように設計されていますが、読み取りの方が書き込みより高速です。その理由は、読み取りの場合、ZooKeeper は古いデータを提示できるためです。これは、ZooKeeper が次のような一貫性の保証を提供していることによるものです。

順序一貫性

クライアントによる複数の更新は、これらの更新が送信された順に適用されます。

アトミック性

更新は成功するか、失敗するかのいずれかです。その中間の結果になることはありません。

シングルシステムイメージ

どのサーバーに接続しているかに関係なく、クライアントから見えるサービスのイメージは同じです。

信頼性

いったん更新が適用されたら、その時点から次にクライアントがその更新を上書きするまで、更新内容は存在し続けます。この保証によってもたらされる帰結は、次の 2 つです。

  1. クライアントが、成功を示す戻り値を取得した場合、その更新は適用された状態となります。通信エラー、タイムアウトなど、障害によっては、更新が適用されたかどうか、クライアントにはわからない場合があります。ZooKeeper では障害を最小限に抑えるためにさまざまな手段を講じていますが、唯一保証されるのは、成功を示す戻り値だけです。(これは Paxos では単調性条件と呼ばれます。)

  2. 読み取りリクエストまたは成功した更新を通じてクライアントから見えるあらゆる更新内容は、サーバーが障害から復旧しても、決してロールバックされることはありません。

適時性

クライアントから見えるシステムは、一定期限において最新のものであることが保証されます。(数十秒のオーダーです。) システムの変更はこの期限内にクライアントから見えるようになるか、さもなければクライアントがサービス停止を検出するかのいずれかです。

上に示した一貫性の保証を利用すれば、リーダー選挙、バリア、キュー、取消可能な読み取り/書き込みロックといった高度な機能をクライアント側だけで簡単に構築できます。詳細については、「ZooKeeper のレシピとソリューション」を参照してください。

開発者は時として、ZooKeeper が実際には保証していないもう一つのことがらを保証されたものと考えてしまうことがありますが、これはまちがいです。そのことがらとは、次のようなものです。

クライアント間でのイメージの同時一貫性 (Simultaneously Consistent Cross-Client Views)

ZooKeeper は、時間軸上の任意の 1 点において、2 つの異なるクライアントが ZooKeeper データの同一のイメージを持つことを保証しているわけではありません。ネットワーク遅延などの要因により、あるクライアントが変更の通知を受け取る前に、ほかのクライアントが更新を実行することがあります。たとえば、2 つのクライアント A と B がある場合のシナリオを考えてみましょう。クライアント A が znode /a の値を 0 から 1 に設定し、その後、クライアント B に /a を読み取るように要求した場合、クライアント B が読み取る値は、クライアント B の接続先のサーバーによっては、古い値の 0 である場合があります。クライアント A とクライアント B が読み取る値が同じであることが重要な場合には、クライアント B は読み取りを実行する前に、ZooKeeper API メソッドから sync() を呼び出す必要があります。

上に述べたように、ZooKeeper それ自体は、変更がすべてのサーバーにまたがって同時に行われることを保証していません。しかし、ZooKeeper プリミティブを使えば、クライアントの同期化を可能にするような高度な機能を作成することができます。(詳細については、「ZooKeeper のレシピとソリューション」を参照してください。[準備中:..])。

バインディング

ZooKeeper クライアントライブラリは、Java と C の 2 つの言語のものが用意されています。以下では、これらの言語バインディングについて説明します。

Java バインディング

ZooKeeper Java バインディングを構成するパッケージには、org.apache.zookeeperorg.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 スレッドの処理または同期呼び出しの処理をブロックしません。

  • 同期呼び出しは、正しい順序で戻らないことがあります。たとえば、クライアントが次のような処理をしたとします。ノード /a に対し、watch を true に設定して、非同期呼読み取りを行い、読み取りの完了コールバックの中で、/a の同期読み取りを行います。(あまりよい処理の仕方ではないかもしれませんが、不正な処理というわけではなく、わかりやすいので例として使います。)

    この例で注目すべき点は、非同期読み取りから同期読み取りまでの間に /a に対する変更があった場合、クライアントライブラリは、同期読み取りのレスポンスを受け取る前に、/a が変更されたことを示すウォッチイベントを受け取りますが、完了コールバックがイベントキューをブロックしているので、ウォッチイベントが処理される前に、同期読み取りによって /a の新しい値が返されるという点です。

なお、シャットダウンに関連する規則には、特に面倒な点はありません。ZooKeeper オブジェクトがクローズされるか、または致命的なイベント (SESSION_EXPIRED および AUTH_FAILED) を受け取ると、ZooKeeper オブジェクトは無効になります。クローズ時には 2 つのスレッドもシャットダウンし、ZooKeeper ハンドルに対するそれ以降のアクセスの結果は未定義となるので、避けなければなりません。

C バインディング

C バインディングには、シングルスレッドとマルチスレッドのライブラリがあります。このうち、より使いやすいのはマルチスレッドライブラリの方で、Java API にもより近くなっています。マルチスレッドライブラリは、IO スレッドとイベントディスパッチスレッドを作成して、接続の維持とコールバックを処理します。シングルスレッドライブラリでは、マルチスレッドライブラリで使われるイベントループを公開することで、ZooKeeper をイベント駆動アプリケーションで使うことができます。

パッケージには、zookeeper_st と zookeeper_mt の 2 つの共有ライブラリが含まれています。前者は、アプリケーションのイベントループに組み込むための非同期 API とコールバックだけを提供します。このライブラリの唯一の存在理由は、pthread ライブラリが利用できないか、不安定なプラットフォーム (すなわち、FreeBSD 4.x) をサポートすることにあります。それ以外のすべての場合、アプリケーション開発者は、同期 API と非同期 API の両方をサポートしている zookeeper_mt をリンクするべきです。

インストール

Apache リポジトリからチェックアウトしたものを使ってクライアントをビルドする場合は、次の手順に従ってください。Apache からダウンロードしたプロジェクトソースパッケージからビルドする場合は、手順 3. に進んでください。

  1. ZooKeeper トップレベルディレクトリ (.../trunk) で ant compile_jute を実行します。これで、.../trunk/src/c の下に "generated" という名前のディレクトリが作成されます。

  2. ディレクトリ .../trunk/src/c に移動し、autoreconf -if を実行して、autoconfautomake、および libtool を使用できるようにします。autoconf バージョン 2.59 またそれ以降のものがインストールされていることを確認してください。手順 4. に進みます。

  3. プロジェクトソースパッケージからビルドする場合は、ソースの tarball を解凍し、zookeeper-x.x.x/src/c ディレクトリに移動します。

  4. ./configure <オプション> を実行して makefile を作成します。以下に示すのは、このステップで知っておくと便利な configure ユーティリティのオプションの一部です。

    • --enable-debug

      コンパイラオプションで最適化とデバッグ情報を有効にします。(デフォルトでは無効です。)

    • --without-syncapi

      同期 API サポートを無効にします。zookeeper_mt ライブラリはビルドされません。(デフォルトでは同期 API サポートは有効です。)

    • --disable-static

      静的ライブラリをビルドしません。(デフォルトではビルドします。)

    • --disable-shared

      共有ライブラリをビルドしません。(デフォルトではビルドします。)

    configure の実行に関する一般的な情報については、INSTALL を参照してください。

  5. make または make install を実行し、ライブラリをビルドしてインストールします。

  6. ZooKeeper API の doxygen ドキュメントを生成するには、make doxygen-doc を実行します。ドキュメントはすべて、docs という名前の新しいサブディレトリに置かれます。デフォルトでは、このコマンドは HTML だけを生成します。その他のドキュメント形式については、./configure --help を実行してください。

C クライアントの使用

クライアントをテストするには、ZooKeeper サーバーを実行し (実行方法については、プロジェクトの wiki のページにある手順を参照してください)、インストール手順の中で作成された cli アプリケーションのいずれかを使用してサーバーに接続します。以下の例では、cli_mt (zookeeper_mt ライブラリとリンクしたマルチスレッドクライアント) を使いますが、cli_st (zookeeper_st ライブラリとリンクしたシングルスレッドクライアント) を使うこともできます。

$ cli_mt zookeeper_host:9876

クライアントアプリケーションを使うと、簡単な ZooKeeper コマンドを実行できるシェルが起動されます。アプリケーションが正常に起動され、サーバーに接続すると、シェルプロンプトが表示されます。このシェルプロンプトでは、ZooKeeper コマンドを入力できます。たとえば、ノードを作成するには、次のようにします。

> create /my_new_node

ノードが作成されたことを確認します。

> ls /

ルートノード "/" の子のリストが表示されるはずです。

自分で作成するアプリケーションの中で ZooKeeper API を使用できるようにするには、次のことを行う必要があります。

  1. ZooKeeper ヘッダーをインクルードします: #include <zookeeper/zookeeper.h>

  2. マルチスレッドクライアントをビルドする場合は、マルチスレッドバージョンのライブラリを有効にする -DTHREADED というコンパイラフラグを指定してコンパイルし、zookeeper_mt ライブラリとリンクします。シングルスレッドクライアントをビルドする場合は、-DTHREADED を指定せずにコンパイルし、 zookeeper_st ライブラリとリンクします。

Java と C での使用例については、「プログラムの構造と簡単な例」を参照してください。[準備中]

ZooKeeper の基本操作

ここでは、開発者が ZooKeeper サーバーに対して実行できるすべての操作について説明します。主にコンセプトについて説明したこれまでの部分とは異なり、より実際的な側面に着目し、ZooKeeper API リファレンスほど細かい部分にこだわらずに、操作の概要について説明します。取り上げるトピックは次のとおりです。

エラー処理

Java と C のクライアントバインディングのいずれも、エラーを報告することがあります。Java クライアントバインディングでは KeeperException がスローされるので、例外に対して code() を呼び出せば、具体的なエラーコードが返されます。C クライアントバインディングでは、enum ZOO_ERRORS で定義されたエラーコードが返されます。API コールバックは、どちらの言語バインディングの場合もリザルトコードを返します。発生する可能性のあるエラーとその意味の詳細については、API ドキュメント (Java の場合は javadoc、C の場合は doxygen) を参照してください。

ZooKeeper への接続

読み取り操作

書き込み操作

ウォッチの処理

その他の ZooKeeper 操作

プログラムの構造と簡単な例

[準備中]

よくある問題とトラブルシューティング

以上で、ZooKeeper についてはおおよそ理解できたと思います。ZooKeeper は高速でシンプルです。作成したアプリケーションも動作します。しかし時として、アプリケーションが期待したとおりに動作しないこともあります。ここでは、ZooKeeper のユーザーが陥りやすい問題をいくつか取り上げます。

  1. ウォッチを使っている場合は、connected ウォッチイベントを監視しなければなりません。ZooKeeper クライアントがサーバーから切断されると、サーバーに再接続するまでは変更通知を受け取ることができません。znode の存在をウォッチしている場合、サーバーから切断されている間にその znode が作成されて削除されると、イベントを取りこぼすことになります。

  2. ZooKeeper サーバーで障害が発生した時を想定して、テストを行っておく必要があります。ZooKeeper サービスは、サーバーの過半数がアクティブである限り提供可能です。問題は、アプリケーション側で障害に対処できるかどうかです。現実の運用では、クライアントから ZooKeeper への接続が切断されることがあります (接続が失われる原因としてよくあるのは、ZooKeeper サーバーでの障害発生やネットワークの分断です)。ZooKeeper クライアントライブラリは接続を復元して、何が起こったかを知らせてくれますが、プログラマの側では、以前の状態を復元し、失敗した未処理のリクエストをやり直す必要があります。実働環境に移行する前に、テスト環境でこうした点がうまくいくかどうかチェックしてください。特に、複数台から構成される ZooKeeper サービスでテストを行い、これらのサーバーを再起動させてもアプリケーションが適切に動作するかどうかチェックしてください。

  3. クライアントが使用する ZooKeeper サーバーのリストは、各 ZooKeeper サーバーが保持している ZooKeeper サーバーのリストと一致していなければなりません。クライアントのリストが実際の ZooKeeper サーバーのリストのサブセットである場合には、必ずしも最適な状態でないにせよ、動作に問題は生じません。しかし、クライアントのリストに含まれている ZooKeeper サーバーが ZooKeeper クラスタに含まれていない場合には、動作に問題が生じます。

  4. トランザクションログの置き場所には注意してください。ZooKeeper の中でパフォーマンスに最も大きな影響を及ぼす部分はトランザクションログです。ZooKeeper は、レスポンスを返す前に、トランザクションを媒体に sync しなければなりません。一貫して良好なパフォーマンスを確保するためには、トランザクションログ専用のデバイスを用意することが重要です。ビジーなデバイスにトランザクションログを置くと、パフォーマンスが低下します。ストレージデバイスが 1 つしかない場合は、トレースファイルを NFS に置き、snapshotCount の値を増やしてください。これで問題が解消されるわけではありませんが、少なくとも問題の軽減にはつながります。

  5. Java の最大ヒープサイズを適切に設定してください。スワップを発生させないようにすることが非常に重要です。不必要にディスクにアクセスするようになると、ほぼ間違いなく、許容できないほどパフォーマンスが低下します。ZooKeeper ではすべてが順序どおり処理されることを忘れないでください。すなわち、あるリクエストでディスクアクセスが必要になると、その他のすべてのリクエストでもディスクアクセスが必要になります。

    スワップの発生を避けるには、マシンに搭載している物理メモリから OS とキャッシュに必要な量を引いたサイズをヒープサイズに設定します。サービスの構成に最適なヒープサイズを決定する最もよい方法は、負荷テストを実施することです。何らかの理由で負荷テストを実施できない場合には、予想を控え目に見積もって、マシンでスワップが発生する限界の値よりも大幅に低い値をヒープサイズに設定してください。たとえば、4GB のメモリを搭載したマシンでは、ヒープサイズは 3GB から調整するぐらいでちょうどよいでしょう。

その他の情報へのリンク

公式ドキュメントのほかにも、ZooKeeper 開発者が利用できるさまざまな情報があります。

ZooKeeper ホワイトペーパー [準備中: url の掲載]

Yahoo! Research による ZooKeeper の設計とパフォーマンスに関する決定版ガイド

API リファレンス [準備中: url の掲載]

ZooKeeper API の完全なリファレンス

Hadoop Summit 2008 での ZooKeeper Talk

Yahoo! Research の Benjamin Reed の動画による ZooKeeper の紹介

バリアとキューのチュートリアル
 [訳注:同じものが「バリアとキューのチュートリアル」にあります。]

Flavio Junqueira によるたいへん優れた Java チュートリアル。ZooKeeper を使って簡単なバリアと生産者−消費者キューを実装します。

ZooKeeper - 信頼性の高いスケーラブルな分散コーディネーションシステム

Todd Hoff 執筆の記事 (2008/07/15)

ZooKeeper レシピ

イベントハンドラ、キュー、ロック、2 相コミットなど、ZooKeeper を使ったさまざまな同期化ソリューションの実装を疑似コードレベルで説明

[準備中]

その他、有益な情報源があれば...