Droonga 1.1.0での「hot add」の仕組みの説明
連休中のこんな時間に書いてもしょうがないんだけど、吐き出しとかないと自分が先に進めないのでDroonga droonga.org/ja/ の事について書いときます。実装面の話です。
2015-05-03 03:50:56Droonga 1.1.0での各変更点がhot addの実現にどのように貢献しているか
今回のリリース droonga.org/news/2015/04/2… では上から下までアホみたいにたくさんの変更が入ってるんですが、これらは基本的には全て、「hot add」つまりデータの書き込み操作を止めずにノードを追加できるようにするための変更なのでした。
2015-05-03 03:53:18昨年末時点では「書き込みを止めないノード追加の実現まであとちょっと」と言ってたんですが、実際着手してみるといろいろ直さないといけない部分が出てきて、ほぼ1ヶ月ちょいも作業にかかってしまいました。(年始からしばらくは別件に張り付いてたのでそもそも作業が進行してなかった)
2015-05-03 03:56:42まず1つ目、ノード間のメッセージ配送に関わる経路の整理。前のバージョンではパケットの送出はForwarderというクラスの単一のインスタンスが一手に引き受けてたんですが、これを、送出先のノード1つにつき1インスタンス持つようにしました。
2015-05-03 04:01:52何故かというと、送出先のノードそれぞれに対して個別に「書き込みバッファ」を用意したかったからです。元々バッファの仕組みは入ってたんですが、それはインフラレベルの通信途絶などに際しての自動再送用で、生きている他のノードに対して任意に「今は送らないで」みたいな制御はできませんでした。
2015-05-03 04:07:44それで、元からあるバッファ「accidental buffer」の手前にもう一段、「intentional buffer」を設けました。
2015-05-03 04:11:37これにより、「このノードはまだデータの同期が終わってないから新規レコード追加のリクエストは送らず溜めておく」「同期が終わったから、その間止めて溜め込んでいた新規レコード追加のリクエストを放出する」というような柔軟な制御が可能になりました。
2015-05-03 04:12:44次に、roleという概念の導入。各ノードはservice-provider, absorb-source, absorb-destinationのいずれかのroleを持つようにしました。
2015-05-03 04:14:56s-pはいわゆる普通のノード。a-dはクラスタに追加しようとしている新しいノード、a-sはそのノードに対するデータのコピー元となるノードです。この内、s-pになっているノードだけが、これまでのノードのように外向けのサービスを提供します。
2015-05-03 04:18:30s-pが新規レコード追加のリクエストを受け付けると、他のs-pに対しては即座にリクエストを転送しますが、それ以外のroleのノードに対しては、書き込みバッファに書き出してそれで終わりとなります。
2015-05-03 04:21:10なぜそんな事をするかというと、新規に追加するノードにも、そのノードに対するデータのコピー元ノードにも、同期が完了するまでの間は変更を加えられると困るからです。
2015-05-03 04:24:27a-sからa-dへのデータの同期が完了したら、これらのノードのroleはservice-providerに戻ります。そうするとやっと、その間止めていたレコード追加のリクエストがバッファから吐き出されるようになります。
2015-05-03 04:29:02バッファが空になって、それらのリクエストが全て処理されたら、a-dとa-sはs-pと同等のデータを持つ状態になる。というわけ。
2015-05-03 04:32:01寝落ちしてた……夜中の続き、Droonga 1.1.0 droonga.org/news/2015/04/2… の実装面の変化の話。データの同期が完了した後、溜まってた書き込みバッファをそのまま愚直に処理すると、レコードに重複が生じてしまいます。それを防ぐための仕組みを整備しました。
2015-05-03 09:20:47何故レコードが重複するかというと、タイミングの問題で、新しく追加されるノードには「データの同期でa-sから取り込んだレコード」と「そのレコードの元になった、レコード追加のリクエスト」の両方が届いてしまうから。
2015-05-03 09:24:33ノードの追加は、「1、新規ノード追加」「2、a-sにあたるノードの戦線離脱」「3、a-sから新規ノードへのデータの同期」「4、a-sの戦線復帰」「5、新規ノードの戦線投入」の順で行われる。新規ノード向けの書き込みバッファは、1〜4の間使われる。
2015-05-03 09:30:44すると、1,2の間にバッファに溜まったリクエストの中には、既にa-sでレコードとして追加されデータの同期を通じて取り込み済み、というものが出てくる。主キーのあるテーブルへのレコードの追加なら、そういう重複したリクエストは単に「既存レコードの更新」扱いになるから問題ない。
2015-05-03 09:34:55しかしGroongaには主キーを持たないテーブルというのが存在し得る。そういうテーブルでは、レコード追加のリクエストが来たら来た分だけレコードが増える。その結果、このままでは新規に追加したノードだけレコード数が多くなるということになってしまう。
2015-05-03 09:37:15なので、a-sは自分が処理した最後のリクエストはいつ時点のものだったかというタイムスタンプをきちんと保持するようにして、既存の各ノードは、新規ノードに対してはその日時以降のリクエストのみを書き込みバッファから送出する(それより前のリクエストは捨てる)ようにしました。
2015-05-03 09:41:00このために、データベースへの書き込みを伴うメッセージにはマイクロ秒単位のdateの指定が強く推奨されるようになりました。droonga-clientもdroonga-http-serverも、メッセージには基本的にいつもマイクロ秒単位のdateを付けるようになりました。
2015-05-03 09:44:03droonga-http-server(express-droonga)はNode.jsで作られてますが、そのままだとミリ秒までの精度でしかtimestampを設定できないため、マイクロ秒単位の時刻を扱えるnpmのライブラリを使うようにしてます。
2015-05-03 09:46:30最後に、完全なGraceful Restartの実現。これまではGraceful Restartの処理が結構いい加減で、再起動の直前・最中に流入したメッセージについて、どこまでは処理できてどこからは処理できてないということを厳密に判断できない状態でした。
2015-05-03 09:49:50