ISUCON 3 の参加記録
2013.11.11 isucon performance
Web アプリケーションのパフォーマンスコンテスト ISUCON 3 に参加し、2 位の成績となった。どのような状態で当日を迎え、どのような作業を行ったのかをまとめる。
私自身はこれで三度目の ISUCON 参加となるが、今回チームを組むメンバーはみんな初めての参加ということもあり、事前の打ち合わせでは以下のようなことを話していた:
- これまでの大会の説明と典型的なアプローチ
- 同時に、過去にとられた戦法は参考程度であること、あくまで現物のアプリケーションを元に戦略を立てるべきでアプリケーションやミドルウェアを事前に決めることは危険であるということは強調
- よく使われるミドルウェアの概要、メリット/デメリット
- 主要機能のほか、キャッシュ部分の永続性の有無(単純に考えればメモリだけで処理してファイルに書き出さない方が早いが、ベンチマークをまたいでキャッシュを引き継げれば切り札になり得る)、キャッシュの時間単位(ベンチマークがフェイルしない限り極力キャッシュしたいという要望を考えると、ミリ秒単位で制御したくなることが多い) などのコンテストで重要となりそうな部分について解説
- 今後問題として取り上げられそうなWebアプリケーションの構成要素
- 毎回趣向を凝らした題材が提供されるので完全に予想しきることは不可能だが、Web プログラミングで頻出する要素(予選で言えば「セッション」「ページング」のような単位)を挙げ、他にもどのようなものが考えられるか、どうやって実装できてどのような高速化が行えるかを話し合い
予選までに数回休日に集まっては過去の問題を解いたり、自分たちで考えた予想アプリに対してチューニングをしてみたり、もくもく自分なりのトレーニングをしたりする会を開いていた。
また、個人的には以下のようなことを行ってきた:
- よく目にしているWebアプリケーションの解析
- 普段 Web を閲覧しているときでも、どのようなデータ構造が想定されそれをどうやって KVS に乗せるか、画面の構成要素のうちリアルタイムで作る必要がある場所はどこでキャッシュ可能な場所はどこか、といったことを考えるようにする
- 速度に対する感覚を養うため、アプリケーションやミドルウェアのチューニングとその際の効果測定
- 自分の日頃扱っているWebアプリケーションではネットワークが飽和するようなアクセスはこないため、限界性能を引き出すような使い方はしていないことが多い。「コンテンツキャッシュでさばいてアプリに届かせないのが定石っス!Nginx とか Varnish っていうのがApacheよりいいらしいっス!」みたいなレベルではなく、アプリケーションと Nginx ではどれくらいの性能差が生まれるのか、Nginx を使うのであれば proxy_cache と比べて Unix Domain Socket でMemcached に接続した SRCache ではどれくらい単位時間あたりの処理数が変わるのか、別サーバーにある Memcached と TCP 通信した場合や Redis に変えたときではどのように変わるのか、というようなことを考えられるようにしておく
ベンチマーク上の最速を求めるということではなく、どういった選択肢が考えられ、それぞれにどのような性能と機能のトレードオフが発生するのかを体にしみつけておく
- 自分の日頃扱っているWebアプリケーションではネットワークが飽和するようなアクセスはこないため、限界性能を引き出すような使い方はしていないことが多い。「コンテンツキャッシュでさばいてアプリに届かせないのが定石っス!Nginx とか Varnish っていうのがApacheよりいいらしいっス!」みたいなレベルではなく、アプリケーションと Nginx ではどれくらいの性能差が生まれるのか、Nginx を使うのであれば proxy_cache と比べて Unix Domain Socket でMemcached に接続した SRCache ではどれくらい単位時間あたりの処理数が変わるのか、別サーバーにある Memcached と TCP 通信した場合や Redis に変えたときではどのように変わるのか、というようなことを考えられるようにしておく
どんなアプリケーションがくるかわからない以上、不安がぬぐいさることはできない。この時点でできることは「当日できるだけ早くウィークポイントを見つけること」、「見つけたウィークポイントに対しての効果的な対応をとれるための引き出しを増やしておくこと」であり、そのための準備をひたすら積み重ねてきた。
予選
予選問題はチームメンバーの勤め先の会議室を借りて取り組んだ。何度も事前打ち合わせで利用していたので、ストレス無く作業に打ち込めた。ただ、興奮しすぎて当日 2 時間くらいしか眠れていない状態だったため、テンションが高いのに頭があんまり働かなくてだいぶつらかった。
題材はコードスニペット投稿アプリケーション、いわゆる Nopaste や Pastebin と呼ばれるたぐいで Gist を想像してもらうとわかりやすいと思う。ログインがありセッションが発生すること、公開/プライベートのフラグがあることがこれまでの題材との大きな違いであったが、ある程度予想の範囲内であったことと後述するとおりベンチマークの抜け道に気づけたので、すぐにスコアを伸ばすことができた。
主な施策は以下の通り:
- インフラ面
- Varnish の配置
- ページキャッシュ。Cookie の値を参照し、ユーザー別にキャッシュを管理することでログイン中のユーザーもキャッシュ対象となるよう設定
- Varnish の配置
- アプリ面
- markdown -> html の事前変換
- 全件 markdown -> html 化したテーブルの dump データを用意しておき、DBクリア後にリストアしようとした
リストア時間が時間が40秒くらいかかり初期化制限時間60秒をだいぶ圧迫すること(*)、途中から初期データはほぼページキャッシュでさばけるようになっていたので、ここに力をかけるのは無駄だと判定して採用しなかった- (*) init の間に /var/lib/mysql をまるごと差し替えるなり別のDBにアプリを接続するなりで回避できるので、これ自体はぬるい判断だったと思う
- 全件 markdown -> html 化したテーブルの dump データを用意しておき、DBクリア後にリストアしようとした
- 更新のないテーブルのインメモリ化や Cookie 内へユーザー情報を登録などで DB 参照回数の低減
- (チームメンバー担当)クエリの組み方やインデックスの追加などのRDB最適化
- 今回のアプリの肝となる部分であり、ひたすら調整を行ってもらっていた
- (チームメンバー担当)Memcached の参照方法を TCP から Unix Domain Socket に変更
- 初期状態では MySQL Memcached Plugin を参照しているので成績が伸びなかったが、こによりトラップを回避してくれた
- markdown -> html の事前変換
事後の講評などでも抜け穴があったという話があったが、後日公開された予選のAMIを利用しても以下のような方法で簡単に確認できる。
- アプリを Perl から Ruby に変更 (※ これは単純に自分がRuby に慣れているからで、他の言語では確認していない)
- Memcached への接続先を MySQL Memcached plugin (ポート 11211) から本当の Memcached (ポート 11212)に変更しアプリケーション起動
- リバースプロキシの Apache を停止
- Varnish をインストール
- 以下の設定をして Varnish を起動
/etc/varnish/default.vcl
backend default { .host = "127.0.0.1"; .port = "5000"; }
sub vcl_recv {
# POST リクエストやCookieがある(ログイン中)の場合は直接アプリを参照
if (req.request != "GET" || req.http.Cookie) {
return (pass);
}
return (lookup); # それ以外はキャッシュを探す
}
sub vcl_hash {
hash_data(req.url);
hash_data(req.http.host);
return (hash);
}
sub vcl_fetch {
set beresp.ttl = 1d; # キャッシュ期間は1日に設定。数字は適当で、とにかく長くしておけばいい
return (deliver);
}
上記設定で初回 workload=1 で 9000 くらい、2 回目に workload=2 で24000くらいでベンチマークが Fail せずに完走することがわかる。Varnish じゃなくて他のコンテンツキャッシュを利用しても同様。
予選開始直後、Sinatra のアクション単位でリクエスト回数、平均実行時間、ワーストケースの実行時間などの情報がそろえ、いざ Varnish のチューニングを行いはじめたところ、どんなにキャッシュの TTL を伸ばしてもベンチマークが通ってしまうことに気づき思わず吹き出してしまった。
キャッシュで簡単にスコアが伸ばせることに気づいたのがたしかお昼前で、このあと夕方くらいまで 1 位だったと思う。その後トップは手放すこととなったが、最終的に予選5位で決勝進出を決めることができた。
予選後
どうにも簡単に勝てすぎたので本当の自分の実力がどの程度かの不安が残るかたちとなったが、穴を見つけられたのも実力のうちと考えて本戦へ向けての準備を進める。
予選が割とシンプルで簡単にキャッシュで返せるような作りだったので、本戦はよりアプリケーションよりの対応が求められるはず、そのためにはいままで以上にアプリの状況を把握する能力を高めておく必要がある。*stat 系のパフォーマンス調査ソフトに慣れておいたり、ログ解析のやり方探してみたり(GoAccess 使ったりだとか)、プロファイリング用のソフトウェアを把握しておいたり、調査用アクセスカウンタ を準備したり、とにかくすぐに分析できるようにしておいた。
また、予選の際は自分もアプリを改修し、アプリメインのメンバーもインフラ面を考慮してくれていたので、本戦で複数台構成になることにより作業がバッティングすることを懸念された。これについては事前にチーム内でそれぞれにメインとなる作業をお願いして競合しないよう取りはからった。
本戦
予選での経験を踏まえ、前日早めに就寝したお陰で当日は体調的には万全、先着で利用可能なミーティングスペースも確保でき順調な滑り出しだった。
題材は画像投稿版の Twitter で、アップロードした画像ファイルがパブリック/フォロワーのみ/プライベートといった公開範囲に応じて閲覧可能となり、関係するメンバーの投稿が画面に自動反映される SPA (Single Page Application) だった。「ファイルアップロード」「フォロワーのアクティビティがタイムラインに表示」などで、チーム内の感想としては「やっぱりきましたね」、といった感じだったのだが、なによりアプリケーションの出来がよくてびっくりした。
作業用サーバーとしては、アプリケーションが稼働している 1 台と自由に使える 4 台の計 5 台が与えられた。これまでの ISUCON では CentOS 5 系が使われており、データホテルの Web サイトでもホスティングしている VPS の OS CentOS 5.8 となっているので CentOS 5 系が来る可能性も考慮していたのだが、今回は CentOS 6.4 であった。
サーバー受け取り後、バックアップをとりつつアプリケーションのプロファイリングを進めるが、どう見ても画像変換のコストだけが突出していることがわかる。データベースへのアクセスはきわめて短時間で終わっており、ボトルネックではない以上手をかけるのは無駄だと判断した。
状況確認を踏まえ、以下のような作業を行っていった:
- 事前に初期データのアイコンおよび投稿画像はリサイズをかけておく
- バックアップとしてローカルに全ファイルを転送していたので、そのファイルを変換して本番環境に書き戻し
- うっかりリサイズのサイズ間違えたり、そんなに早くない自分のマシン(Macbook Air)で実行していたので、かなりの時間がかかってしまった
- バックアップとしてローカルに全ファイルを転送していたので、そのファイルを変換して本番環境に書き戻し
- 画像のうち、誰でも閲覧可能なファイルは Web サーバーから直接配信できるよう、公開領域にシンボリックリンクを作成
- 全ユーザーと全ファイルのアクセス権限を組み合わせてリンクを作成し、アクセス権限をDBに問い合わせずにレスポンスを返すというのもアイデアとしてはあったのだが、時間がかかるのでこのようにした
- (チームメンバー担当)画像変換時にファイルシステムへファイルを保存。また画像表示時に変換済みファイルがないかのチェックを追加
- (チームメンバー担当)ファイルシステムに保存したタイミングで、リサイズを非同期処理で実施
- 負荷が高くなってくるとこの部分が詰まってしまい、変換待ちが大量にたまるのであまり効率的ではなく、不採用
最終的に以下の構成で計測を迎えた。フロント 4 台に Nginx を配置のうえ try_files
でローカルにファイルがあるかどうかをチェックし、なければバックエンドサーバーに処理を委譲するようになっている。
+----------+ +----------------+
Benchmark -> | Web x 4 | ---- | Web + App + DB |
+----------+ +----------------+
時間がなくてこのようなかたちとなったが、今考えてもこれがよかったかどうかでいえばまったくもってよくなかった。完全にバックエンドのアプリサーバーが負荷でつぶれていたが、全サーバーでアプリを動かせばきれいに台数分スケールしていたはずだったのに、本当に悔やまれる。
タイムアップ時点ではそれほどの成績ではなかったが、本戦のベンチマークを無事乗り越え、気づいたら 2 位という成績で ISUCON 3 の幕を閉じた。
ふりかえって
正攻法ではない方法もいろいろ考えてはいたんだけど、結局のところ他のチームと比べても過激な改修は行わずに済ませた。直しやすそうなところ、目のつきやすいところではなく、手早く確実にウィークポイントを直すというのが目標だったので、それは実現できたんだと思う。
けどやっぱり優勝できなかったのは心残りで、試合終了後や帰宅後のチャット上でもチームメンバーと「非同期のワーカー作る時間の無駄だった」とか「どう考えてもアプリサーバーネックだったし、他の4台でもアプリ動かしてベンチマークのワーカー増やせばトップとれたんじゃないの」とか「Macbook Air でちまちま画像変換するの失敗だった」とか、いろいろ話をしていた。1位があまりに鮮烈で、それ以外の順位は空気みたいな存在だし、やはりこの世はトップ総取りなのだなぁ、と痛感した。
毎回このような場を用意していただいている LINE 社、データホテル社の方々には感謝の限りです。お弁当おいしゅうございました。出題のカヤックの皆さんもすばらしい問題をありがとうございました。