9/29 追記
tokuhiromさんにより,Thrift::BufferedTransportを使って高速化する方法が紹介されました.おそらく,現状ではperlのクライアントの高速化では一番シンプルで高速化できると思います.tokuhiromさん,情報共有していただきありがとうございます!
http://d.hatena.ne.jp/tokuhirom/20090928/1254155859
先週末はThriftのスピード問題にはまり、ガンダム戦記にはまり、ほとんど外に出られませんでした。前回のエントリー(Thriftのスピードについて)の続きとなります。
やっぱりperlのクライアントライブラリに問題がありそう?
多くの有識者の方にアドバイスをいただき感無量でございます。前回のエントリーでは、perlライブラリ、pythonライブラリでThriftが異常なほどに遅いんじゃないか?といった内容でございました。当方のバグではないかと、おそるおそる前回のエントリーをポストしたのですが、tokuhiromさんがこの現象に関して調査と考察の結果を示してくれました(ThriftのPerl Clientが遅すぎる件について)。
クライアントが Pure Perl で書かれており、かつ実装に適当さが感じられ、「速そうには、みえないな。。。」と感じました。
Facebook 内で実際に使用されているとおもわれる、PHP Client の方がつくりこまれている可能性があるので、こちらをためしてみると、また結果が違うかとおもいます
http://d.hatena.ne.jp/tokuhirom/20090928/1254093779より引用
perlのThriftライブラリを眺めてみたのですが、確かにあまり最適化されている感じではないかもしれないですね。僕はPHPは素人なので、PHP Clientのクライアントライブラリがどれほどよく作りこまれているのかはあまり判断できませんが、最初っからPHPでベンチをとればよかったなあとちょっと後悔。実は今もまだ試せていないので、今週末あたりにチャレンジしてみようと思います。
そして、同エントリーでJSONRPC over HTTP in pure perlな環境を構築していただき、やっぱり結果的にThriftのperlライブラリは遅いよねという調査結果をしてしてくれました。そして、なお同エントリーではThriftのperlライブラリを実行したときのstraceした結果までもを示してくれており、以下の表のような結果になり、毎回getpeernameをコールしているのはどうなのかといった可能性を示してくれました。tokuhiromさんありがとうございます。
| syscall | #/request |
| select | 19 |
| send | 11 |
| recvfrom | 8 |
| getpeername | 12 |
解決策の検討
ここまでの調査でやっぱりperlライブラリに問題がありそうかなという目星がついてきました。で、今朝にmikioさんからtokyo tyrantでも開発中にSocketまわりで同じような問題があったよーと教えていただき、ThriftのperlライブラリのSocket周りを調査しました。
Nagleアルゴリズムが原因
以下のサイトに詳しく解説してあるので引用したいと思います。
通常、デフォルトの設定におけるソケット通信ではNagleアルゴリズムというものが使われています。
TCPソケットを使用して通信する場合、やり取りされるデータはブロック単位に分割され、通信用のTCPペイロードに格納されます。TCPペイロードのサイズはいくつかの要因(パスに応じた最大パケット・サイズなど)によって決定されますが、通信が開始されるまでこれらの要因を特定することはできません。最高の性能を実現するには、それぞれのパケットにできるだけ多くの使用可能データを格納することが必要です。ペイロード内に十分なデータがない場合(ペイロードのサイズが最大セグメント・サイズ(MSS)になります)、TCPはNagleアルゴリズムにより、複数の小さなバッファーを自動的に連結して1つのセグメントを作成します。これにより、アプリケーションの処理効率を上げ、小さなパケットの送信数を最小限に抑えてネットワーク全体の混雑を緩和します。
http://www.ibm.com/developerworks/jp/linux/library/l-hisock/index.html より引用
ちょっと難しい説明かもしれませんね。以下の書籍にもうちょっと詳しく書いてあったので引用します。
UNIXネットワークプログラミング〈Vol.1〉ネットワークAPI:ソケットとXTI
W.リチャード スティーヴンス

Nagleアルゴリズムの目的は、WAN上の小さなパケットの数を減らすことにある。このアルゴリズムでは、あるコネクション上で未解決(outstanding)データ(送信済みで応答を待って待機中のデータ)がある場合、それらが承認されるまで、そのコネクション上には小さなパケットを送信しないことになっている。ここで``小さな''パケットとは、MSSより小さなすべてのパケットを指す。TCPは可能な限り最大長のパケットを送信しようとし、Nagleアルゴリズムの目的は、複数の小さなパケットが同時に未解決になることを防止することにある。
UNIXネットワークプログラミング〈Vol.1〉ネットワークAPI:ソケットとXTI P.196 より引用
つまりわかりやすく言うと、膨大なデータをやり取りする場合は細切れにパケットを送信するよりも、最大長になるように待ってから送信したほうが効率がいいし、速度があがるわけですね。
wikipediaにも解説があったので興味がある方は参照してみてください。
http://en.wikipedia.org/wiki/Nagle%27s_algorithm
解決策
で、先ほどの解説ページには以下のように解決策が示されています。
解決策 まず覚えておかなければならないのは、Nagleアルゴリズムは有効であるということです。TCPパケット・セグメントの容量いっぱいにデータを格納しようとするため、パケットの送信までにはどうしても待ち時間が発生してしまいますが、ネットワーク上でのパケット送信数を最小限に抑えるという利点があり、結果的にネットワーク全体の混雑を緩和することができます。 しかし、この送信待ち時間を最小限にしたい場合には、ソケットAPIを使用するのが有効です。Nagleアルゴリズムを無効にするには、リスト1のようにソケット・オプションのTCP_NODELAYを設定します。
http://www.ibm.com/developerworks/jp/linux/library/l-hisock/index.html より引用
今回のThriftのユースケースではファイル転送のようにデータを連続的に送信するわけではなくて、細かい計算の要求を送っては結果を返すという処理になっているので、Nagleアルゴリズムの待ち時間は無駄な時間になってしまうんですね。計算要求をするたびに少しwaitがかかってしまう感じになってしまいます。上記の引用先にも書いてあるように、TCP_NODELAYというオプションを使うとNagleアルゴリズムが使われなくなります。そこで、ThritfのperlクライアントライブラリでもTCP_NODELAYを使えばスピードが上がるのではないかという、仮説が立てられます。
ThriftのperlライブラリでNagleアルゴリズムを使わないようにするには
修正するのは、
Thrift::Socket
です。
Thriftのパッケージに含まれている、以下のファイルを修正します。
thrift/lib/perl/lib/Thrift/Socket.pm
Thrift::Socketの中にopen()というコネクションを確立させる関数があります。この関数にNagleアルゴリズムを使わないようにするために以下の一行を追加します。123行目あたりにあたりにある$self->{handle} = new IO::Select( $sock ) の上に追加します。
$sock->setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1);
修正したdiff
123c123 < $sock->setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1); --- >
これでNagleアルゴリズムを使わなくなります.
結果
修正する前の結果
前回のエントリーにも載せましたが、修正する前はThriftよりRESTのほうが約8倍速い結果となりました。

Nagleアルゴリズムをオフにした結果

なんと、速度が逆転しました。逆にThriftがRESTよりも約2.78倍速い結果となり、QPSは282とかなり良い結果となりました!
まとめ
Thrift::SocketでNagleアルゴリズムを使わないようにすることによってThriftのperlクライアントライブラリの速度が飛躍的に向上しました。このスピードであれば、サービスのバックエンドなんかでは十分に使えるパフォーマンスだと思います。他の言語のクライアントでは試していませんが、おそらくPythonでも同じような設定にすることによってパフォーマンスが向上することと思います。phpに関してはちょっとわかりませんが。。。
また、tokuhiromさんの調査によりそのほかにもThriftのperlライブラリにはいくらか無駄があるので、まだまだハックする余地はありそうです。282QPSという値だけでも十分なのに、XSで書いてもっと早くなったら、ちょっとこれはすごいですね。
nokunoさんがコメントで教えてくださったように、サーバーサイドでもチューニングの余地がまだまだありそうです。今後はThriftのサーバーごとのパフォーマンスをチェックしてみたいと思います。
一応念を押しておきますが,ユースケースによってはThriftでもNagleアルゴリズムを使ったほうが高速な場合はあると思います.ですので,今回の修正はユースケースに合わせて使ってみることをお勧めします.
ご協力いただいた皆様ありがとうございましたー。
Thirftって 「すごく、いい」 です