kestrelの作者さんによるkestrelの紹介

古い記事ですが、kestrelの作者さんによるkestrelの紹介。

http://robey.lag.net/2008/11/27/scarling-to-kestrel.html

生い立ち

もともとScalingという名前だったそうです。
というのも、RubyにあるStarlingというMQサーバーをScalaに移植しようとして作り始めたそうで、memcacheプロトコルを使うという仕様もここからきています。
Starlingについてはhttp://www.rubyinside.com/starling-and-rudeq-persistent-ruby-queues-958.html
だけど色々機能を追加したからちゃんと名前つけようか、ということでkestrelになったとのこと。

kestrelで実装した3つの機能について。以下、簡単に和訳します。
(Fanoutについては別のエントリで書いているみたいです。このときは未だ無かったのかも。)

Big Queues

まともに動いてるMQサーバーなら、producerよりもconsumerのほうが多いべきで、キューは空っぽのはず。リクエストが来たら速攻処理するのが当然だよね。キューは全部メモリに置けるもので、キューするのはいざって時のため。

だけど、大統領選挙みたいなイベントでTwitterに負荷がかかると、kestrelを手動で調整しないとメモリが足らなくなることがあるよね。

今のkestrelは、メモリ消費が設定値(デフォルト128MB)の限界を超えると、メモリ内のキューには入れずにジャーナルにだけ書き込むようになってるんだ。(read-behind mode)メモリ内にはキューの変わりにジャーナルファイルへのポインタを持っておいて、その後にキューが消費されると、ジャーナルファイルから読み込んでメモリ上のキューに詰めなおすよ、というわけ。*1

こんな感じで、キューの先頭部分だけメモリに持って、残りをジャーナルファイルとしてディスクに持つことで、メモリ消費を抑え、高いパフォーマンスを実現し、突発的なトラフィックにも備えてるんだ。
ディスクがいついっぱいになるか分からないのが不安かもしれないけど、一時的な高負荷のときは、そんなことより別のこと心配したほうがいいよ。 少なくとも一時的な高負荷には対応できるよ。(このアプローチで全てを解決するわけではないけどね)*2

Blocking fetches

memcacheプロトコルを使うと、キューをフェッチするときにクライアントの処理をブロックする手段がないってよく言われるね。kestrelはキューに項目があれば、すぐにそれを返し、無ければ無いとすぐに応答しちゃうから。さっき書いたように、処理すべきキュー項目よりもconsumerが多いべきで、consumerは常に仕事を求めてフラフラしてるはず。
そうすると、以下のようなクライアントコードを書くことになるね(Ruby

while !(response = QUEUE.get(queue_name))
  sleep 0.25
end


そこで、ちょっとイマイチだけどmemcacheのgetコマンドに"/"区切りでオプションを指定できるようにしてみた。"/"ならファイル名に利用できないのでキューの名前としても使われることはないよね。*3
さらに以下のようなタイムアウトのオプションを追加した。これでクライアントが一定時間処理をブロックできるようになるわけさ。*4

while !(response = QUEUE.get(#{queue_name}/t=250")); end

"t=250"は、「今キューに何も無かったら、250ミリ秒は何か来るまで待ってろ」という意味。タイムアウトするまで何も無かったら、kestrelは空の応答を返す。ここで大事なのは、memcacheクライアントがこのタイムアウト時間より長い時間を設定していないといけない、という点だね。

いろいろ試したけど、これが一番簡単だった。
各キューは待ち行列を持ってて、そこにクライアント(consumer/worker)が連なる仕組みになってる。クライアントがタイムアウト指定付きのgetリクエストを送ってきて、その時点でキューが空だったら、クライアントを待ち行列に追加するんだ。このとき、クライアントのActorがreceiveWithin(timeout)メソッドで新しいメッセージが届くのを待つ。キューに項目が届いたら、クライアントはキューの待ち行列から削除され、メッセージが通知される、という仕組みだ。

1つのproducerにゆっくりキューイングさせて、100とか500クライアントからタイムアウト付きのgetリクエストを送ってみたところ、いい感じに動いたよ。

Reliable Fetch

キューは信頼してもらってOK。set操作が正しく動けば、kestrelは"STORED"を返すんだけど、その時点で既にジャーナルファイルに書き込まれていて、"STORED"を返したからにはkestrelはそのキュー項目に対して責任を持つ。

でもキューからフェッチする処理はそんな簡単じゃないんだ。
kestrelがキュー項目をクライアントに送っても、クライアントはackも返さないし確認メッセージも送ってこない。すべてちゃんと受け取れたと仮定することしか出来ないんだ。クライアントがデータ転送中にダウンしたり、受け取ってからクラッシュしてみろよ、そしたらキュー項目はどっかいっちゃって、二度と手に入らない。

そこで、getリクエストに"open"というオプションを追加したんだ。
もしキュー項目があれば、kestrelは通常通りその項目をキューから削除してクライアントに送るんだけど、もしそこでクライアントが確認メッセージを送らずに接続を切った場合は、kestrelはキュー項目をロールバックしちゃう、という仕組みさ。"open"オプションによるフェッチは以下のようにやってくれ。

1."open"オプションを付けてgetリクエストを送る。

QUEUE.get("#{queue_name}/open")
and confirmed with:

2.続いて"close"オプションを付けてgetリクエストを送る。

QUEUE.get("#{queue_name}/close")

これは何も応答を返さない。余分な通信を避けるために、以下のようにopenとcloseを一度に行うことも出来るぞ

QUEUE.get("#{queue_name}/close/open")

クライアントが"open"オプションつきでフェッチしていれば、もし接続をロストした場合でも、フェッチされたキューはロールバックされ、次のクライアントが処理してくれるよ。

まとめ

ここで紹介した3つの機能によって、

  1. 突発的なトラフィックの増大に対処できるよ(Big Queues)
  2. リクエストに即応できるよ(Blocking Fetches)
  3. 信頼性も提供できるよ(Reliable Fetch)

RubyのStarlingを拡張して、コードサイズを50%増しにしたけど、それだけの価値のある機能を実現できたと思うよ。


以上です。
昔の記事ですが、現在のソースコードと見比べても内容はそんなに変わっていないようです。

*1:read-behind modeについてはソースコード見ると今も書いてありました

*2:英語表現が良くわからない。。。and give you one less thing to worry about when the snake is trying to swallow the pig. => 鹿島さんが教えてくれた!thx!!

*3:キューの名前がジャーナルファイルの名前になります

*4:ドキュメント化しろよ ⇒ ありましたすみません。http://github.com/robey/kestrel/blob/master/docs/guide.md