ISUCON7の予選を突破しました!
ISUCON6からはや一年…
今年も去年と同じメンバー(@mot, @syusui)で「チーム新卒」という名前で参加して、めでたく予選を突破できました!! この記事では自分が主にやったことを書こうと思います。 ちなみに、ソースコードはgitlabにあげてます。 gitlab.com
全体の流れについては@motさんの記事を見てください。 programmermot.hatenablog.com
始まった直後
今回もチームメンバー全員が使い慣れているrubyを使うことにして、まずはstackprofとリクエストごとにSQLのログを取れるお手製ツールを仕込んでボトルネックを探しました。
出力結果を見てみると/fetchや/messageにN+1があったり、画像をDBから取得する処理が遅いことに気が付きました。 画像周りは@syusuiくんに任せて、自分は/fetchや/messageのN+1を改善していきました。
アプリケーション側の改善
/fetchのN+1は、チャンネルごとの既読情報の取得と未読メッセージ数をカウントするというものでした。 プロファイラを見たところ、既読情報の取得と、既読情報が一つもない場合の未読メッセージ数(そのチャンネルのメッセージの総数)のカウントが多く呼ばれていました。なのでとりあえず既読情報がある場合の未読メッセージ数のカウントはN+1を残したまま、ほかの部分のN+1を改善しました。
あと、謎のsleep 1.0
を見て「なんだこのsleepは!?」となりましたが、レギュレーションを読んでいなかった自分は深く考えもせず消してしまいました。(レギュレーションはちゃんと読みましょう)
その後にMySQLでtoo many connectionsが出ると@motさんに言われたので調べていました。 今までの問題をやっててこんなことはなかったよなーと去年の予選の問題と見比べてみたところ、去年の問題ではThread.currentを使用してスレッド内でDBのコネクションを共有していましたが、今年の問題ではリクエストごとにコネクションを張るようになっていたことに気が付きました。過去問大事ですね。
/messageのN+1は単純だったのでサクッと直しました。 また、ベンチ回してると/historyも遅いと表示されたので見てみると、似たようなN+1があったので直してました。
DBの最適化
@syusuiくんが画像をローカルに保存する変更を加えたり、@motさんがnginxの設定を追えて10万点を超えたあたりで、改めてプロファイリングやDBのスロークエリを見ました。 プロファイリングの結果を見ると明らかにクエリの実行に時間がかかっていて、スロークエリの箇所と一致していました。
スロークエリはmessageのテーブルで発生していて、EXPLAINを実行してみるとindexが効いていませんでした。 クエリではchannel_idとidを使っていましたが、idにしかindexがなかったのでchannel_idとidの複合indexを作成しました。 これで20万点近くまで点数が上がりました。
もう一度スロークエリを見てみると、チャンネルのメッセージの総数のカウントに時間がかかっていることに気が付きました。 メッセージはどんどん増えていくので、総数のカウントにかかる時間もどんどん増えていきます。
だいぶ時間も迫っていてredisを入れたくなかったので、カウンターキャッシュを実装することにしました。 新しくmessage_countというテーブルを作成して、メッセージの追加のたびにチャンネルごとにメッセージ数をインクリメントするように変更しました。 また、何度ベンチマークを回しても大丈夫なように/initializeが呼ばれたときにリセットする機能も実装しました。 これで25万点を超え、最終的には268,588点で1日目の2位になることができました!
感想
今回のISUCONの予選の問題が複数台構成だったのでびっくりしましたが、手も足も出なかったISUCON 6本戦での複数台構成のリベンジができてよかったです。 アプリケーション側のボトルネックは事前に練習していたpixiv社内ISUCONと似ていたので、だいぶスムーズに進めれた気がします。
今回は全然レギュレーションを読んでいなくて、あとから/fetchや/messageの得点について知ったので、ちゃんとレギュレーションは読まないとダメだということを再認識しました。もしちゃんと読んでてsleepを残すという考えがあればもう少し点数が伸びていたのかも?
今回のISUCONの個人的な目標は一般枠で予選通過して本戦に出ることだったので、目標が達成できて本当によかったです! 本戦に出たからには何かしらの功績を残せるように頑張りたいです!