Isucon12 参加記

2022年7月24日,ISUCON 12に参加しました.結果は予選敗退でした.本記事はその参加記です.

はじめに

本番前の練習を含め本コンテストを通して,Webアプリケーションのボトルネックの発見や様々なチューニング方法を知ることができたほか,Go言語を楽しく勉強することができました. 本コンテストを開催してくださったISUCON運営関係者の方々に感謝いたします.また,一ヶ月以上前から練習に付き合ってくれたチームメイトである@_u017 ,@donkooo00 に感謝いたします.そして,過去のISUCON参加記を書かれた方々含めコミュニティに感謝いたします. 

競技に使用したリポジトリ

チームメンバーと役割

チームメンバー全員がISUCON初参加,また,自分含めチームメンバー2人がGo初心者でした. そこで,役割分担としては各々勉強したい分野を担当しました.また,役割分担は厳密ではなく,当日は各々空いた人が必要なことをやっていました.

普段は強化学習の研究をしています. コードのGit管理のための準備,アプリケーションを担当していました.

@_u017

普段はデータマイニングの研究としてプログラムの識別子名推薦をしています. nginx周りや複数台構成への拡張.アプリケーションを担当していました.

@donkooo00

普段はスパコン関連の研究をしています.唯一のGo経験者. 本番前は解析ツールやdeployスクリプトを用意するためのalpの準備,本番中はアプリケーションを担当していました.

本番当日までにやったこと

本番一週間前までは,毎週4時間ほど過去問をみんなで解いていました.一週間前からは本番時と同じく8時間という制限時間のもと本番を想定して練習していました.それ以外は各自が各々勉強していました.

本番当日

使用した解析ツール

  • htop: メモリがどれぐらい使われているか,どのプロセスが重いかを見る時に使っていました.
  • netdat: htopではわからない情報(ネットワーク周りなど)を見る時に使っていました.
  • alp: どのエンドポイントが重いかを見る時に使っていました.基本的にここを見ながらボトルネックを発見していました.
  • pt-query-digest: どのSQLクエリが重いかを見る時に使っていました.
  • pprof: エンドポイントの中でどこが重いかを見るときに使っていました.しかし,本番は諸々の事情で使うことができませんでした.

開発方法

各々のブランチ上で各自作業をし,本番環境でpullしてベンチをとり,ボトルネックが解消されればmainにマージ,という流れを取りました. 基本的にローカル環境ではアプリケーションは動かさず,SQLクエリ関連のデバッグをするために,MySQLだけはローカルに持ってきていました.

本番当日の流れ

開始直後

開始直後は@u017がインスタンス作成,@donkooo00と僕がマニュアル読みをしていました.インスタンスが立ち上がったあとは,@u017はalpの流し込み,僕はGIt管理のための諸々の設定をしていました.

10:20

各々上記の作業が終わったあとは,@_u017はnginx confの設定,@donkooo00がdeployスクリプトの書き換え,僕はMySQL関連のボトルネックの洗い出しやINSERTやUPDATEが行われているテーブルの確認,ローカル環境でMySQLを実行できるようにしていました.

11:00

開始直後に行うことの一通りの作業を終え,ベンチの結果,整形後のalpの出力が出ました.この時,テナントごとにsqliteを毎回connectするのは遅そうという話をしていました.そこで,sqliteからMySQLへ移植するという話がでました.しかし,自分達はsqliteからMySQLへの移植をしたことがなく,また,どのやり方が良いかを考える必要もあり,競技時間中に終わらない気がしたため,@donkooo00がsqliteからMySQLへの移植作業,自分は移植作業が失敗した時を考え,N+1などの自明なボトルネックの解消をすることにしました.

competitionRankingHandlerのN+1の解消

@_u017がcompetitionRankingHandlerに存在するN+1の解消をしました. スコアは3857.

indexの追加

indexの追加をしました.このとき,mysqと同様の方法でindexを追加しようとしたため,エラーが出続けていました.SQLはDBによって微妙に構文が違うことは知っていたのですが,それに気づくことができず余計に時間を費やしてしまったのは勿体なかったです.スコアは4000.

playerHandlerのN+1の解消

@_u017のコードを引き継ぎ,playerHandlerのN+1の解消をしました.JOINを使用してN回呼ばれるクエリの結果を結合したものを1回のクエリでとってくることが理想でしたが,練習時にサブクエリが必要になってくるようなJOINに多くの時間を費やしてしまったことを考え,ひとまずin memory cacheを使用して解消しました.スコアは4418.

competitionScoreHandlerのbulk insert化

competitionScoreHandlerにて,INSERTを複数回呼ぶようになっていたので,bulk insertするように変更しました.スコアは6886.ここら辺はテナントごとのqueueを使うことでさらに高速化ができそうで,書こうとしたのですが時間が足りずできませんでした.

14:00

ここら辺でsqliteMySQLへ移植することが難しそうということから@donkooo00も自明なボトルネックの解消をすることになりました.

playersAddHandlerの改善

playersAddHandlerのinsertも複数回呼ばれていたため,まとめて行うようにしました.その流れでN+1は自然に解消されました.スコアは8242.

2台構成

@_u017がapp x1,db x1の2台構成を試しました.スコアは10674.ここまでが15時までにできた作業でこの時点では割といい順位にいた気がします.ただし,この作業は現段階ではmergeせず,ひとまず1台構成で開発を進め,最後に複数台構成に変更しました.

competitionScoreHandlerのN+1の解消

competitoinScoreHandlerのN+1を解消しました.スコアは8601 .

billing内のクエリにLIMITをつける

@donkooo00がbilling内のクエリにLIMITをつけました.元の実装では,billing関数では上位11件のデータしか使われていないにもかかわらず,全ての行を取り出していました.スコアは計測忘れ

competitionRankingHandlerのin memory cache化

終了したcompetitionのランキングは変わらないと思ったため,終了したコンペティションのみメモリ上にキャッシュしました.しかし,別の箇所でcriticalなエラーが出て途中でベンチが終了しスコアが少し下がったため,mergeしませんでした.

playerHandlerのin memory cache化

tenantからdisqualifiedされたユーザー情報をメモリ上にキャッシュしました.スコアは8888.

終了作業

ログを切ったり,netdatを止めたり,複数台構成化をしたのちに再起動試験をしました.ここら辺から特にコードを変えていないのにベンチのスコアが安定しなくなるという問題が発生し,かなり焦りました.本来はApp x1,DB x2の構成をしたかったのですが,DBを複数サーバーに分けるのが難しく,最終的にApp x1,DB x1の構成にしました.最終スコアが10795

良かった点と反省点

良かった点

まず最初に良かった点としては,Goの構文周りの実装に詰まったらメンバーにすぐに聞くようにしたため,余計な沼にはまるということをなくすことができました. また,開始作業や終了作業などは特に大きな問題はなく,練習の成果が出せたと思います.

反省点

次に反省点としては,自分の実装が遅く,比較的自明なボトルネックの改善が全て終わらなかったことです. 特にN+1の解消に関しては,コードをある程度よく理解しないと効率よく解消することが難しく,コードリーディング力不足が出てしまっていたと感じました. ただし,自明な改善ができれば予選突破ラインぐらいには到達しそうだったため,かなり悔しいです.

また,チームメンバーがtenantsBillingHandlerのN+1の解消にずっと詰まってしまっていた時に手伝えなかったことです. 後から確認したところ自分が他の箇所で解消したN+1と同じような感じで解消することができることがわかり,ここら辺の情報共有をもっとしっかり行うべきでした.

終わりに

結果は予選敗退と悔しい結果になってしまいました. これは単純に自分の実力不足が原因であることを表しているため,今後はさらに精進していきたいと思います. また,今回はISUCON周りにGoにしか触れなかったため,今後はもっと広くGoを触っていきたいと思います.