#isucon で優勝してきました

なんでもありのWebアプリケーション高速化バトル、#isucon に会社の同僚 @Songmu @sugyan と3人で、fujiwara組として参戦してきました。結果、幸いにも優勝を勝ち取ることが出来ました。

こんなに楽しいイベントを企画、運営していただいた Livedoor の皆様、本当にありがとうございます!!


さて、ざっとチューニングした経過などを記録しておきます。

まず説明を聞いて、環境を作るところから。IPアドレスでは作業がしにくいし事故も起こりそうなので、hostname とパスワードなしで繋がるように ssh 鍵設定。作業開始当初のバックアップも。

11:58 fujiwara: hostsにrev16, db16, ap116, ap216, opt16設定した
11:58 fujiwara: ssh鍵も登録したので
11:58 fujiwara: rev16から他のホストは ssh db16 -p 16522 でつながる
12:00 fujiwara: rev16:/home/isucon.backup/ に全部取った
12:01 sugyan: ありがとうございます
12:05 songmu: ありがとうございます
12:05 fujiwara: 全部にepelいれた
12:13 fujiwara: Apache access_log format で %D 追加
12:14 fujiwara: nginxに変えるけど
12:18 fujiwara: /etc/sysconfig/network にHOSTNAME設定 & hostname

最初はスコアが 800 / min 程度。何回か計測ベンチマークを回して、初期状態のボトルネックMySQL にあるのを把握。

12:22 songmu: アプリはちゃんとモダンな感じで、薄いシンプルな作りになってるので、最適化の余地はほとんど無さそう
12:37 fujiwara: いまのところdbネック
12:55 fujiwara: mysql query cache有効
12:57 fujiwara: 1.50 inserts/s, 0.00 updates/s, 0.00 deletes/s, 1006190.90 reads/s
12:58 fujiwara: この大量のDBからの読み込みをなくさないとDBネックは移動しない
13:03 fujiwara: slow query 0.1秒にした
13:04 fujiwara: 毎秒1コメント追加されるたびにquery cacheが切れる
13:05 fujiwara: のでslow queryにSELECT a.id, a.title FROM comment c INNER JOIN article a ON c.article = a.id GROUP BY a.id ORDER BY MAX(c.created_at) DESC LIMIT 10;
13:05 fujiwara: これがでて Rows_examined: 172853 をfilesortするから重い

ちなみに、MySQL の Query Cache を有効にしただけで、スコアは 1200 / min ぐらいまで上がりました。

DB にカラムを追加して、クエリを軽くする作業。

13:26 fujiwara: alter table article add comment_created_at datetime;
13:26 fujiwara: create index article_comment_idx ON article (id, title, comment_created_at);
13:26 fujiwara: update article set comment_created_at = (select max(created_at) from comment where comment.article=article.id);
13:26 fujiwara: カラム足して既存データ突っ込んだ
13:33 fujiwara: select id, title from article order by comment_created_at desc limit 10;
13:33 fujiwara: サイドバーはこれで作れる

これで 20,000 / min 程度まで一気に改善。

この時点でボトルネックは DB サーバから app サーバの CPU に移動しているので、そこを軽くできないか検討。

14:05 fujiwara: rev16にmemcached建てた

高速化を狙って、以下のようにアプリケーションを @sugyan, @Songmu に書き換えてもらいました。

  • nginx memcache plugin を使用し、memcached に存在するコンテンツは app を介さず、nginx から直接配信する
  • POST /comment/[articleid] の後、1秒後にはその内容が反映されている必要があるため、POST を受けたら、その直後に GET されるであろう内容を app で生成して memcached に保存

ここで 30,000 / min 程度。

更にボーナスポイントの 100,000 / min を狙って、GET 時にも cache を生成して、POST があれば更新、という構成にしたところ、「サイドバーが更新されてない!」というチェックに引っかかってベンチが fail するという問題が発生。

レギュレーションを読んだ限りでは更新した1秒後のみに整合性チェックと思いきや、実はその後の任意のタイミングでサイドバーの整合性チェックが走る。まさに鬼(ry

少しでも cache に hit して app の負荷が減り、かつサイドバーのチェックをかいくぐれるように、cache 寿命を微調整。

  • 5秒 : 完走
  • 10秒 : ごくまれに fail

という状態だったので、本番計測の 180 秒に向けて 5秒で行くか 3秒にするかいろいろ考えたものの結局は cache 寿命 5秒で最終決定。

結果、270,000 / 3min (≒90,000 / min) 程度のスコアで、2位に3倍程度の差をつけて優勝できました。ありがとうございます。

[あれこれ細かいこと]

次回があるかどうか分からない、ということですが、今回の優勝構成がデフォルトでそこからどこまで伸ばすか (by mala さん)、というハイパフォーマンス特化イベントになるのを wktk して待ってます!