なんでもありのWebアプリケーション高速化バトル、#isucon に会社の同僚 @Songmu @sugyan と3人で、fujiwara組として参戦してきました。結果、幸いにも優勝を勝ち取ることが出来ました。
こんなに楽しいイベントを企画、運営していただいた Livedoor の皆様、本当にありがとうございます!!
さて、ざっとチューニングした経過などを記録しておきます。
- [追記] もっと詳しいレポートを @Songmu が上げているのでそちらもご覧ください おそらくはそれさえも平凡な日々: #isucon で優勝させてもらってきました
- [さらに追記] #isucon ではどんなことを考えながら作業していたか - 酒日記 はてな支店 自分でももう少し詳しく振り返りエントリ書きました。
まず説明を聞いて、環境を作るところから。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倍程度の差をつけて優勝できました。ありがとうございます。
[あれこれ細かいこと]
- SELinux どころか iptables も off (ip_conntrack が溢れた
- nginx で keepalive なし (まさに鬼畜な kazeburo の罠が!
- nginx -> memcached の接続が都度切断で local port が枯渇したので対処。See http://d.hatena.ne.jp/download_takeshi/20091013/1255443592
次回があるかどうか分からない、ということですが、今回の優勝構成がデフォルトでそこからどこまで伸ばすか (by mala さん)、というハイパフォーマンス特化イベントになるのを wktk して待ってます!