2015年6月20日土曜日

ZooKeeper でリーダー選出を実装する

ZooKeeper による分散システム管理』 を買って読みました。

書籍では、ZooKeeper を使う典型的な例として 「複数のマシンの中から一台をマスターとして選ぶ」 というユースケース、いわゆる 「リーダー選出」を挙げ、3 章以降でコーディング例を示しています。しかし、コールバックやら Watcher やらの組み合わせ方がややこしいため、コードのフローは追いにくいものになっています。

8 章では 「ZooKeeper の高レベル API」 として Curator フレームワークを紹介し、Curator を使った場合にリーダー選出のコーディングがどうなるかを示しているものの、それでも分かりにくいです。というか、分かりやすいかどうかよりも、リーダー選出のために、素の ZooKeeper API とはだいぶ異なる Curator API を学習しなければならないという点で既に、「なんか違う。そうじゃない」 感があります (あくまで個人的に)。

「リーダー選出、もっとすっきり書けるはず」、という信念のもと、もんもんと設計を考え、最終的に LeaderElection という一つのクラスにリーダー選出アルゴリズムをまとめるに至りました。このクラスを使うと、リーダー選出のコーディングは次のように直感的で簡潔になります。

// ZooKeeper インスタンスを用意します。
ZooKeeper zooKeeper = ...;

// リスナーの実装を用意します。
LeaderElection.Listener listener = new LeaderElection.Leader() {
    @Override
    public void onWin(LeaderElection election) {
        System.out.println("私がリーダーです。");
    }

    @Override
    public void onLose(LeaderElection election) {
        System.out.println("他の誰かがリーダーです。");
    }

    @Override
    public void onVacant(LeaderElection election) {
        System.out.println("リーダーが辞めました。選出を再実行します。");
    }

    @Override
    public void onFinish(LeaderElection election) {
        System.out.println("コールバックチェーン終了。もう選出には参加しません。");
    }

    @Override
    public void onStateChanged(LeaderElection election, State oldState, State newState) {
        System.out.format("状態が %s から %s に変わりました。\n", oldState, newState);
    }
};

// リーダー選出を実行します。直感的で簡潔でしょ?
new LeaderElection()
    .setZooKeeper(zooKeeper)
    .setListener(listener)
    .start();

// 上記と同じですが、各変数を明示的に設定すると次のようになります。
new LeaderElection()
    .setZooKeeper(zooKeeper)
    .setListener(listener)
    .setPath("/leader")
    .setId(
        String.valueOf(Math.abs(new Random().nextLong()))
    )
    .setAclList(ZooDefs.Ids.OPEN_ACL_UNSAFE)
    .start();


LeaderElection の実装は、次のいずれかを検出するまでは、リーダー選出に参加し続けます ─すなわち、コールバック (および必要に応じて Watcher) をスケジュールし続けます。

  1. 与えられた ZooKeeper インスタンスの状態が AUTH_FAILED もしくは CLOSED である。
  2. LeaderElection.finish() メソッドにより 「終了すべき」 とマークされている。


LeaderElection.Adapter クラスは LeaderElection.Listener インターフェースの空実装です。 コールバックメソッドの幾つかにしか興味がない場合は、このクラスが便利かもしれません。 例えば、onStateChanged() にしか興味がない場合は、次のようにコードを短くできます。

// リーダー選出を実行します。
new LeaderElection()
    .setZooKeeper(zooKeeper)
    .setListener(new Adapter() {
        @Override
        public void onStateChanged(LeaderElection election, State oldState, State newState) {
            System.out.format("状態が %s から %s に変わりました。\n", oldState, newState);
        }
    })
    .start();

LeaderElection クラスは nv-zookeeper という Maven artifact に入れてあるので、pom.xml に次のように書けば、すぐに使えます。


nv-zookeeper は作成したばかりなのでテストも不十分ですが、よかったら試しに使ってみてください。不具合指摘、改善要望、プルリクエストは歓迎します。JavaDoc はこちらです。

2015年6月16日火曜日

ZooKeeper の起動に苦労した点

ZooKeeper クライアントを起動して、「Session 0x0 for server null, unexpected error, closing socket connection and attempting reconnect」というエラーが出たら、これは単純に ZooKeeper サーバーが起動していない可能性が高い。私の場合、ZooKeeper のソースコードを追っていって、ClientCnxnSocketNIO.java の 361 行目 (version 3.4.6) の sc.finishConnect() でエラーが起こっていることが分かったが、この sc というのは java.nio.channels.SocketChannel のインスタンスなので、もはや ZooKeeper の範疇ではない。そういうわけで、ZooKeeper の設定云々の話ではなくて、単なる通信エラー。

「zkServer.sh でサーバー起動しているはずなんだけど・・・」と思いながら、zookeeper.out というファイルの中身を見たら、「line 109: nohup: command not found」というエラーメッセージを発見。Windows の Git Bash で作業していたから、「nohup アップというコマンドがない」というエラーが起こってたということか。起動に失敗したのなら、エラー表示くらいしてくれよ > zkServer.sh

Windows では zkServer.sh ではなくて zkServer.cmd を使うべき、ということで、「zkServer.cmd conf\zoo.cfg」という形で起動したら、またもやエラー。

2015-06-16 19:43:30,086 [myid:] - WARN  [main:QuorumPeerMain@113]
  - Either no config or no quorum defined in config, run
ning  in standalone mode
2015-06-16 19:43:30,158 [myid:] - ERROR [main:ZooKeeperServerMain@54]
  - Invalid arguments, exiting abnormally
java.lang.NumberFormatException: For input string: "(略)\zookeeper-3.4.6\bin\..\conf\zoo.cfg"
  at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
  at java.lang.Integer.parseInt(Integer.java:580)
  at java.lang.Integer.parseInt(Integer.java:615)
  at org.apache.zookeeper.server.ServerConfig.parse(ServerConfig.java:60)
  at org.apache.zookeeper.server.ZooKeeperServerMain.initializeAndRun(ZooKeeperServerMain.java:83)
  at org.apache.zookeeper.server.ZooKeeperServerMain.main(ZooKeeperServerMain.java:52)
  at org.apache.zookeeper.server.quorum.QuorumPeerMain.initializeAndRun(QuorumPeerMain.java:116)
  at org.apache.zookeeper.server.quorum.QuorumPeerMain.main(QuorumPeerMain.java:78)
2015-06-16 19:43:30,163 [myid:] - INFO  [main:ZooKeeperServerMain@55]
   - Usage: ZooKeeperServerMain configfile | port datadir [ticktime] [maxcnxns]
Usage: ZooKeeperServerMain configfile | port datadir [ticktime] [maxcnxns]

このエラーメッセージがまた意味が分かりにくいため、結局 QuorumPeerMain.java, ZooKeeperServerMain.java, ServerConfig.java やらのソースコードを読むことになって、「汚いコードだな」とげんなり。

parseInt でエラーが発生している箇所 (ServerConfig.java 60 行目) は、コマンドライン引数が 1 つだけのときには通らない場所なんだけどなぁ、、、あっ、zkServer.cmd で引数追加してるんだな、これは。(くそっ、先に zkServer.cmd みておくべきだった)

zkServer.cmd の中身はこんな感じ (途中に改行追加)

setlocal
call "%~dp0zkEnv.cmd"

set ZOOMAIN=org.apache.zookeeper.server.quorum.QuorumPeerMain
echo on
java "-Dzookeeper.log.dir=%ZOO_LOG_DIR%" "-Dzookeeper.root.logger=%ZOO_LOG4J_PROP%" ^
  -cp "%CLASSPATH%" %ZOOMAIN% "%ZOOCFG%" %*

endlocal

%ZOOMAIN% のあとに、%ZOOCFG% が挿入されてるぞ・・・。

しかし、こういう書き方をされると、設定ファイル名をコマンドライン引数として簡単に渡せなくなる。別の設定ファイルを使おうとすると、ZOOCFG 変数をいじってから zkServer.cmd を起動しないといけない。

「引数の数の違いによって、同じ位置にある引数なのに解釈の仕方を変えてしまう」、というのは、下手な設計。ZooKeeper サーバーのコマンドライン引数処理はひどい。ついでにいうと、設定ファイルのパース処理 (QuorumPeerConfig.parseProperties) もひどすぎる。

昔読んだ HBase のソースコードの汚さに比べたら全然マシだけど、なんか Hadoop まわりは力技感が半端ない。コード品質の悪さを人数でカバーか・・・。

結局、引数無しで zkServer.cmd を実行。手元の実験ならこれでいい。ダウンロードしてきた zookeeper のソースツリー (zookeeper-3.4.6) 外で作業しようとしたばっかりに、無駄な苦労をしてしまった。