ISUCON 7 予選2日目を3位で通過しました

まずは出題と運営チームの皆様にお礼を。予選から1チーム3台、合計1200台のサーバを用意するという空前の規模で、快適な競技環境を用意していただいてありがとうございました。

isucon.net

今回は ISUCON 4 の時の fujiwara組 (@fujiwara, @acidlemon, @handlename) を再結成して、自称社内最強チームで望むことに。1日目には同じくカヤックから参戦のチーム MSA が1位を取っていて、これは予選通過はもちろん、スコアでもできれば負けたくないという戦いでした。

f:id:sfujiwara:20171023114230p:plain

最終的には 48万点越え、両日通してのスコアでも3位ということで、まずまずの結果が残せたと思います。

やったこと

  • あらかじめ用意しておいた Chef recipe で各種ツールや各人のアカウント作成、公開鍵設定
    • さくらのクラウドで用意されている Ubuntu から使われるとすれば今回は 16.04 LTS だろう、ということで決め打ちできたので楽だった
  • Go 実装で行くことに決定
    • 前日1位の MSA は (聞いてないけど面子的に) Go だろう、変にハマることもなさそう、という判断
    • (とかあるので、なんだかんだで2日目の方が毎回微妙に有利なんですよね。運営もトラブルが少ないし…)
  • dstat をみながらベンチを掛け、どう見ても画像が DB に入ってるのがまずいのでそれを外出しすることに
    • @handlename が Perl でさくっとやってくれた
  • my.cnf を軽く調整 (@fujiwara)
    • Slowlog出力、innodb_flush_log_at_trx_commit = 2InnoDB Buffer Pool Warmup の設定のみ
    • 結局これ以外いじらなかった
  • css, js, fonts を gzip_static で配信 (@fujiwara)
    • expires max を追加してここは 304 が返せるようになった
  • 画像がアップロードされたタイミングでファイルに書き出し、それがあったらDBをみないで返す (@acidlemon)
  • index 追加 (@handlename)
  • channel テーブルに messages_count カラムを追加して count(*) をやめる (@acidlemon)

ここまで app 1台でやっていて、16時ぐらい。ボトルネックは /icons のファイル配信での帯域で、ここをクリアしないとどうしようもない。

Expires と Cache-Control max-age では、css 等は 304 を返せるんだけど、なぜか icons では効かない…としばらく悩んで、でもここは絶対 Cache-Control でクリアできるはず。@acidlemon が Cache-Control: public を付けてみては、と閃いたので付けたら効く。10万点ぐらい。

ここで css等と icons でベンチマーカーのキャッシュ挙動が異なったのが、だいぶ難しい上にここを突破できないと先に進めないという一番の難関だった感じがします。

  • GET /message, GET /history のループクエリ解消 (@handlename)
  • pt-query-digest の結果から、prepare, execute が2クエリに分かれているので interpolateParams=true の設定を入れてクライアントサイド prepare に (@acidlemon, @fujiwara)

そろそろ複数台構成を伺いたい。となるとファイルの共有なり分散配置が必要になるので、以下のような戦略を決定 (@fujiwara)

  • アップロードされたホストが自分のホスト名を元にディレクトリを切って icons/01/xxxxxxx.png というファイル名で保存、DB にも 01/xxxxxxx.png を入れる
  • nginx(01)
    • /icons/01/ はローカルファイルを見る (そこに保存されているので必ずある)
    • /icons/02/ は nginx(02) へローカルのネットワークで proxy_pass
  • nginx(02)
    • /icons/02/ はローカルファイルを見る
    • /icons/01/ は nginx(01) へローカルのネットワークで proxy_pass
  • nginx(03)
    • /icons/01, 02 はそれぞれ nginx(01, 02) に振る
    • それ以外は app(01, 02) に均等に振る

このように一カ所にファイルを保管しないでローカルに配置し、表からそれぞれ持っているはずのホストに Proxy するという手法は、 ISUCON 3 の出題をしたときに想定解答の一つとして考案していたもの。ファイルは一カ所にしかないので各種メタデータもずれないし、特定のホストに負荷が集中することもないのでお気に入りの手法です。

そういえば、先日の Fastly Yamagoya meetup でも、Fastly は Cache を持っているホストに内部で Proxy することでヒット率を上げている、という話がありましたね。

これで複数台の帯域を使えるようになり、予選ボーダーの20万点を大きく超えてきたのが18時過ぎ。19時前に最高スコア 54万を記録。

このあたりから、Go のアプリを再起動しないで複数回ベンチを掛けるとアプリが太って swap を使いだし、極端にパフォーマンスが落ちるのを発見。

  • 環境変数 GOGC=50 を設定してメモリ消費を抑える (@fujiwara)
    • 1回のベンチ走行後が 800MB → 450MB ぐらいになって、仮に最後の確認で複数回走行されても swap しづらいので安定する
    • ピーク性能は落ちるので、最終的に 50万を超えられなかったのは多分このため
  • ファイル名を決めるのに SHA1 を全部舐めて求めているのが無駄なので先頭 1024 byte だけで決める (@acidlemon)
    • これは先頭 16KB までで判断(したつもりだったけど多分実装ミスでなってなさそう)、初期チェックは通るもののなぜかベンチ中の整合性チェックで死ぬので revert
  • POST /profile で2発飛ぶ update 分を1個にまとめる (@acidlemon)
  • Session に user 情報を全部入れて DB を引かない (@handlename)
    • この二つはいまいち効果がみられず。ただし revert することもないのでそのまま

最後30分は再起動試験をして、再起動後の一発目のベンチで 48万がでたのでそこで打ち止め。

最後は †空中庭園†《ガーデンプレイス》には及ばなそうと覚悟はしていたものの、スギャブロエックスに逆転されたのがちょっと悔しいですね!

今回もコードを書くのは @acidlemon, @handlename に任せて、それぞれ並行して有効手を打ち込めたのでよかったですね。あと Cache-Control: public が早い段階で突破できたのがだいぶ余裕を生んだ感じ。

ともあれ 3位でフィニッシュできて、晴れて本選に進めることになったので、本選も頑張りたいと思います。