#isucon ではどんなことを考えながら作業していたか

前のエントリ #isucon で優勝してきました は当日夜に酔っ払った頭で勢いで書き上げたので、少し冷静に振り返ってまとめてみます。

最初のボトルネック発見

  • DB が CPU 4コアをフルに使って回っているのですぐに Query が重いのは分かった
  • 重いクエリはキャッシュすれば、という発想は自然 (実際 MySQL のクエリキャッシュだけでスコアは 1.5倍程度上がる)、とはいえ
    • このクエリは実行に 300〜400 ms 程度かかる
    • アプリケーションの要件上、毎秒更新する必要がある
    • 1秒ごとに更新に 0.3〜0.4秒かかる処理をするのは悪手だろう
    • cache が消えてから生成、とすると生成処理が複数同時に走って無駄が大きい (実際ベンチマーク中の slow query を見ると 600〜700 ms 程度の時間が掛かっていた)

ということで、DB のテーブル構成を変更して高速化できないか、という作業を優先。

アプリケーションサーバの CPU ネック

  • この時点でスコアが 20,000 / min 程度
  • 全体の 80% 程度がアプリケーションへのアクセス
  • つまり 20,000 / 60 * 0.8 = 266 req/sec で処理できている
  • DB を引いて、テンプレートをレンダリングする Perl の Webapp として、この速度はかなり速い
  • ここを攻めても数倍の改善をするのは難しいだろう

ということで、アプリケーション自体をあれこれするのは手段から外し、フロントで cache させる作業を優先。
想定される優勝ラインが 100,000 / min、つまり 1,666 /sec で、これは Starman で何もしない Hello World を実行する程度の速度。そこまで高速化するのは無理という判断です。

Apache -> nginx への切り替え

  • Apache は最近あまり触っていないので、慣れている nginx のほうが安心
  • cache に memcacehd backend を使うつもりだったので当初から予定
  • ソースから入れる時間がもったいないし、そこまで最高性能は要らないはずなので EPEL の RPM を使用 (memcached も同様 RPM)

通称「kazeburo の罠」への対処

  • Apache から nginx に切り替えてスコアが一気に落ちたので、access.log を tail -f で観察
  • 最初は順調に流れるようなのにどんどん詰まっていくかんじ
  • (経験上) これは KeepAlive かなー
  • keepalive_timeout をデフォルトの 65秒から 5秒に変更したらスコアが上がったので確信
  • keepalive off

ip_conntrack 溢れへの対処

  • 大量のリクエストを捌くときには実環境でもありがちなので、最初から頭にあった
  • 問題が起きたら off にしよう、と思ってた
  • スコアが不自然に落ちたので dmesg を見たら案の定、だったので迷わず off

nginx -> memcached ローカルポート枯渇への対処

「サイドバーの罠」への対処

  • これは現象を把握するまで1時間ぐらい悩んだ
  • おそらく ESI 的な対処が最善だろう、と見当は付いたものの、使ったことのない varnish を導入してはまったら確実に時間切れ
  • この時点で 30,000 / min までいっていたので、最悪 GET 時の cache off にして安全策をとっても 2〜3位にはなれるかな (弱気)

その後公開された ベンチマークツールのソース をみたところ、「毎秒1回更新されるリストの最新10件中、どこかに反映されていれば OK」というロジックだと判明しました。
つまり、最大で 10秒弱までは cache を保持していてもチェックが通るはず。
これは作業中の、

  • 5秒は完走
  • 10秒は完走することもあるが、まれに失敗する

という現象とも符合します。
とはいえこの時点ではチェックロジックは未知のためかなり悩み、最終的にベンチマークが完走するかどうか、不安をもって最終判定を迎えることになりました。

事前βで最高スコアをたたき出したのは nginx で SSI を使う、という目から鱗というか、SSI とか10年ぐらい使ってないので自分は考えもしなかった構成だそうで。いいことを聞いたので、自分でも検証してみたいと思います。