ブログトップ 記事一覧 ログイン 無料ブログ開設

kuenishi(7) RSSフィード

find me on twitter

2012/01/26

ひとりZooKeeperコードリーディング(1) クライアント編

さて準備ができたので読み始めることにする。この手のシステムは大体の機能が分かっているならAPIを見るのが手っ取り早い。もろもろの名前でなんとなく雰囲気と使い方が分かるので下手にドキュメントだけを読み込むよりもよい場合もある。Javadocだとクラス毎になってしまうし*1慣れているのもあってC APIというかヘッダにまずは目を通す。基本的には

  • znodeのCRUD
  • それのsync, async, w系のAPI
  • initialize / finalize
  • multiops (3.4+

などである。特徴的なのはZOO_SEQUENCEとかZOO_EPHEMERAL。Chubbyの論文にはEphemeral fileというのが出てくるから後者は分かりやすいけど、前者はZK特有。ACLなんかは気になるけどとりあえず本質的ではないので後回し。

ZooKeeper.java

というわけでExampleを見るとZooKeeperオブジェクトをnewしていろいろ取り回すようだ。ZooKeeper.javaの冒頭のコメントは

  • ZooKeeperのサービスを使うにはこのインスタンスのメソッドをいろいろ呼ぶ
  • 特に何も書かれてないのなら各メソッドはスレッドセーフ
  • サーバーとセッションがつながるとIDが割り当てられてハートビートする
  • セッションIDが有効な限りZooKeeper APIを呼べる
  • ハートビートを送れないとサーバーでセッションが無効になって、そのオブジェクトはもう使えなくなる
  • セッションを再接続するにはnewしてネ
  • セッションが切れる前に怪しくなったときに、クライアントは他のサーバーに再接続しようとする。成功すればそのセッションを使い続けることができる
  • APIは同期・非同期それぞれある。非同期APIはコールバックオブジェクトを仕掛けて結果を得る
  • サーバー上のznodeに"watch"とかいうものを仕掛けることができる
  • 全てのwatchはただ一度だけ(only once)実行されるが、サーバーとの接続が怪しくなって再接続が起きると消えることがある(そりゃそうだわな
  • undelivered eventsを察知するために、コネクションが切れたときのためのwatchを仕掛けることができる

といった感じ。ほー分かりやすい。ソースなんてわざわざ読まなくてもいいんじゃないか。※ここでのセッションはTCPのセッションとは異なる、ZooKeeperのセッションのことを指す。どうも英語でTCPのセッションを指すときはコネクションと言うらしい。紛らわしいので要注意。

ZooKeeperクラスのコンストラクタは2つある。シグネチャから察するにACLの認証してくれる版としてくれない版だ。認証が必要ないAPIをわざわざ残しているということは、ACLなしでのサーバーセットアップが可能か、もしくは特定の部分だけ認証なしにACLを設定できるかのどちらかだろう。大して変わらないけどここでは後者を追っていくことにする。で、何をやっているかというとClientCnxnをnewしてstart()している。

あ、その前にConnectStringParserというのをnewしている。ZooKeeperのホストは "192.168.0.1:2181,192.168.0.2:2181,192.168.1.1:2181" といった風に指定するのだが、どうも '/' を渡すとそこにChrootして接続してくれるみたいだ。なんというキモい機能。Cクライアントでもついてたら嬉しいな(要確認)。

次にnewされているStaticHostProviderはパーズして配列に入れてシャッフル。これで負荷分散をしているようだ。getClientCnxnSocket()で特に何も設定していなければClientCnxnSocketNIOというのを持ってくる。あれ、しかしこのクラスはnewInstance()というメソッドがないみたいだ。そもそもJavaでネットワークIOするときのお作法が分かってないのが問題か。依存ライブラリにnettyが入ってるからnetty使ってると思うのだが。

ああ、ちなみにコンストラクタの引数のsessionTimeoutの単位はミリ秒なので注意すること。注意すること…orz watcherにはセッションまわりで引っ掛けたいのをまとめたオブジェクトを渡す。

ClientCnxnはZooKeeperクラスが持ってるものをそのままもらってくる。メンバにsessionIdというのがあるのだけど初期値は0。長さはlong。

        connectTimeout = sessionTimeout / hostProvider.size();
        readTimeout = sessionTimeout * 2 / 3;
        readOnly = canBeReadOnly;

        sendThread = new SendThread(clientCnxnSocket);
        eventThread = new EventThread();

おお、これは…。おそらく、上から順にTCP接続のタイムアウト。台数で割ってるから、例えば5台構成で1000msにすると一台あたり200msしかないがいいのか。readTimeoutというのは、想像だが、read系のAPIのタイムアウト時間だろう。2/3とかどういう意味があるんだろう。で、おそらく送信用のスレッドとwatcherのイベント処理用のスレッドをひとつずつ立ち上げている。まそんなもんか。あれ、readのスレッドはなくていいのかな。SendThreadはClientCnxnの内部クラスでStatusをCONNECTINGに変更してる。

で、ClientCnxnをstartして終わっている。あれ、connect(2)は‥? というわけで次。

dm = new DataMonitor(zk, znode, null, this);

DataMonitor.javaをみると、WatcherとStatCallbackを継承している。コンストラクタの中で

        zk.exists(znode, true, this, null);

している。これはノードの存在を確認して、それとアトミックにwatcherを仕掛けるAPI。どうしてこうなっているかというと、existsとset_watcherの二段設定になっていると、existsとset_watcherの間にイベントがトリガーされたら届かないので困るのを防ぐことができる。

…うーんペースが遅いのでもうちょっと巻いていこう。

ZooKeeper#exists

直接呼んでるところからもうちょっと入ったところにある。ざっくり何をしているかというと

  • WatchRegistrationのインスタンスを作ってMap<String, Set<Watcher>>を持っておく
  • RequestHeaderというクラスを作ってOpCode.existsをセット
  • ExistsRequestのインスタンスを作って
    • serverPathを設定
    • setWatch(bool)
  • cnxnにリクエストをqueueする。リクエストにわたすのは
    • RequestHandler, new ReplyHeader, request, new SetDataResponse, callback, clientPath, serverPath, ctx, WatchRegistration
    • つまりリクエストヘッダ、リプライを受け取るオブジェクト、リクエスト本体、レスポンス、etc.
    • StatCallbackとWatcherの違いがよう分からんね。DataMonitorはどっちもimplementしているので分けているのはどうしてか…と思ったが簡単か。セッション状態が変わったら多分StatCallbackでWatcherはznodeの状態が変わったら。

と思って調べてみたらStatCallbackは

public void processResult(int rc, String path, Object ctx, Stat stat);

というメソッドが必要なinterfaceなのだが、この引数のStatが曲者でorg.apache.zookeeper.data.Statらしいのだがソースをgrepしてみてもそんなものはどこにもない。というかorg.apache.zookeeper.dataというパッケージがない。ぐむむ。Watcherの方はもういいよね。processというabstract methodを持っていて、イベントが起きると引数でそれがWatchedEventで渡されてくるというもの。

とりあえずここまで一瞬たりともsynchronizedした形跡がないのだがそれでいいのか。

ClientCnxn#queuePacket

さてやっと送信キューのところまできた。想像するにパケットをキューに積んでくれる。TCP上での送信自体は別の送信スレッドがいるか、もしくはロックを使って毎回送ってみるかのどちらかだろう。

おおsynchronizedがないと心配していたらやっと出てきた。outgoingQueueはLinkedList<Packet>という型を持っているので大体わかりますね。えっFIFOキューじゃなくてリストですか?という(コードを読み進めると分かるが、優先パケットは先頭に突っ込むとかやっているので、まあ妥当か)。

まず最初にPacketにxidというのを設定する。これはクライアント側でつけるRPCのIDだろう。インクリメントしていることから、サーバー側で欠送に使うのかもしれないが、実は単調増加なだけでもいいのかも。ちょっと先に進んでみないと分からない。

で、当然だけどPacketをnewしている。引数はRequestHeader, ReplyHeader, Record req, Record res, watchRegistrationの6つ。RecordというのはピンとこないがBodyだと思えばいいのか。単なるSerializableみたいなものだ(ちょっと違うけど)。で、引数で渡されたものを全てPacketに押し込んでいる。基本的にはセッションが生きていたらoutgoingQueueにaddする。それでsendThread.getClientCnxnSocket().wakeupCnxn()とかやっている。クラスローダーの壁に阻まれてEclipseでは追えなかったがそれで起こされて送信でもするのだろうか。近くにdoTransportとかいうメソッドがあるからそれで起きるのだろう。

ClientCnxnSocketNIO.java

ClientCnxnSocketを継承しているので、おそらくデフォルトではこのクラスが送信に使われるのだろう。wakeupCnxn()ではjava.nio.channels.Selector#wakeup()を呼んでいる。

ついでにdoTransportも見てみるがネットワークIO周りは未知だな。名前からしてselectorがselect(2)するのだろう。で、SelectionKeyというのがファイルディスクリプタみたいなものか?SelectionKey#selectしておいて、readableだかwritableだかしているところから#selectedKeys()で取り出してくる。

で、そこから取り出せた全てのSelectionKeyについてfor文をまわして*2、それぞれでConnectedになっていたらSocketChannelとかいうのでreadyだったらなんかいろいろゴニョゴニョする。READABLEかWRITABLEになっていたらdoIOメソッドに飛ぶ。

doIOは

  • sendThread.readResponse(incomingBuffer);
  • synchronized (outgoingQueue) {
    • ByteBuffer pbb = outgoingQueue.getFirst().bb; sock.write(pbb);

辺りがキモ。特にreadResponseは受信まわりなのだがちょっと力尽きた。想像するにreadして何かあったらイベントのコールバック呼ぶとか、待ってるスレッドに返事送るとかそんなん(TODO)。

ちょっとまだこの辺は自分にはレベル高い。というかもう眠い。疲れた。

SendThread#run

そういえばThreadなんだからそのスレッドが何してるのか見ればいいんだった。ざっくり流れを。whileで

  • startConnect()
    • 初回接続じゃなかったら1~1000msくらいをランダムに選んでSleep
    • 並べてあるIPアドレスから選ぶ(SASLでの接続もできるらしい
    • 全員に聞いてしまってそれでもいなかったら1000ms待つ(hostProvider.next(1000)っぽい
    • ClientCnxnSocket#connectでふつうに接続
  • connectなり初回readがタイムアウトしていたらSessionTimeoutExceptionを投げる
  • pingを送る
                   if (state.isConnected()) {
                        int timeToNextPing = readTimeout / 2
                                - clientCnxnSocket.getIdleSend();
                        if (timeToNextPing <= 0) {
                            sendPing();
                            clientCnxnSocket.updateLastSend();
                            clientCnxnSocket.enableWrite();
                        } else {
                            if (timeToNextPing < to) {
                                to = timeToNextPing;
                            }
                        }
                    }
    • ここは大事なのでメモ。timeToNextPingは、最後に何か送ってからの経過時間がreadTimeoutの半分を超えていたらPingを送る。そうじゃなかったらまあtoを延長。toってなんだ…と思ったら、最後に何か受け取ってから経過した時間だ。ここはシーケンス図を書いた方がよいだろうな。
  • // If we are in read-only mode, seek for read/write server
    • おそらくフェイルオーバーか何かの異常が起きたときかな?
    • if (state == States.CONNECTEDREADONLY) {
    • pingRwServer()して、"rw"なサーバーに接続できたら今持ってるのをcloseしてそっちに乗り換える、と書いてあるように読める
  • clientCnxnSocket.doTransport(...);
  • と、ここまでが壮大なtry clauseの中
  • whileループを何かのきっかけで抜けたらEvent.KeeperState.DisconnectedをeventThreadにenqueueしたりするなど。

まとめ

クライアントのexistsの流れをざっくり追った。宿題というか未解決なのは

  • ClientCnxnSocketの実装は結局何なのよ?
  • シーケンス図を書いてタイムアウト設計を考える
  • フェイルオーバーするときの流れも
  • ClientCnxnSocketNIO#doIOの中はもうちょっと追うか
  • org.apache.zookeeper.data.Statが見つからない
  • どこでSessionIDもらってるの?!
  • doIOの中身
  • プロトコルは把握しておきたい

*1:というかそもそも公式から辿れるリンクが404とかいう

*2:mapになっていない。もっというとliftMapになってない。これだからJavaは…

2012/01/24

ひとりZooKeeperコードリーディング(0) 準備

特に頑張ろうとかではないのですが、ZooKeeperのソースを読み始めようかなと。ソースを落としてこようとしてEclipseを久しぶりに起動したらEGitだかのインストールでEclipseがだんまりを決め込んだのでうろうろしていたらConfluenceにそれっぽいドキュメントをみつけた。基本的には

  • 頑張らない
  • 僕が知っている暗黙の知識は特に説明しない
  • 不定期
  • Javaは素人です

くらいの適当な感じで。

ドキュメントは公式サイトの記述が割といい感じになっているがInternalsのページもちょっと物足りない。Confluenceのこの辺りのドキュメントは開発の初期に使い始めてメンテナンスに挫折した形跡があるので、思考や歴史を追うのにいいかもしれない。MLは一応購読していて、気になるSubjectが来たら見るくらいにしているが、新機能や新しい情報をウォッチしたい場合はMLが一番よいと思う。もっと細かく開発の流れを追いたい場合はJIRAをウォッチするのがよい。Apacheプロジェクトでは、基本的に全てのタスクがJIRAで管理されている。新機能もバグもロゴも…まあ著名な情報源といったらその辺りだろうか。事例を知りたい場合は…疲れたので以下略。

Zabの論文は以前読んでいたので基本的な知識は持っているつもりだがObserverMultiops辺りから詳細がもはや分からなくなってしまった。あと噂によるとOSGi使ってノンストップアップグレードができたりするらしい[要出典]。

あとはZooKeeperを使ったモノとしては最近だとNetflixのCuratorとかTwitterのSnowflakeとか。

そういういってるうちにEclipseのgitプラグインの準備ができたので適当にEclipse ProjectとしてImportする。EclipseのGUIは割と苦手だけどJava使いには必須スキルだと思うので我慢して使う。ivyも使えるらしいよ。mavenは分かりやすくて好きなんだけどなあ。やっぱりちょっと苦戦だけどこのページでなんか分かった気がする。

基本的には

  1. 適当なディレクトリ(/tmp辺りかな?)にgit cloneで持ってくる。import...でやっても持ってこれる。
  2. New Project from File Systemでふつうにファイルを取り込む。
  3. Team...からShare Projectで適当にGitレポジトリだとEclipseに教えてあげる
  4. build.xmlを右クリックしてbuild as an Ant project...とかってやる。

とやると、ivyで依存ライブラリを解決してくれる。…と思ったら、もろもろ解決してくれる前にEclipseでデフォルトになってるjarタスクを先に実行してしまうのでエラー。jarだけ外してもういっかいbuild...と思ったらもう夜も遅いのでHowToContributeにある通り

zookeeper/ $ ant -Djavac.args="-Xlint -Xmaxwarns 1000" clean test tar

を実行してみたらあっさりテストまでスムーズに進んでいる。warningを1000以下に抑えるとか、いいのだろうか。ブラウザなどのアプリケーションはいいのだが、開発環境がGUIである必要はあるのだろうか。人によってはIntelliJとかNetBeansがよいみたいだが。いやあしかしテスト長い。junitでずーっとシリアルに流しているようだ。そりゃあTCP挟んだテストが沢山あるだろうから簡単には並列化できないだろうけど。JUnitの制約かなあと思っていたらJUnit4からは並列クラスとかいうのになっているらしい。ivyがとってきたのはJUnit4.9となっているから後方互換性を気にしているのかな。どっちだろうというのはテストの中身を見ないとちょっとわかりませんね。

と思ったらcppunitがないとかでBUILD FAILED。入れなおす。install-shとかがないって言われる。どうせCクライアントのところだからスキップでいい。そんなこんなでとりあえずJavaのところは大丈夫そう。だけどEclipseは相変わらず赤いバッテンだらけでソース読むだけなら影響ないのだけどこの先が思いやられる。

追伸

そういえば規模を調べるの忘れていた。

$ find . -name "*.java" | grep -v test | xargs wc

59233 208484 2040114 total

$ find . -name "*.java" | grep test | xargs wc

22863 76632 839826 total

ざっくりいって本体60KLOC, テストコード30KLOC…でかい… と思ったが

$ cd src/java/main

$ find . -name "*.java" | xargs wc

37380 133202 1336852 total

やった!ちょっと減ったぞ!まあ…それでも…でかい…

2012/01/23

ガンダムUC

角川スニーカー文庫の小説を読み始めてしまったのでここいらでひとつまとめを。

とりあえず2巻まで読み終わった。ガンダムシリーズというと30代〜40代のオッサンが「坊やだからさ」とか言ってるのが常なのだが、かくいう私もふとしたことから会社の先輩にDVDの1,2巻を貸してもらって観たのがきっかけだ。それまではたまにガンダムのようなものは見ていた。子供の頃はVとかGとかやっていたし、ちょっと後にSEEDなんかが出てきてオッサンだけじゃなく若い女性たちにも人気が出始めていてで、テレ東をつけるとたまに見かけたものである。

機動戦士ガンダムUC(ユニコーン) 1 [DVD]

機動戦士ガンダムUC(ユニコーン) 1 [DVD]

続きを読む

2012/01/01

2012年、而立

あけましておめでとうございます。新年エントリももう6回目になります(2007, 2008, 2009, 2010, 2011)。予定通りにアナログ放送は終了せず再送信されていることに旧年中の怒りを抑えきれていません。オンラインでお付き合いのある皆様におかれましては、大変お世話になりました。今年はもっとお世話をかけることになると予想されますので、今後ともよろしくおねがいいたします。さて昨年は特に目標を立てるということもなくダラダラとしていたように見えますしオンラインでの活動も特に減った一年でございました。Twitter投稿数なんかは大きく落ちたのではないでしょうか。アーカイブをみても月に2〜3本もブログを書けてませんね。

昨年は、なんといっても東日本大震災に驚きました。私にとって震災といえば阪神・淡路大震災を指す言葉でしたが、いずれの震災も中途半端に近くで震度4〜5を体験した者としてはどう受け取ってよいか困ります。神戸のときは何十年かぶりの都市型震災ということもあってキー局や関東の主要メディアも注目しており、ああでもないこうでもないという言論が続いたのを覚えています。それをきっかけに建物の耐震基準を10年スパンで全国的に見直してきたわけですが、それが2011年には津波で全部流されてしまった。というと言い過ぎですが、津波が届かない範囲で震度6〜7だった地域で、新しい耐震基準が生命を助けた場面は多かったのではないかと推測しています。また自衛隊の出動が遅れたことが話題になりましたが、今回は被害規模が大きくマクロ的に初動には問題なかったのではないでしょうか。我々は確実に進歩しています。と思ったところで津波と原子力発電所ですからね。しかし日常生活をしていて思いますが、どんなに理論立っていようと危機やリスクの告発は狼少年になります。これは本当のことだと思います。「これが危ない」とどれだけ主張・説得しても余程の調査とシミュレーションがないと受け入れてもらえません。正常性バイアスは本当に恐ろしく、原発事故の根本的な原因はこれだったのではないかと思えるほどです。しかし災害に対する備えも資本主義というか弱肉強食の進化メカニズムの中ではやはり優先度が下がってしまいます。こういった大きな車輪に私たちのような小さな個人が対抗していくためには団結が必要なのかなと思ってしまいます。とはいえ昭和的・家族主義的な団結の崩壊が始まって久しく、コミュニティの崩壊とか世代の崩壊とか多様化の時代とも言われており、何らかの説得力ある形での新しい団結の形が見えた希望の一年でもあるかなと思っています。肝心の私自身はとんでもない人見知りですがね。

よく考えたら2011年は日本における関数型言語の年といってもよいくらいの年ですね。今振り返ってみて思い出した。何といっても関数型プログラミングのトップカンファレンスICFPが日本で開催されたのですから。それに伴い函数型プログラミングの集いが開催され、Erlang updateのスピーカーとして呼ばれたのは自分にとっては大きな一歩だったなあと思っています。DSLはともかく、今後、関数型言語は必ず汎用プログラミング言語の中で主流を占めていきます。これからも、関数型言語の普及に尽力したいと思っています(普及させるためには、それでお金を稼げることを示すのが一番の近道なんですがね)。

Python温泉も今年で一区切りとなりました。私にとって大きなイベントだったのでなんとも惜しいですが、みんな歳とったしねー。

さて、私自身は一年なにをしていたかというと、ひとつは会社で某商用システムの某開発をしておりました。開発も4年目にしてようやく商用サービスが開始され評判はさておいてお金を稼ぎ始めたことに灌漑一潮でございます。目から塩水が…別に出るわきゃないのですが今のところ大きな問題はなくシステムも停止しておらず、稼働しながらいくつか未解決のバグに光明が見えたりもしてホッとしています。今年でその開発も5年目に入りマンネリ化する恐れが大きいですが、こちらも何か新しい流れがこないかなと期待しております。というのと並行してJubatusというのを作りました。何か著名なOSSのコミッターになるのを以前から目標にしていたのですが、まさかこんな形で実現に近づくとは…!OSSのコミッターではありますが著名にはまだまだ程遠いので昨年以上の試行錯誤をしていくことになるのだろうと思います。以前から独学でC++をある程度書けるようにはなっているつもりでしたが、Jubatusの開発を通じてトップクラスのエンジニアや変態さんとお付き合いでき自分のC++力が随分上がったなと体感しています。開発の途中で何度かダークサイドに堕ちかけ闇の軍団の三軍に加担しそうになっていましたがなんとか踏み止まりました。

あとはMessagePackのErlangを本格的に作ったりしていました。いちおRPCも実用的なところまでこれたかなぁと思っていたところに、Jubatusが来たので実用性を頭の中で評価していたのですがJubatusの用途が特殊すぎてあまり評価にならなかったです。一方でMessagePackのIDLがHaskellで書かれたりしてそのコードの短さに驚いています。

プライベートなことに関しては、息子は1歳になり私は30歳になりました。親父と私の歳の差がちょうど30で計算しやすかったのですが、私と息子の歳の差は29と偶然にも肉を表す数字になっていて神の手を感じます。はい適当なこと書きました。今のところ彼にとって私は朝は寝てて週末はなぜかいる電車動画を見せてくれるオッサン、というくらいなのですが一緒に風呂に入っても泣かれない程度にはなれたかな。

自分の言語履歴はこんな風になりました。*1

2004C, Java, C++
2005Java, TeX, PHP
2006C++, TeX, PHP, Ruby
2007Python, C++
2008Erlang, C
2009Erlang, C, Python, Ruby
2010Erlang, OCaml, Python, C/C++
2011C++, Erlang, Python, OCaml

今後の予定

2012プログラミング言語がどうこうとかめんどくさいこと言わなくなる
2013自分言語開発(たぶんやらない)
2030長男成人

おお。めんどくさいこと言わなくなるのはいい。2012年の目標は...特にないな。やりたいことは沢山あってClojureとかJSとかOCamlとか。ボーナス上げたいです。あと友達増やしたいな。

*1:読んだ量および書いた量をなんとなく平均的にスコアリングした感じに基づいて左から順に並んでいる。

2011/12/21

カーネル、libc、gccのビルドで気持ちを軽くする

MySQLのビルドが30秒を切るという話を聞いて一度ためしてみたものの、UbuntuでやるとカーネルやGCCのアップデートがある度に手順が最初からやり直しになってめんどくさかったんだけど、さすがにDebian開発者の人だってこんな面倒な手順踏んでないで何か自動化してるだろ、というのを効果を調べながらやってみたので結果報告です。

$ git clone git://github.com/jubatus/jubatus.git

$ ./waf configure

$ time ./waf

Waf: Leaving directory `/tmp/jubatus/build'

'build' finished successfully (2m26.716s)

real 2m27.113s

user 3m12.364s

sys 0m53.899s

うん、これは遅い。C++ってのもあるけど、もうちょっと速くしたい。WAFなのでコア数を増やせばその分簡単に速くなるんだけども、そこはホラ、ノートPCで仮想マシンなのでちょっと勘弁。イカ、順を追ってやってみたでゲソ。

続きを読む