9月8日(日)に開催された ISUCON9 予選の2日目に1人チーム「 nil 」として参加し、全体1位となり本選出場が決まりました。 最終スコアは 52,440 イスコイン (ベストスコアは 53,460 イスコイン) でした。 このエントリーでは主に参加するまでにやってきたことと、当日やったことについて書こうと思います。
参加するまでにやってきたこと
練習 (去年)
ISUCON には去年の ISUCON8 で初めて参加し、今年で2回目です。 去年は ISUCON8 に向けて毎週のように過去問の練習をしていました。 1年以上前の記憶ではありますが、今年はあまり練習することができなかったので、この経験や知恵が今回の優勝にも影響したと考えています。
練習 (直前)
今年は他のことで忙しく ISUCON の練習をする時間が確保できませんでした。 そのため練習できたのは5日(木)から前日の7日(土)までの3日間だけです。 3日間の全てを ISUCON に使えたわけでなく、環境の構築も3日間の中に含まれているので、実際の練習時間はかなり短かったと思います。 練習には ISUCON8 予選の問題を使い、スコアを上げるよりもツール類の使い方などを復習したりすることを目的に行いました。
ツール類の準備
1人チームなので当然1人で全てを行う必要があります。頻繁に行なう作業などに時間を使ってしまい時間が足らなくなることは想定できるので、事前にツール類や便利なスクリプト等を準備しました。 計測には以下のツール等を使用しました。
また、去年や前日の練習の中で、必要になるであろうスクリプトやスニペット等をいくつか準備しておきました。
- サーバーからソースコードを GitHub に push するスクリプト (git init から deploy key の追加等まで)
- 使用するツール類をまとめてインストールするスクリプト
- アプリケーションのビルド、再起動、ログのローテーションをするスクリプト
- alp の結果を GitHub に投げるスクリプト
- ログ等の設定のスニペット
当日やったこと
ここからは競技中にやったことについてまとめます。 コードは takonomura/isucon9-qualify で公開しています。
10:00~ いつもの
練習などで毎回やっている設定などを最初に行いました。 最低限の設定のみをして、初回のベンチマークを実行しました。 (2,010 イスコイン) その後に、追加で pt-query-digest や Stackdriver のための設定を追加しました。 また、この段階で毎回練習などで毎回行っている設定や MySQL をアプリとは別のインスタンスに分けたりしました。
- 10:00 インスタンスを1台作成・セットアップ
- 2台目以降はベンチマークの実行中等の時間を利用して、途中にセットアップを進めていました
- 10:07 git リポジトリの初期化 91327c0
- 10:13 nginx のログフォーマットを alp のために変更 d61dfc2
- 10:21 スロークエリログの設定 72f64da
- 10:26 Stackdriver {Trace,Profiler} の設定 1dcd1ee
- 10:53 DB への接続設定を変更 adca81f
- SetConnMaxLifetime など
- 11:22 nginx の設定 7144bb2
- worker_rlimit_nofile などの変更
- 11:38 MySQL のみ専用インスタンスに移動 c74a056
12:00~ 各種改善
様々な改善を並列で行っていたため、時系列順ではなくて改善内容別にまとめました。
N+1 の修正
いくつかのエンドポイントで categories や users テーブルに対して N+1 になっているものがありました。それらは IN 句を使ってまとめることで改善をしました。
GET /users/transactions.json
c74a056..c2cc325- うまくコミットをわけてなかったので、他の変更も入ってしまってます。すみません。
GET /new_items/:category_id.json
b70d040GET /new_items.json
f425199
items テーブルにインデックス追加
items テーブルへの SELECT で、 created_at でソートをして、 pagination を行っているエンドポイントがいくつかありますが、それらに適切なインデックスが与えられていませんでした。それらに必要なインデックスを追加しました。 具体的には
created_at
- (
category_id
,created_at
) - (
seller_id
,created_at
) - (
buyer_id
,created_at
)
の4つです。
また、 GET /users/transactions.json
内のクエリでは WHERE に seller_id = ? OR buyer_id ?
と書かれており、そのままだと適切にインデックスを利用してくれなさそうだったので UNION 句を使うように変更しました。 6d68a38
categories をメモリに持つ
categories は変更のないデータなので、起動時に MySQL からロードしてその後は全てそれを使うように変更しました。 8f6d41e
外部 API リクエストの並列化
GET /users/transactions.json
や POST /buy
では外部 API に複数回リクエストをしていました。外部 API はレスポンスがとても遅いので、これらを並列にリクエストすることで改善しました。
GET /users/transactions.json
c74a056..c2cc325- うまくコミットをわけてなかったので、他の変更も入ってしまってます。すみません。
POST /buy
ce3e085
GET /users/transactions.json
でリトライ
GET /users/transactions.json
の呼んでいる外部 API がたまに 400 エラーを返してきて、エラーとなってしまうことが何度か発生していました。
エラーは外部 API のドキュメントに書かれているものではなく、 nginx が出力するものだったため詳細の原因は不明ですが、同時リクエスト数が多すぎると発生するものだと考えられます。(最終的に最後まで発生し続けていました)
そのため、 400 エラーが発生した場合にはリトライをすることで正常にレスポンスを返すようにしました。 5f6a753
GET /users/transactions.json
から外部へのリクエストをなくす
せっかく改善してきた GET /users/transactions.json
ですが、そもそも外部へのリクエストが不要なことがわかりました。
shippings テーブルの status を取ってくるだけで問題なく機能しそうなので、それを使うようにします。 1cd1a0e
ここまでで、 17,260 イスコイン までスコアが上がりました。
15:30~ キャンペーン開始
今までの部分はアプリと MySQL を分けただけで、2台だけで動いているので、まだ使えるのが1台余ってます。また、この段階では CPU やメモリ等にも余裕があるので分けなければいけない箇所は特にありませんでした。 なので、今まで無効でやってきたキャンペーンを有効にすることにしました。
しかし、有効化するとスコアは 6,850 イスコイン まで下がりました。
キャンペーンによりユーザー数が増え、 POST /login
へのアクセスが爆発したようです。その結果、 bcrypt の負荷にアプリサーバーが絶えきれなかったようです。
そのため、残りの1台のサーバーを POST /login
だけに使うことにしました。
その変更の結果、スコアは 34,240 イスコイン となりました。
割合やキャンペーンの数値の変更は cb3cee9..317a4c7 でいろいろと弄っていましたが、キャンペーンの数値を3にしたところでベストスコアの 53,460 イスコイン を記録し、その後は若干下がっていきました。
最終的にはキャンペーンの数値は 4
で、 POST /login
の7割を専用サーバーで、残り3割は CPU リソースが余っていたアプリサーバーと MySQL サーバーで処理するようになりました。
その後は静的ファイル配信を nginx から行なうようにしたりと細かい変更をしましたが、特にスコアに変化はありませんでした。
最後はベストスコアが出ることを祈って何度もベンチマークを回していましたが、徐々に下がっていって焦りました。(しばらく出ていなかった外部サービスの 50x などがありましたが、もしかしたらベンチマークする人が多くて混んでいたのでしょうか?) 17:50 ごろに 52,440 イスコイン となり、これ以上は期待できないと判断して、それが最終スコアとなりました。
感想
今年はあまり練習時間も取れていなかったため、あまり結果には期待していませんでした。しかし、本選出場が決まってとても嬉しいです。不安も多いですが、初の本選も頑張りたいと思います。
また、 Go 言語の参考実装がとても良かったです。ベンチマーカーに関してもスコアが非常に安定していて、不明なエラーなども発生しませんでした。
運営の皆さん、本当にありがとうございました。
本選もよろしくおねがいします。