ISUCON11予選を学生枠で通過してしまった

こんにちはdas08です。 大学に入ってからの目標の1つであったISUCONに参加してきました!
ISUCONは8時間で改善の余地のあるWebアプリをチューニングして高速化しスコアを競う大会です。

同学科の友人2人とチームを組んでどこまでできるか腕試しがてらの挑戦です。備忘録として当日の様子を残そうと思います。

チームメンバー

  • das08 (電気電子工学科3回生) インフラ周りの改善担当
  • tinaxd (同上) Webアプリの改善担当
  • arakistic (同上) DB周りの改善

メンバー全員同じ学科でチームを組みました。暇なときよくApexを一緒にするのでチーム名は「快適PandApex」になりました。もう少しマシな名前にすればよかったです。
役割分担も大まかに決めて予選に挑みました。過去の予選問題をGoで何問かといて対策もしてたので本番もGoで挑むことになりました。

予選当日

8:00 起床

競技開始自体は10時からでしたが対面でやりたかったのでチームメイトの家に向かうため早めに起きました。結論から言うと直接コミュニケーションが取れたのでコーディングがスムーズに行きました。

10:00 競技開始

公式YouTube Liveで予選問題の紹介があった後競技が開始されました。テーマは「Isu Condition」らしく,イスの状態を管理するWebアプリをいい感じに高速化してねとのことです。

チューニングを始める前にツールなどを導入しました。導入したものは以下のとおりです。

  • git-prompt (コマンドラインにgitのbranchを表示するやつ)
  • MySQL slow query log
  • pprof
  • phpMyAdmin
    正直高機能なデバッグツールは使いこなせる自信がなかったので導入を見送りました。

10:30 初回ベンチ(Score: 1636)

まずはということでプロファイラやログ出力を設定した後1回目のベンチを回しました。
このときのDBのログからまずIndex周りとN+1クエリの改善をしようという話になりました。

11:00 DESCにIndexを貼った(Score: 14184)

一気にスコアが跳ねました。SELECT * ... ORDER BY timestamp DESC を実行しているところがあったのでtinaxがここにindexを貼ることになりました。 MySQL5系ではDESCのIndexが貼れないことがわかっていたのでGenerated Columnを使って負のtimestampを生成しASCでIndexを貼れるように持ち込みました。
だいぶ効果があったみたいで瞬間的に上位に浮上して嬉しかったです。

11:15 SELECT ALLを改善(Score: 16226)

arakisticとtinaxがスロークエリ見てSELECTをALLから必要最低限にするよう改善してました。よくわかってないけどスコアが2000くらい増えました。

11:30 GROUP BY句の改善(Score: 17374)

Goで発行されているSQLクエリを眺めていたらGROUP BYでカテゴリを取っている感じのものを見つけたのでDISTINCT句に置換しました。 Stackoverflowによると700倍くらい速度変わるらしいので気づけてよかったです。

11:40 Bulk INSERTの実装(Score: 22280)

スロークエリを見ていたら明らかにループしてINSERTしてるクエリがあったので過去問で実装したことがあるtinaxにお願いしてBulk INSERTしてもらった。 これもかなり効いてきて一気に5000点近く上がりました。このとき学生トップ,全体では11位になって舞い上がっていました(笑)

12:00 N+1クエリの改善(Score: 25386)

またGoのSQLクエリを眺めていたら典型的なN+1を見つけたので改善することにしました。 ついでにMySQLのconnection数やpoolサイズとかもいじったりしてました。

13:00 conditionの正規化

conditionがカンマで連結されたものがテーブルに入っており明らかに改善してほしそうだったのでtinaxが改善することに。 その間にarakisticがワーカーの数やリクエストを落とすパラメータを弄ってました。

14:00 停滞期(Score: 0)

正規化がうまく行かず,Generated Columnに置き換えようという話になりましたがベンチでFailが続いてしまい停滞気味に。 お昼の戦略会議で停滞が続いたらAppとDBを分けようという話をしていたのでそろそろ分離作業に入ることにしました。
1時間半くらいスコア0(Fail)が続きDBのスキーマもいじっていたため他の改善もできなかったのでNginxのCache-Controlの設定をしていました。

14:30 DBをServer#2に分離(Score: 36067)

とりあえずMySQLを2台目のサーバーに分離することに。以前練習していたのでHostの変更や3306番Portの開放(予選失格になりかけた)を行いました。 MySQLの設定やパーミッションの変更などをして無事に分離成功。 スコアが一気に上がり再び上位に食い込めました。

14:35 Server#2 のDBのチューニング(Score: 37063)

Appと分離したことでメモリに余裕ができたのでbufferの設定などを再度行いました。arakisticと相談してinnodb_buffer_pool_sizeを2GBくらいにするのがベストと判断しました。 スコアは若干伸びましたが上振れしただけかもしれないです。 このあたりから5万点いけるんじゃねと夢の大台が現実的になってきました。

15:00 Server#3 にAppを振り分け(Score: 50499)

今回の予選では3台のサーバーが使えたのでもう1台をどうしようか話し合いました。スロークエリやcpu使用率を見て総合した結果Appサーバーをもう1台に割り当てようという話になりました。 (あと純粋にDBを垂直分割するのが大変そうだったので...)
nginxのロードバランサーで行けそうだったのでtinaxと相談しながらnginx.confをいじりupstreamでServer#1にきたアクセスをServer#3に振り分ける処理を書きました。 意外とかんたんに実装できて無事アクセスが振り分けられてることを確認したのでデプロイ。またスコアが跳ねて10位台に乗りました。

15:15 gzipの実装(Score: 46476)

ベンチからのリクエストをみるとgzipを受け付けてくれていたのでnginxでgzip圧縮をかけることにしました。 色々いじりましたがどうしてもスコアが伸びず断念。推測ですがgzipの際CPUのリソースやメモリを食ったのが原因な気がします。

15:30 IsuGraphまわりの改善

ベンチの結果を見るとGraphの生成がうまくできておらずWorstのものしか達成できていない事に気づきこれを治すことになりました。 コードフリーズは17:00と決めていたので最後の改善になるかなと言う感じでした。

16:30 停滞期2(Score: 14245-48460)

dropProbabilityを0に近づけるとWorstは減るが処理が増えるなど板挟みにあいスコアが停滞しました。
Graphの処理がなんとなくわかってきたもののうまいこと改善できず,ベンチもFailすることが増えました。 せめてもの改善ということでtinaxがGraph生成日時前後の値をSELECTするように修正。

17:00 コードフリーズ

学科の先輩のp1assさんのブログでコードフリーズと再起動試験が大事だと学んだので17:00にコードフリーズしました。 AWSコンソールからrebootをかけてベンチが通ることを確認して手動rebootに切り替えようとしたところISUCON Portalがダウンしてしまった... エラーでベンチが実行できなくなったのでとりあえずsystemdの設定を確認することに。
このときレギュレーションを再読していたところインスタンスのセキュリティルールを変えては行けないとの記載が。MySQLを分離するときやphpMyAdminの導入時にポート開放していたので慌てて修正しました。
マニュアル軽視、ダメ、ゼッタイ。

18:45 競技延長&&ログ出力オフ(Score: 63544)

待機していると運営から45分の延長が宣言されたので再起動試験を再開。導入したツールやログ出力を完全に切り,スコアが一番高かった状態までrevertして最大限のパフォーマンスを出せるようにしました。
その結果過去最高の6万点を突破!めちゃくちゃ嬉しかったです。追試で落ちないようにと祈りながら再起動を数パターン繰り返して競技終了。

スコアの推移

こんな感じでした。
運営による追試の最終スコアは61586でした。

Scoreの推移

ちなみに学生6位(うち2チームは一般枠なので学生枠では4位)です。

学生枠で4位

感想

初めてのISUCONでしたがまさかの予選通過で嬉しかったです。 やはり過去問を解いて改善ポイントを勉強し,チームで相談しながら分担して実装できたのが高スコアにつながった気がします。
社会人チームの数十万点には今は到底及びませんが本戦までに練習を重ねて優勝を目指したいと思います!