ISUCON11予選に参加した

どうもyapattaです.

今年もISUCON11予選参加記RTAを目指す.

ISUCONとは?

お題となるWebサービスを決められたレギュレーションの中で限界まで高速化を図るチューニングバトル.

Iikanjini Speed Up Contestの略らしい.

ISUCON11のまとめ↓, ここからISUCON11の欲しい情報は見つかりそう.

isucon.net

今回は椅子の状態を管理するウェブアプリ「ISUCONDITION」のパフォーマンスを最適化するとか.

チーム

去年のチームは解散して、チームメイトとは本戦で会おう!とお互いに誓いあった.

今年は190ikpとnkpoidと新たにチームを結成し参加した.

チーム名は「ハードウェア自作から始めるISUCONチューニング」. 因みにハードウェアからチューニングはできなかった.

使用言語はGoだ.

役割分担

190ikpがインフラ全般のチューニング, yapattaとnkpoidがDBとアプリケーションのチューニング担当だ.

終始, 二人におんぶにだっこしてしまった...(申し訳無さとありがたさ)

事前準備

事前準備ではデプロイ, パフォーマンス解析, ソースコード管理における自動化を行った.

ローカルで変更(MySQL, Nginxのconf, アプリケーションコード変更, SQLの初期化関連のスクリプトなど)を修正した場合, deployスクリプトを用いてrsyncでリモートサーバに流すという大まかな方針.

アプリケーションコード, SQLの初期化関連のファイル, ミドルウェアのconf, デプロイもしくは解析用のスクリプトは全てプライベートレポジトリで管理するようにした.

こだわり点として,

  • ローカルでGo1.17を利用してビルド, バイナリをリモートサーバに配布(少しでもパフォーマンスを上げたかった).

  • デプロイが完了したらWebPushによってdiscordで通知が確認されるように

  • アプリケーション側の律速を見るためにpprofを利用

  • slow queryもしくはnginxのログの確認はスクリプトを叩いてリモートサーバからrsyncで取ってくるように

  • dev環境と本番環境で流すconfを変えることをスクリプトの引数で実現(ログの出力の有無など)

これにより快適なデプロイライフを歩めた気がする

本番

残念ながらスコアがどのぐらい上がったかが所々記憶からすり抜けてしまい, ただやったことぐらいは書けそうなので書いていく.

皆無事起床に成功. 最重要難関をクリアした.

今年は予定通り10時に開催されたみたいだ.

本番開始後, 190ikpがCloudFormationでサーバ構築, 自分とnkpoidがとりあえずアプリケーション仕様だったりの必要そうなドキュメントを眺める.

そしてサーバ構築が取り敢えず終わったのでベンチマークを走らせる.

その後自分はMariaDB用にmy.cnfを書き換え, その間にnkpoidがアプリケーション用のソースコードを読む.

バイナリログを無効にする書き方がskip-log-binだったりと所々違っていた.

nkpoidがgetTrendでN+1を見つけたためそれを修正することに.

自分はmy.cnfを書き換えながら, UPDATEやINSERTの後に別トランザクションでSELECTが無かったら, transaction-isolationREAD-UNCOMITTEDでも大丈夫じゃねとか言っていた(最終的に一番READ-COMITTEDが安定).

あと, ADMIN PREPARE対策でinterpolateParams=trueを追加した.

   dsn := fmt.Sprintf("%v:%v@tcp(%v:%v)/%v?interpolateParams=true&parseTime=true&loc=Asia%%2FTokyo", mc.User, mc.Password, mc.Host, mc.Port, mc.DBName)

190ikpがdeployとanalyze用スクリプトをええ感じに動かせるようにし, 用意したmy.cnfとnginx.confだったりを適用してdeployスクリプトを回す(この時点ではサーバ一台のみで実行).

取得ログとpprofの結果から, アプリケーションの律速はgetTrendとpostIsuConditionだとわかる.

引き続きnkpoidがgetTrendを最適化し, 自分がpostIsuConditionを最適化する.

postIsuConditionではまずログ出力がボトルネックになっていたため, ログを切るように.

   log.SetOutput(ioutil.Discard)
    e := echo.New()
    e.Logger.SetLevel(log.OFF)

    e.Use(middleware.Recover())

次にInsertがN回別々に行われているため, そこをBulk Insertにしたら速くなるのでは?ということでBulk Insertを実装しようとする.

そんなことをしている間にnkpoidがN+1を解決. スコア10000を超えた気がする.

その後sqlxでBulk Insertがうまく書けず悩んでいたところ, nkpoidに介抱(やってもらった)してもらい実装.

190ikpにはnginx用のconfを書いてもらい, http2用の設定を適用してもらっていた(SSL使っていたため).

そういえばマシンスペックは3台全て同じであった. 一度サーバ1でプロキシとアプリケーション, サーバ3でMariaDBを動かすように構成を変更.

アプリケーション側のボトルネックを大分解消できたため, DBの設定を見直す. DBのメモリが張り付いていなかったためinnodb_buffer_pool_size=3Gと設定(メモリ3.5GiBほどあったため), またinnodb_log_file_size=512Mに(これより上げると何故か落ちた).

さらにlimits.confLimitNOFILE=65536に. 扱えるファイルディスクリプタの数を増やした.

このときスコアが15,000を超えた気がする.

アプリケーション実行サーバでCPUが張り付くようになったため, サーバ2にアプリケーションサーバを建ててロードバランシングを試すように.

ロードバランシングを開始してから非自明にscoreが0になり, うーんと悩ます.

ロードバランシングをやめてアプリケーション一台構成にしたら動作するがいまいちスコアが伸びず(何なら下がった).

アプリケーション2台持ちの状態で, DBのコミットレベルを変えたり, nginxにおけるhttp2のkeepalive_timeoutを変更し問題を見つけ出そうとしたがスコアが0のままに.

その間にorder byでDESCしているクエリが存在するから, Descending Indexesを貼れば速くなるんじゃねとインデックスを貼り貼りしていた.

最終的に課題解決ができず, nkpoidがコミットハッシュを大分昔に戻して, N+1修正前にまで戻した状態でインフラのcnfを最適化してデプロイしたら, スコアが25,000を超えた.

いらないserviceは尽く止めたし, さらにjournaldまで止めたのが効いたのか??

スコアがでなくなるコミットを確認するために履歴をたどっていったが最後問題究明ができないままスコアが0であった. nkpoidがとっさの機転で最大スコアのコミット状態をデプロイしてコンテストが終了した.

Failにならないのを祈るばかりだ.

狐につままれたような終わり方をしてしまった.

どこのせいで動いていないか理由を究明できないまま別の修正に手を加えようとしていた自分の態度に反省.

そしてチームメイト本当にありがとう, 今日も今日とてお世話になりました.