完全に文化祭疲れで昼寝3時間ぐらいしてしまいましたが、懇親会で聞かせて頂いた話やblogやtwitterをみる限り好評だったようで、うれしく思っています。ISUCONに参加して頂いた方、社内で協力して頂いた方ありがとうございました
いくつか至らぬ点がありますが、明日以降に公式にフォローさせて頂きたいと思っています。
さて、既に公開されているので見た方は多いと思いますが、今回ISUCONで使ったベンチマークツールは大きく分けて次の3つのツールに分かれています。
- (1) 1post/secでコメントを投稿し、1秒後にコメントをしたページと、インデックスおよび適当な記事のDOMチェックを行う node.js
- (2) http_load + patch
- (3) css/js/imageのMD5値を検証する perl script
最終的な順位はhttp_loadが行ったリクエスト数で決まるのでもし(1)と(3)だけまじめに処理をして、(2)に対しては適当なコンテンツを返答すれば最速になる、はずです(多分この思考ができるのは根が悪い人だけです)。tagomoris氏が最初に作ったベンチマークツールを見ながら、このようなチートを如何に防ぐかというのをかなり開催日ぎりぎりまで考えていました。
以下はデフォルト状態でのnode.jsのHTTPクライアントがリクエストの際に発行する HTTP のヘッダです
GET / HTTP/1.1
Host: 192.168.1.1
Connection: close
User-Agentは指定しないとでません。User-Agentを指定すると、
GET / HTTP/1.1
User-Agent: Isucon1
Host: 192.168.1.1
Connection: close
とGET行の次に追加されます。http_laodはどうなのかと調べると
GET / HTTP/1.0
Host: 192.168.1.1
User-Agent: http_load 12mar2006
となります。User-Agentヘッダの場所、Connectionヘッダの有無、そしてHTTP/1.1プロトコルのバージョンの違うことが分かると思います。もし参加者がこのあたりに気付けばチートが簡単にできてしまいます。そのためまず node.jsのDOMチェックツールにUser-Agentを追加し、http_loadにはパッチをあてました。
--- http_load-12mar2006.orig/http_load.c 2006-03-13 04:17:03.000000000 +0900
+++ http_load-12mar2006/http_load.c 2011-08-29 00:50:53.000000000 +0900
@@ -1019,13 +1019,15 @@
}
else
bytes = snprintf(
- buf, sizeof(buf), "GET %.500s HTTP/1.0\r\n",
+ buf, sizeof(buf), "GET %.500s HTTP/1.1\r\n",
urls[url_num].filename );
bytes += snprintf(
+ &buf[bytes], sizeof(buf) - bytes, "User-Agent: %s\r\n", VERSION );
+ bytes += snprintf(
&buf[bytes], sizeof(buf) - bytes, "Host: %s\r\n",
urls[url_num].hostname );
bytes += snprintf(
- &buf[bytes], sizeof(buf) - bytes, "User-Agent: %s\r\n", VERSION );
+ &buf[bytes], sizeof(buf) - bytes, "Connection: close\r\n" );
bytes += snprintf( &buf[bytes], sizeof(buf) - bytes, "\r\n" );
これがまず、プロトコルを合わせ、ヘッダの順をあわせるだけのパッチです。そしてこのとき1つ機能を追加しました
+ if ( (unsigned long) random() % 100 > 30 ) {
+ bytes += snprintf(
+ &buf[bytes], sizeof(buf) - bytes, "Accept-Encoding: gzip, deflate\r\n" );
+ }
それがこの一定の割合でのgzipヘッダの追加です。コメントの数が増えることでトラフィックも増え、HTTP圧縮が有効に活用できるんじゃないかと思ったんですが、、、、実際はproxyのCPUを使ってしまうためあまり効果がなかったようですne。
この変更により、http_loadが行っていたコンテンツのサイズのチェックは意味がなるなる可能性があったので、コメントアウトしました。
@@ -1682,6 +1688,7 @@
++http_status_counts[connections[cnum].http_status];
url_num = connections[cnum].url_num;
+/*
if ( do_checksum )
{
if ( ! urls[url_num].got_checksum )
@@ -1716,9 +1723,9 @@
}
}
}
+*/
}
その代わり追加したのが、(3)のperlによるcss/js/imageのMD5チェックサム確認ツールです。チェックサム確認ツールは単純に取得して確認するだけはなく、Accept-Encodingを追加したリクエストと、追加していないリクエストの両方を行っています。もちろん、Accept-Encodingが付いているリクエストはhttp_loadだと想定してのチートを防ぐためです。ただし、css/js/image以外のインデックスや記事ページにたいしてはなんのチェックもしていないので実はここが穴でした^^。本当は記事が3000件あるかどうかのチェックとあわせてやろうかと思っていたんですが時間切れで間に合いませんでした。
と、このパッチをあてている最中に、例の鬼とか悪魔とか言われたリクエストパターンを思いつきます。
もともとtagomorisと性能がでない罠を作ろうと言っていてまるまる2日ぐらい悩んでいたのですが、サーバ側の設定を変更してバックログや使えるリソースの制限をしてもあまり効果(逆効果)がないか、エラーがアプリケーションのログやサーバのログにすぐにでるのである意味分かりやすいという理由で、罠パラメータやめようかと相談していのですが、ちょうどその時、別案件でnginxに対してこのパッチをあてるという作業をし、罠の天使(悪魔)が舞い降りてきた訳です。原因はすべてnginx-1.0系に上のパッチをあてれるようにしてくれたthi■■■o氏だった!!(ちがう)
それが
+ if ( (unsigned long) random() % 100 >= 97 ) {
+ bytes += snprintf(
+ &buf[bytes], sizeof(buf) - bytes, "Connection: keep-alive\r\n" );
+ }
+ else {
+ bytes += snprintf(
+ &buf[bytes], sizeof(buf) - bytes, "Connection: close\r\n" );
+ }
このhttp_loadへの変更。
http_loadはそもそも、HTTP/1.1やKeepAliveをサポートしていないツールで、Content-Lengthがない場合サーバから切断されるまでコンテンツを読み取ろうとします。上の変更により少ない確率ですが、HTTP/1.1、Connection: keep-aliveでのサーバにリクエストを行い、サーバ側がHTTP/1.1をサポートし、KeepAliveが有効になっていて、コンテンツがLengthが付かないchunked転送で送られると、http_loadとrevser_proxy間の接続がコンテンツの転送後もしばらくの間(KeepAliveTimeout)切断されなくなり、http_loadはコンテンツ読み取りが完了していないと思い込み、半ストール状態に陥り次のリクエストに移れず、ベンチマークの結果が悪くなります。
デフォルトで用意していたApacheはKeepAliveの設定がOffでしたが、Nginxなどイベントベースな軽量なWebサーバを使うとKeepAliveがデフォルト有効で、Timeoutも長い(Nginxのデフォルトは65秒!)ので、ApacheからNginxに変更したとたんベンチマークの値が劇落ちします。わりと多くのチームがはまり、そしてApacheに戻したりしたようです。中にはApacheの時にKeepAliveを有効にして遅くなったのを確認したチームもあるようです。
時間がもうすこしあればtcpdumpでみたり、http_loadの中身を確認するチームが出てきたのかもしれませんね
今回、こんな感じでチート対策と罠をしかけましたが、ソースコード一式も公開されているのでまだまだこんな穴があるとか、こういう嫌がらせはどうかとかありましたらblogなどなどで書いてもらえるとうれしいです。本当に参加頂きありがとうございました。