現在STF は分散オブジェクトストアとしてピーク時にフロントのディスッパチャー1台につき最大80Mbpsを捌いています。この通常のオブジェクト配信するための動作に関しては裏で実際のオブジェクトを格納しているストレージサーバーもさくさくと動いていて特に問題はないのですが(本当の事を言うとアクセス量が増え続けているので、ストレージは増やし続けないとiowaitがじわじわとあがっていく、という問題はあるけど、それはあくまでも中長期的な問題なので今回の話からは除外)、運用しているとストレージサーバー側でオブジェクトの実体(エンティティ)を補充したり、ストレージサーバー間で移動させたりという処理が必要になります。
この際「このストレージにはいってるオブジェクトを全部なめて、正しい状態に戻す」(リペア)という処理を行う事があります。STFのインスタンスごとに規模が違うのですが、最大規模で1ストレージに付き3000万個程度のオブジェクトが格納されているのでこの「全てのオブジェクト」に対する操作は結構膨大なストレージサーバーへのアクセスを生みます。

例えばワーカーを100プロセス展開しておくと、リペアが起こるまでは平穏なわけですが一旦リペアが始まると突然それぞれのワーカーが一斉に唸りを上げながらストレージを痛めつけ始めるわけです。この際ストレージサーバー全台に対してアクセスがばらけていればこの処理もまだ対応できるのですが、同じストレージサーバーにアクセスが集中すればもちろん死亡フラグが立ちます。

STFに触りはじめて1年半、この間ストレージサーバーは当然壊れますので、リペアはたびたび行う必要がありました。その際アクセス負荷による障害を起こさないようにするのは運用者によるワーカー数の調整が必要になっていました。具体的にはワーカーの数などをちょこちょこ調節するわけです。

STFを触り始めた時からつい最近までこのワーカープロセス数の設定はファイルに記述されており、ファイルの編集→プロセスの再起動という処理を行う必要がありました。もちろん設定ファイルはレポジトリに格納されているのでレポジトリで編集→デプロイでもいいのですが、これはこれでまた面倒な話です。

というわけでこれを自動化していくべく何をしたか:


自律分散機能

そこでこの夏(記録によるとこれに関する初コミットは8月3日)ワーカー達に「全体としてN個のプロセスを起動しなさい」という設定を与えることによってワーカーが走っている各ホストごと(厳密には「ドローン」と呼ばれる親ワーカー単位)に起動するプロセス数を分散して調節するという自律分散機能を実装しました。これは前にもエントリを書きましたので実装方法についてはそちらを参照。

この実装によりワーカープロセス数の設定が劇的に楽になりました。Web管理画面から一括で管理できるので必要だと思ったらワーカー数を増やし、アクセスが多すぎると思ったらワーカー数を減らす。簡単ですね!

でもこれでも根本的に運用している人がアラートに気づいて負荷を減らすよう変更をする、というような処理を行う必要があることには変わりありません。面倒ですね!

そこでまずプロセス数をDBの項目一つで一元管理できるということに着目してアクセスピーク時間等に合わせてプロセス数を調節するようなcronジョブを書いてみました。具体的には普通は 50プロセス、負荷があがり始める時間帯の直前に20プロセス、ピーク時に10プロセス、ピークがすぎたら50に戻すという感じです。

これで少なくともあり得ない量のアクセスがワーカーによって生じる事はなく・・・なってない!なぜならピーク時に10プロセス、という値の算出がただ単に「経験上この値で大丈夫だろう」という推測から算出された適当な値だからです。例えばある日突然アクセスが多めの日があったら当然アラートが鳴り始めるわけです。IRCに書き込みが走り、モニタリングツールに登録され、他の人の仕事を増やしてすみませんと肩をすぼめることになってしまいます。Ay Caramba!

スロットリング

そこでスロットリングです。本当に細かくやるならストレージごとに負荷を記録してストレージ毎にスロットリングとかやりたいところですが、それをやると最悪ユーザーからのアクセス方式とワーカーからのアクセス方式をわけるとか激しく面倒くさい事になりそうです。そこでワーカー種別ごとに最大ジョブ処理数をスロットリングすることにしました。

スロットリングのステート管理はどうしようかと考えましたが、揮発性の高いデータだし結構ガツガツ書き込みをしないといけないのでmemcachedでやっちゃいます。キャッシュキーにエポック秒をつけて、1秒ごとのアクセス数を保存します。これは単純に以下のようなコードで実装しています:

   if (! $memd->incr($key)) {
     if (! $memd->add($key, 1, $expire)) {
        $memd->incr($key);
     }
   }

incr() はカウントアップするのにそもそもキーが存在していないといけないので、incr()が失敗したら一度add()を試します。add()が成功していれば"1"が保存されているので処理終了。add()が失敗したらもう一回incr()して、今度こそ成功するべきなので終了。データの厳密な整合性についてつっこまれるとアレですけど、あくまでスロットリングの参考にするための値なのでこんなものでOK。

スロットリングする際参考にする値は1秒ごとのカウントではばらつきが大きすぎるので、適当なスパンのデータを集計します。自分はとりあえず10秒というスパンを選びました。この値は単純に以下のようなキーを取得して合計をとるだけで算出できます:

   my $key = "......";
   my $time = time();
   my @keys = map { join ".", $key, $time - $_ } (0..9);
   m $h = $memd->get_multi(@keys);
   my $count = 0;
   foreach my $value (values %$h) {
     $count += $value;
   }

この値を任意の閾値と比較して、閾値に達していたらワーカーを少しsleepさせます。蛇足ですが、僕は昔sleepに関しては固定値でやるとロック待ちとかしている時に実は意外とみんな同じタイミングで起き上がってきてロックコンテンション率が高いという話を聞き(注意:未検証!未検証!未検証)それを鵜呑みにして以降割とsleepの値はある程度の上限を設けたランダムな値を使用するのが癖になっています。これっていいんですかね?

さて、これでスロットリングできました!わーい、これでアラートも・・・なくならないですね!というか、ピーク時の負荷に合わせると閾値はほとんど0にしないといけないですし、平常時の負荷に合わせればやっぱりピーク時にはアラートがなりまくります。フ○ック!

これはあれか、スロットリングの閾値も自律的に変えてくれないと仕事が減らないじゃないか!

適応スロットリング

スロットリングの閾値のコントロールはどうしたものか。前に書いたように本当はストレージごとに負荷を計測してそのホストに対する処理を遅延・・・とかしたいのですが、STFのワーカーの都合上1ジョブで複数ストレージにアクセスしたりしてそのあたりを厳密にコントロールするのはなかなか難しいです。

であればしょうがない。「ストレージ全台の負荷を確認して、1個でも負荷の閾値を超えていたらスロットリングの閾値を下げよう」という戦略にすることにしました。ストレージの状態に適応してスロットリングするわけです。それだけを定期的に行うワーカーであるAdaptiveThrottlerを作りました。

負荷はもう至極単純にSNMPでlaLoadInt を持ってきて、そのうちの最近1分の値を参考にします。この値がストレージごとどれか一つでも閾値を超えたら、現在のスロットリング閾値の60%まで閾値を落とします。逆に負荷の閾値より下だったらスロットリング閾値を10%あげます。スロットリング閾値には上限が設定してあり、この値まできたらそれ以上閾値の変更はしません。

・・・というのをやっているのをグラフ化したのがこちら。黄土色っぽいのが閾値、青色がカウントしているジョブ処理数です。ピーク時には閾値、処理数ともほぼ0に近いところまで落ちているのがわかりますね。

stf-throttle


この仕組みをデプロイして2日ほどしかたっていないのでまだ調節が必要かとおもいますが、とりあえずわりとうまいこと動いているようです。これでアラートを回避しつつ、好きなだけリペアができるはずだ!YATTA!

以上、最新STF事情でした。