前提: STFのワーカーのプロセス数

STFを受け継いだ時点ですでにワーカープロセスの形は決まっていた。いくつかのサーバーにワーカーの「親」プロセスがいて、そいつが必要に応じて本当の処理をするための子プロセスをforkしていく。どの種別のワーカーがどれだけforkされているか、というのはParallel::Scoreboardに記録されてた。それぞれの種別のワーカーがどれだけforkされるべきか、というのは設定ファイル等に書いておく。設定ファイルは(面倒なので)全てのワーカーで共通。

共通設定ファイルにワーカー数が書いてあるということは、例えばワーカー(親)を増やしたりすると、その分だけ単純にワーカーが増える。例えば Replicateというワーカーが4プロセスforkされるように書いてある設定ファイルで、ワーカーを3から4に増やすと、システム全体のReplicateワーカー数が単純に12から16に増える。

だいたいの場合これでも問題ないけれども、そもそも直線的にワーカー数を増やした場合に皮肉な事に裏方のストレージサーバーが耐えられない、ということもありえる。ということは常に総ワーカー数を逆算しながら設定ファイルを書いて例えば「最大30個くらいのワーカーまで耐えられて、で今5サーバー x 4プロセスで20個で、これから4個追加するから1サーバーあたりのプロセス数は・・・」とか考えなくてはならない。

別に親ワーカーの数が10000とかおかしな事にならなければできない事じゃないけど、まぁそれでも計算を間違える可能性もあるし、なにより人間がその計算方法をちゃんと意識しなければいけないのがいけてない。総数から勝手に調節してほしい!

・・・というのがそもそもの発端。



MySQLにたどり着くまでの試行錯誤

これは要はおのおののワーカーで勝手にプロセス数を計算して、全体に周知して、お互いバランスを取る・・・って方法を取らないといけないわけですね。ということはリーダーエレクションしてなんかするといいんだなー?っていう発想からぐぐってたらzookeeperさんが出てきた。

その時点ではよくzookeeperがわかってなかったけど、プロセス間の協調をするためのミドルウェア的な事書いてあるし、これか!カッ!という感じでちょっと調べてみたところ、どうもPerlバインディングが最新版のzookeeperでうまく動かない。謎のsegvで死ぬ。作者にコンタクトを取ったところ、最新版は使ってないから動かないかもねーというお返事。

さらにそのエラーが出ている間にドキュメントやコードを読んでいると、どうもPerlバインディングで簡単なブロッキングな処理をしているつもりでも裏でスレッドが色々たってたり、なにやら心証として俺が求めているより難しい処理をしている予感がふつふつとしてきた。さらに追い打ちとしてtwitterで「zookeeperわーい☆」的な発言を俺がしたあとで隣の席の某kazebur○さんから「zookeeperか・・・」という発言が来てぞっとしたので若干やる気がなくなった。

この時点ですでにzookeeperによる簡単なリーダーエレクションのコードは書いてたんだけど、よくよく考えればこれは普通にSQLでできるじゃん、という事にようやく気づいたので、これはMySQLでやってみたほうがよくねぇか?と。zookeeperさんはきっとCとかでちゃんと書いた方がちゃんとその性能を使い切れるだろうし、もっと難しい問題をきちんと解いてくれるために存在するのだろう。そういうわけでMySQLに挑戦してみた。

MySQLで実装

さて、MySQLでの実装だけれども、基本的な考え方としては単純。AUTO_INCREMENTなりなんなり、単純増加していくキーをつくって、それの順番にSELECTするだけ。一番上の行が勝って、リーダーになる。zookeeperサイトに書いてあったアルゴリズムそのままですね!

mysql> select * from worker_election;
+-------+------------------+------------------+
| id       | drone_id         | expires_at      |
+-------+------------------+------------------+
| 3405 | xxxx.10421   | 1347939786 |
| 3406 | yyyy.10926   | 1347939792 |
| 3407 | zzzz.11508   | 1347939802 |
+------+-------------------+------------------+

勝ったプロセスはリーダーになり、自分も含めていくつのワーカー(親)が登録されているのかを知れる。これとは別に各種ワーカー(子)がそれぞれいくつ起動するべきかが登録されているテーブルを参照し、1ワーカー(親)につき最大何個子プロセスをforkすべきなのかを登録する。リーダーを含めたそれぞれのワーカー(親)はこの情報を読み込み、プロセス数を制御して、必要あれば新規にforkし、プロセス数が多すぎれば既存のプロセスを殺す。

基本的にはこれだけ。ここにいたるまで2パターンほどおかしな実装を書いたけれども、最終的に確実にバランスするにはリーダーが全部決めて、それを定期的に他のワーカーが参照して自分の状態を直すのが一番正しいということになった。

あとは重要な機能としては必要でない時にはリーダー選択のためのSQLをムダに発行しないとか、ある程度の時間更新がないワーカーは自動的にリーダーの選択肢からはずすとか、Parallel::ForkManagerを使いつつでもループのコントロールは自分でしたかったので->wait_one_child()という非公開APIつかってるとか、色々あるんだけど、それはまぁ枝葉。

こういう仕組みを入れたので、あとはぼこん、とデプロイすると勝手にワーカー達が最大プロセス数から逆算して誰がどのワーカーを何プロセス分forkするのか計算してやってくれる。人間はもうシステム全体の最大数以外設定する必要はない。

あー、楽ちん☆ という結果が今このブランチに入っています(そろそろmasterの古いコードを別ブランチに移動してこのブランチをマスターに切り替えるか・・・)

ちなみに自動化されているとは言え、障害等いつ起こるかわからないので、GrowthForecastに10分間隔くらいでps -ef | grep stf-workerをパースした結果をワーカー数を報告させている。さすがにモニタリングしないでいいや、と思うほど自分のコードを過信してない。

まとめ

というわけでそれまでも使っていたMySQLでそのままリーダーエレクションアルゴリズムを使って(多分)賢くリソースを自動的に調節するようにできました。zookeeper自体も別によかったんだろうけど、うちのチームにとっては未知のミドルウェアを入れないでもやりたいことできてよかったですね。