😺

ISUCON12本選参加記

2022/08/31に公開約4,800字

前回予選を通過したので、8/27(土)のISUCON12本選に参加しました。

https://zenn.dev/dice801/articles/760f8fc6af5a08

当初8/28(日)に発表された結果は追試で正常動作せず失格判定でしたが、紆余曲折あり、再追試と判定見直し(失格の取り消し)を行っていただきました。
運営の方には再追試の作業等、大変お手数をおかけしたなと思っています。
というか必死になって色々聞いてしまいホント済みません、という気持ちです。
ありがとうございました。

失格の取り消しも含む最終結果は以下に公開されています。

競技中のベストスコアは143,194点でしたが、競技時間後の計測で126,837点とやや下振れし、全体11位で終了しました。(失格取り消し後の順位)

ダッシュボードが凍結され他チームのスコアを見ることができなくなった17:00時点で、まあ賞金圏内の入賞は難しいなという気持ちでいました。しかし、終盤にMySQLのシャーディングが成功してスケールアウトでみるみるスコアが増えていった時はそれなりに脳汁出てました。楽しかったです。

チームメンバーや事前準備、基本方針は予選の時と違いがないため割愛します。
言語も予選と同様にRust一択でした。

予選ではやらなかったが本選では事前にやった唯一のことは、MySQLTuner使う準備くらいです。
一方でチームメイトはガシガシとツール類のアップデートをしてくれていました。ありがたいことです。

当日やったこと

チームで主にやったこと

  1. MySQLのシャーディング
  2. インデックス追加
  3. N+1の修正
  4. MySQLとNginxのチューニング

こう書くと割と普通な感じですが、1.を実現できたのがやはり良かったなと。

最終構成

配られた5台には、is1からis5までのホスト名が付与されています。
スペックは全部CPU 2コア、メモリ4GBだった筈。

  • 役割
    • is1 : Nginx, App
    • is2 : App (is1と均等バランシング)
    • is3 : MySQL
    • is4 : MySQL
    • is5 : MySQL
  • AppからMySQLへの接続は、整数値であるリクエストのユーザーIDをMySQLの台数で除算して決定する。
  • 初期化処理はAppがMySQLノード全てに並列でssh接続してinit.shを実行する。
  • init.shは127.0.0.1のMySQLに繋いでデータを初期化する。

当日の推移

自分でやってないことは殆ど書いてないです。
Discordのログと、commitの履歴と、あとベンチマークのスコアの履歴から類推して書いており間違いがあるかも知れません。

  • 8:45
    • 予選と同様にOpenTelemetryで使うつもりだったJaegerサーバの準備を忘れていたことに気付いて慌てる。
  • 9:03
    • Jaegerセットアップ完了。
  • 9:40
    • YouTubeを眺める。
  • 10:00
    • 競技開始。とりあえずそのままベンチマーク。(575点)
  • 10:09
    • 設定ファイル類をgit管理下に。初期データの.sqlファイルがそこそこ巨大でちょっと悩む。
  • 10:18
    • Rustに切り替えた状態で初回ベンチマーク。(703点)
  • 10:32
    • APM有効にした上でベンチマーク。(203点)
  • 10:42
    • モニタリングの準備が一通り整う。
    • netdataを参照するためのポートを公開できなさそうだったので、NginxでProxyするConfigを即興で入れた。
  • 10:50
    • なかなかテーブル多いなとER図を作って眺めていた。
  • 10:57
    • MySQLのパラメータ変更。InnoDB Buffer Pool Sizeを脳死で2GBにした。(412点)
  • 11:38
    • インデックス少し追加と、N+1の修正ひとつ目。(7,035点)
    • この頃すでにトップは2万点超えていて、インデックス足さないとなーという会話をしていた気がする。
  • 11:46
    • 一部の言語だと最初からadmin用のフロントが正常に動作しないため、修正版が配布された。
    • ようやく管理画面にログインしての操作ができる。
  • 11:50
    • いくつかインデックス追加。(11,420点)
  • 12:25
    • 明らかにDBネックだったので、App 1台とDB 1台の計2台に分離して計測することにする。(20,272点)
    • ここから約4時間、1万点から2万点の間でずっと伸びない苦しい時間。
    • ベンチマーク時に謎のステータス500が多発し減点になっているが、それがなぜ起きるのか追及できずにいた。
  • 12:50
    • MySQLの分離レベルをREPEATABLE-READからREAD-COMMITEDに変更。
    • 音声通話の結果、READ-UNCOMMITEDと誤認しかけた。危ない。
  • 12:56
    • MySQLのbinlogを出力しないようにする。(21,446点)
  • 13:56
    • DBを分割できないと無理そうという判断の元、割り方を考え始める。この時点ではテーブル単位で分割することを考えていた。そのための実装をチームメイトに丸投げする。
  • 14:26
    • MySQLのWriteがきつそうだったので、innodb_flush_log_at_trx_commit = 2 にする。(21,579点)
  • 14:33
    • Appを2台にして均等バランシング。DBは1台。(21,329点)
    • この時はどちらかと言うとAppをスケールアウトさせる上での課題の有無を確認するモチベーション。設定ファイルの反映漏れでベンチのFailを繰り返し、やや時間を溶かしたので反省。
  • 15:52
    • Appから複数のMySQLに接続できるようにする改修が完了。
  • 16:00
    • 作戦を考える。
    • テーブル単位ではなくユーザーID単位でDBを分割できそうだし、ゲームという特性を考えればそうすべきだろうという方針。
    • 一方でadmin機能を全然読んでなく、マスターは更新されないものだと思っていたら全くそんなことないと気付き慌てる。(1時間位で修正していた筈)
  • 16:06
    • NginxでリクエストをユーザーID毎によってバランシングすることを考えていた。サンプルConfigが残っている。ステータスコードそんな風に使ってデバッグするのかと、チームメイトから妙に感心された。POST /loginとPOST /userの扱いが難しいことからAppでユーザーIDから接続先DBを決定する方針にしたため、このConfigはお蔵入り。
      location /user {
        if ($uri ~ "^/user/\d*[0-3]/" ) {
          return 200;
        }
        if ($uri ~ "^/user/\d*[4-6]/" ) {
          return 201;
        }
        if ($uri ~ "^/user/\d*[7-9]/" ) {
          return 202;
        }
      }
      
  • 16:23
    • 何の気なしにNginxのエラーログを見たら、Too many open filesが頻出していることにやっと気付き、worker_rlimit_nofile = 8,192 にする。値は適当。(63,284点)
    • 唐突にスコアが3倍に跳ねたので、この時少し茫然としてしまった。
    • 謎の500エラーの原因がわかり、ログは見るものだなと改めて。
  • 16:43
    • DBを分割した際の初期化処理はSSH経由でinit.shを実行することにし、鍵を作って各ノードに配る。
  • 17:00
    • /initialize後にAppが過負荷になりSSHできなくなる。しばらく待ったら復活してdmesgにはoom-killが記録されていたので、何かバグってたんだと思うが追及していない。
  • 17:33
    • ユーザーID単位の修正が一通り完成。App 2台 + DB 2台構成。(101,647点)
  • 17:40
    • モニタリング停止。actix-webのworker数を3に変更。(132,197点)
  • 17:51
    • OS再起動テストし、App 2台 + DB 3台の最終構成で当日ベストスコア。(143,194点)
    • ここで作業終了にする。いつももっとギリギリまでベンチしてるので、なんだか空き時間ができて不思議な感触だった。
    • もう少し台数の調節をしたい気持ちもあったが、諦めた。

よかった点

  • Nginxのエラーログからボトルネックを発見することができた。本当にあれ見てなかったらどうしようもなく落ち込んでいた筈。
  • 5台使い切ることができた。
  • 5台使い切るためにも、DBをシャーディングするという考えを諦めなかった。実装はチームメイトが何とかしてくれた。
  • 初期化処理をSSH経由でやる案がすっと出た。あれアプリケーションだけで何とかするのしんどかった気がする。
  • MySQLTunerは割と便利だった。

反省点

  • Nginxのエラーログはもっと早く見るべきだった。本当に気が向いて見てみた、という感じなのでこれは本当に反省。
  • マスターバージョンが異なると422が返るという仕様がよく理解できておらず、このゲーム動かんなぁと思って触るのを諦めていた。ゲームが動いているのを初めて見たのは8/29(月)になってから。
  • 毎回、どんなアプリか動かしてみるぞ!と思っているがなんでかあんまり上手くできない。

まとめ

予選に引き続きチームメイトが優秀でした。途中スコアが伸びず辛い時間を過ごしていたものの、挫けずにその時間に積み上げた改善が最終盤で効果を出したなという印象で、どうにかそこに至ることができたと感じられる点が良かったです。

Rust使うの1チームのみでしたが、その初期実装を準備くださっていた運営の方には本当に感謝しています。ありがとうございました。

来年も頑張ります。(まずは予選申し込みから・・・)

Discussion

ログインするとコメントできます