2008年06月19日
Erlang勉強会
行ってきました。初mixiオフィス。
Dynamoのアルゴリズムに関しては知ってる話だったので、僕としてはErlangの実装の所が色々と楽しかったです。プログラミング言語Erlangを4月に読んで、結局実際に書かずじまいになってるので良くないなあ。
あとTokyoTyrantではepoll(2)で読み込み可能なfdをゲットしてそれをqueueに入れて、スレッドプール(8 thread)で同期的にread(2)して処理しているらしい。
それだとまだ届いてないパケットが有る時にread(2)がブロックして最大性能が出ない気がするのですが、mixiで実際に運用してる所をみるとあんまり関係無いのかしら。
僕はいつもepoll(2)ループでプロトコルの終端までread(2)してから、その内容と共にqueueに突っ込んでたので、あれ?と思ってしまった。
打ち上げではid:tasukuchan、瀧内さん、みかかの人達と分散系・アルゴリズム系の話をしていた。余りにも面白かったので、一部の人としか話せなかったのはちょっと勿体無かったかな・・・。
- by
- at 08:25

comments
あくまで想像でしかありませんが、もしもほとんどのリクエストが高々数百バイト程度しかないとすると、read()できる=全部読める、がほぼ成り立つので(例外は複数のリクエストを続けざまに送って、かつ、バッファリングが効いている場合ですが、TCP_CORKやTCP_NODELAYの使い方次第)、read()は実際的にはほとんどブロックしないのかもしれません。たまたま分割されたパケットがあったとしても、複数スレッドを使っていれば、影響は無視できる程度になるのではないでしょうか。それよりも各作業スレッド自身で読むことでキャッシュ性能が良くなる効果の方が大きいのかもしれません。
確認したわけではないので「かもしれません」ばかりで申し訳ないですが、こうも考えられるということで。
ではリクエストのサイズが大きくなりがちな時にどうなのかというのも興味深いテーマなんですが、太田さんがおっしゃるように悪影響があるかもしれませんし、最近のEthernetはスループットだけはやたらと良いので、一旦送信が始まれば、(LANの中なら)案外ブロックしながらでも速いのかもしれません。誰か真面目にテストしてくれたら面白いだろうなあと思います。
コメントありがとうございます。
memcachedプロトコルを使ってらっしゃるという事なので、確かにほとんどのリクエストがMTUに収まりそうですね。それなら納得できます。ご指摘ありがとうございます。
そういう状況なら確かにokujiさんが指摘されているキャッシュなどの問題を考えると、Tokyo Tyrantの方式が一番良いですね。
というかHTTPサーバーとか大体のサーバーにとってはこの方式のサーバーが最適そうですね。
逆にリクエストサイズが大きくなりがちな場合はCだと読み込んでから別スレッドに渡すのが良さそうですけど、クライアントが多くなりすぎるとread(2)スレッドの負担が大きくなりすぎてスケールしなさそうかな?という心配も有ります。
ここ1年ぐらいこんな事ばっかり考えてたんですけど、辺マシンのアーキテクチャにもよるし色々ケースバイケースなので難しいですね...。
色々なサーバーアーキテクチャがとっかえひっかえ出来て、ワークロードを与えると最適なものが分かるようなフレームワークが欲しい所ですね:-)
ただしこの方式は外に晒しているサーバで使うと、悪意のあるクライアントが故意にブロックさせられてしまいますし、悪意がなかったとしても、何らかの理由で通信が途切れた場合に止まってしまうので、かなり信頼性の高い環境でしか使えないと思います。TCPのdefault timeoutはかなり長いので要注意です。
クライアントが多い場合ですが、もしもどのクライアントも同程度のワークロードしか与えないと仮定できるならば、複数スレッドにepollさせて、ソケットグループを適当に割り振ってしまうのが安易だけど最も簡単な方法だと思います。
それ以外に、担当スレッドもepollすることにして、すぐに読めない場合にはキューからさらに担当ソケットを取得するという方法があります。ただこの方法はロードバランスが難しくて、一部スレッドに担当ソケットが溜まりすぎてしまう可能性があります。結局一旦担当することにした場合でも、他のスレッドに(すでに読み込み済みのデータも含めて)再割り当てする機構が必要になるでしょう。
これをさらに単純化すると、すぐに読めなかった場合には、epollしているスレッドに即座にソケットを(すでに読み込み済みのデータをくっつけて)返却するという方法もあります。これならロードバランスが崩れるという問題はありません。しかしソケットが行ったり来たりしないといけないので、オーバーヘッドが馬鹿にならないかもしれません。しかしもし頻繁にこういうことが起きるならば、そもそもepollしているスレッドが過負荷にはなりにくいでしょうから、こういう面倒なことは初めからやらなくていい、ということかもしれません。
いずれにしてもユースケースに合わせてテストして、十分な性能が得られるものを選ぶしかなさそうですよね。でもこういうのをフレームワーク化するのは大変そうです。フレームワークのユーザが一切フレームワークの仕組みを知らずに書けるとは思えないので。だから、フレームワークがあったとしても、別の方法を試すために結局ユーザ側にも手を入れないといけないんじゃないでしょうか。この問題はコンポーネント屋さんには常に付きまとう課題ですね。
確かに適当なパケット送れば簡単に止められてしまいますね。そこまでは考えてませんでした。
僕が実装してみたサーバーでは、acceptスレッド・epollスレッドグループ・プロトコル処理スレッドグループという3つのスレッド郡を用意しました。
acceptスレッドはbindしたソケットを監視し、connectしてきたクライアントが来たら、各epollスレッドが所持しているキューに入れます。入れるときには、キューの長さが一番少ない所に入れるようにする事でロードバランスします(least connection)。加えてキューに入れたときにepollスレッドに対応するパイプを最初に作っておき、そこに1バイト程書き込みをします。
epollスレッドはpipeに書き込みが有るとepoll_waitから返ってきてキューを舐め、監視しているソケット郡に加えます。後は適当に読み込み可能イベント等を監視しプロトコル終端まで読み込み、読み込み終わったら次はワーカースレッドにソケットと読み込んだ内容をキューで渡します。
ワーカースレッドグループは単一のキューを持ち、そこからタスクを奪い合ってどんどん処理を行い返答を返す所までやってしまいます。本当は返答を返す用のスレッド郡も作ると理想的なのかもしれません。あとはpipeに書き込むことでepoll_waitの呼び出し回数が増えている影響がどこまで出てるかも興味が有るのですが見てる感じここはそんなにオーバーヘッドになってなさそうです。
自分の勘と好奇心のままに最適そうなサーバーを実装しただけで他のアーキテクチャと比較したベンチマークをしていないので何とも言えないのですが、これだと大体過負荷になるスレッドを撲滅する事が出来てそうです。
でも、やっぱりフレームワーク欲しいですね!ハンドラを定義するだけで全てのアーキテクチャをカバー出来ないですかねえ・・・。Thrift(http://developers.facebook.com/thrift/)だとeventベースのサーバーとスレッドベースのサーバーを簡単にとっかえひっかえできる(使うクラスを変える)のでRPCの仕組みと組み合わせればなんとか作れそうな気がしなくも無いと思っています。
長々とすいません(^_^;
いえいえ。とても興味深くて参考になります。フレームワーク、もし出来そうだったら是非作って公開してください。どれぐらいユーザに影響を与えないで作れるものか、関心があります。
ちなみに、釈迦に説法で、もうすでにされているかもしれませんが、よくあるハックの一つに、epollで読めると判定されたソケットについては、non-blockingでreadを失敗するまで繰り返すことができます。そうすると、epoll_waitの呼び出し回数を減らすことができます。ただし、大量に送ってきたソケットに付き合う時間が長くなってしまって、他が待たされることがあるので、最大回数は制限しておかないとリアルタイム性能が悪くなります。この回数はヒューリスティックで決めざるを得ませんが、大体8回ぐらいに抑える人が多いみたいです(根拠は不明)。
それでは。