isucon13に参加しました
はじめに
isucon13に参加してきました。
チームは同い年(修士1年)の3人チームで、全員isucon参加は初でした。
結果としてスコアは22284点で108位という結果でしたが、訳あって再起動試験(追試)でエラーを吐いて失格となってしまいました。(このあたりは後ほど説明します)
チーム構成と役割
全員同い年で学生チームとして参加しました。 各メンバーとそれぞれの役割は以下のとおりです。
- saity : 私。インフラ担当, 手が空いてる時はGoの実装をする。
- auaua : App,DB担当(主にDB)
- kentakunkentakun : App,DB担当(主にGoの実装)
最初のセットアップ(ssh接続やMakefileの編集など)は自分がやり、他メンバー2人はDBのインデックスを貼ることやGoの実装などをメインでやってくれました。とはいっても、最初のセットアップが終われば自分も2人と一緒にチューニング作業をしました。
予行練習
予行練習として、私達は週一で4,5回ほどオフラインで集まって過去問を解くということをしました。本番も同じように現地で集まって行いました。オフラインにしたのはコミュニケーションが取りやすく、情報共有が容易であるという理由からです。この予行練習ではチューニングに必要な環境構築や具体的にどのようなチューニングをするべきかの目処を立てるというのが主な目的でした。前者は主に以下の内容を指しており、ここを詰まらせないようにするかが最初のポイントでした。
- ssh接続
- Makefileの編集
- ベンチを回す。
- alpやslow-queryを見る
- マニュアルを読む
- git運用方針
3回目ほどで環境構築は慣れてきて、計測ツールの導入やgit運用なども問題なくできるようになりました。 チューニングに関しては主に以下のところを重点的に見ました。
- Indexを貼る
- N+1を解消する
- 無駄なDBへのアクセスを減らす(Redisのようなオンメモリでデータを持たせるなどの解決策)
- バルクインサート
- DBサーバーとAppサーバーを分ける。
これらの典型的な実装や設定は調べながら練習していきました。Indexを貼ることやDBとAppを分けることは設定方法が簡単ではあったのですぐにできるようにはなりましたが、N+1の解決やRedisでデータをもたせるようにするにはGoの知識や実装力が必要だったのでかなり苦労しました。(少なくとも自分はかなり苦労しました。。。)
本番の流れ
起床試験は全員合格できました。9時半からYouTubeのライブ放送を見て、isuconの雰囲気を楽しんでいました。(サービスの紹介動画が毎回面白くて好きです。)
コンテスト自体は10:00から開始でした。
10:16 環境構築、初回ベンチマーク->3379点
10時にコンテストが開始して、自分は環境構築(ssh接続やMakefile編集)、他2人がマニュアルを読んだり、DBのスキーマを覗いたりでそれぞれ作業しました。10:40ごろにひとまずMakefileの編集が終わり、ベンチマークも正常に回せることを確認。githubにpushし、alpやslow-query,マニュアルの内容などを共有して、各自でチューニング作業を行いました。
12:31 インデックスを貼る->4172点(auaua)
初動でのalpやslow-queryを見て、必要そうなところにインデックスを貼ってくれました。
13:44 Redisでアイコン画像をキャッシュ->9,316(kentakunkentakun)
アイコンの取得に時間がかかっていることをalpで確認できたので、早い段階でRedisでキャッシュさせようという方針が立ちました。画像の枚数も見た限りだと100枚程度だったので、おそらくメモリは足りるだろうということでRedisのimportやキャッシュ処理の実装を彼にやってもらいました。
15:22 インデックス追加->11469点(auaua,kentakunkentakun)
中盤からボトルネックになりだしたライブストリーム関連のDBにインデックスを貼ってくれました。
16:41 getUserStatisticsにあるN+1を部分的に改善->13978(saity)
15:00あたりでユーザ配信統計情報とライブ配信統計情報の2つの処理がボトルネックになっており、そのうちのユーザ配信統計情報の方にN+1が複数あったのを確認したのでその改善をしました。Go言語の実装力が低いせいですべてのN+1を解決できませんでしたが、結果的にスコアは少し伸びました。
17:29 DBサーバーとAppサーバーの分割->21794(auaua,saity)
17:00時点でGo言語実装での改善は終わらせることにして、DBとAppの分割を共同で行いました。
17:53 log出力の停止->22558
nginxのaccess_logとslow-queryのログを吐き出さないようにしました。
17:58 最終ベンチ ->22284
その他の改善
13:41 fillLivestreamResponseのN+1改善(saity)
序盤でボトルネックになるエンドポイントの関数中にN+1の処理が入っていたので改善しました。しかし、実装した後でボトルネックがアイコン画像の取得であることを知ったため、どの程度スコア向上に寄与しているかが不明でした。Redisキャッシュの改善をマージしてベンチを回す時に、ここの改善も一緒に反映された可能性があります。
17:54 ReactionsやStatistics周りのデータをオンメモリで持たせる。(kentakunkentakun)
詳しいことは聞いてませんが、ライブ配信の統計情報のデータをRedisで持たせて、煩雑な処理を改善しようとしていました。終了5分前ぐらいにベンチマークを回しましたが、failedになってしまったので、変更を反映させることができませんでした。
結果
スコアのみの順位⇨
最終結果⇨
結果は570位中108位という結果でした。ですが、追試も含めたらそもそもランキング外のようなものなので複雑な気分です。実は今回のコンテストでは、Indexを貼るにあたってベンチマークを回す前にインデックスを正常に設定するためにDBの初期化を行うという追加の処理が必要となっていました。再起動試験の失敗はその処理のし忘れが原因でした。これは単純に全員そこまで気が回らなかったというのが原因ではあります。終了3分前ぐらいにリマインドタイマーを設定するぐらいの対策をするべきでした。
感想、反省
個人的には初参加にしてはある程度戦えているようなスコアを出せていて結構嬉しいです。チームメンバーともオフラインでわいわいやりながら開発できて楽しかったです。
ですが、反省すべき点もいくつかあったので、それらについて詳しくまとめておこうと思います。
アイコン画像配信は条件に応じて304を返せばよい。
これはアプリケーションマニュアルに書いてあって、全員見落としていました(ちなみに自分はアプリケーションマニュアル自体を見ていませんでした…)。内容としては、ユーザーがすでにサーバー上にある画像と同じものを保持している場合(画像のハッシュ値が一致している場合)は、画像を返すかわりに304を返せば良いという仕様でした。これによって無駄なRDBへのアクセスを低減させることができたのです。 結果的にはRedisのキャッシュでもボトルネックの解消はできたようですが、これに気づいていればもう少し楽に改善できていたと思います。(Redisでの実装というのもあまり良い方針ではなかった気がします。これは自分の判断ミスです。)
DNSに全く手を付けなかった。
今回のコンテストの肝はおそらくDNSの名前解決の効率化なのですが、私達はalpやslow-queryを見て、典型的な問題を解消することのみしか考えていませんでした。マニュアルを読めば、ある程度重要な要素であることは把握できたはずなのでしっかりと読むべきでした。
N+1の改善が遅すぎた。
N+1の改善は全て自分が担当しました。N+1の改善を2つ解消するのに4,5時間もかけてしまいました。これは自分のGoの知識のなさや実装力の低さに起因するものであり、自分が単純に力量不足でした。これをもっとスピーディーにこなしていれば、他のボトルネックを解消するための時間をもっと確保できたと思います。
細かいミスで時間ロスすることが多かった。
makefileの不具合、alpのエンドポイント誤設定、運営からのベンチマーカー不具合報告の確認漏れ、DB初期化をベンチマーク前にすることの共有漏れなどがありました。特にDB初期化において自分はその処理のことを知らなかったので、実際に聞くまでに20分近くを無駄にしました。もっと早く聞くべきでした。実装面での間違いはしょうがないことですが、何かしらの不具合が起こった時にはメンバーにその情報を共有したり、相談したりすることは大事だと感じました。
今後の目標と課題
今回のisucon13は予選がなく、全員が本選に出場できるという異例の回でした。今後もこのような方針で開催されるかどうかはわかりませんが、次回は予選通過のラインである30位圏内に入りたいと、今回のコンテストに参加して強く思いました。できることならまた同じチームでわいわいしながらやりたいです笑。今回はインフラの知識やgolangの実装力、sqlの読解力のようなテクニカルな面だけでなく、確認ミスの頻発や集中力のなさが目立ちました。次までにはそのあたりを改善して、もう一度チャレンジしたいと思っています。