📝

ISUCON12予選参加記

2022/07/26に公開

7/23(土)のISUCON12にチームOという名前で会社の同僚2名と参加して、今年は予選を通過することができました。
何か書かなければなと思い、先ほど初めてZennのアカウントを作ったところです。

チームのスコアは25,598点で、20位でした。当日の推移は下記。

チームメンバー

チーム名: チームO

  • daiju
  • omu
    • 数年前にひとりでISUCON参加した際に自作した作業用のツールを準備してくれた。
    • 参加記
  • Dice801 (自分)

私自身はベースがインフラの人でかつ暫く実務でシステムをほぼ全く触っていないので、基本的に他のふたりを応援することだけしていました。

なおチーム名のOの文字の意味は、実はよく覚えていません。(チームの命名は私)

自身のこれまでのISUCON歴

  • 2018年(ISUCON8)に初参加
    • 予選通過
    • 本選は0点で惨敗
  • 2019年 予選敗退
  • 2020年 予選敗退
  • 2021年 予選敗退
  • 2022年(今回) 20位で予選通過 (最終スコア 25,598点)

概要

基本方針

  • Rustを使う。
  • オンラインで参加。
  • コードレポジトリとしてGitLabを使う。
  • Discordを使う。
  • モニタリングに関しては以下の通り。
    • Metricsはnetdataとdstat
    • MySQLのスロークエリはpt-query-digest
    • Webのアクセスログをkataribeで見る
    • チームメイト謹製のライブラリで、OpenTelemetryデータをJaegerに送信してビジュアライズ

Rustを使った理由はチームメイト2名ともRust大好きだったからですが、私自身は全然経験ない言語でした。事前にrustupだけしてました。

事前準備

  • チームメイトが粛々と休日等使って練習していたが、私自身は本当に何もしていない。
  • cargo makeできるようになっておくよう指示されていたので、一応インストールした。
  • Makefile.toml が作られて、よくある操作が準備されていたので、どう使うかは一応なんとなく理解しようとしていた。

問題の概要

  • マルチテナント型の、社内ISUCONの結果入力を受けリーダーボードに表示させるWebサービス。
  • *.t.isucon.dev というドメインで公開されており、サブドメイン部分の文字列でテナントが特定できる。
    • これ仕事で見たことある設計・・・。
  • 競技開始時点での大きな特徴は、テナント単位のデータがSQLiteでファイルレベルで分割されていること。
    • 負荷試験の走行中にテナントが順次参加してくることで、このSQLiteのファイルは増加していく。
    • こいつとどう向き合うかが今回のポイントではあったと思う。
  • サブドメイン名と紐付くテナントのIDをはじめ、全体を管理するためのデータはMySQLに入っている。(adminのDB)
  • テナント単位でのデータ不整合を防ぐロック機構がコード中に実装されており、実体としてはロック用のファイルを生成することで実現している。
    • 従って単純にAppサーバを複数台並べる構成が難しい。
  • Web(Nginx) + App + DB(MySQL)というところは割と例年通り。
  • サーバは最大で3台まで使える点も例年通り。

当日やったこと

チームで主にやったこと

  • SQLiteのデータはMySQLに移行。
  • Web + Appを分離した2台構成。(1台余剰)
  • id_generator を使うのをやめて、アプリケーションがUUIDを生成する。
  • visit_historyplayer_score の不要なデータを圧縮。
  • N+1を粛々と直す。

最終構成

  • is1, is2, is3は配られた3台のEC2のホスト名。

  • 最初にMySQLに入っていた visit_history は、データを圧縮して visit_history2 という名前に変化。
  • 同じく id_generator は、アプリケーションでUUIDを生成することにしたので不要になった。
  • SQLiteから移行した competition, player, player_score はいずれも初期データのまま init_ という接頭辞を名前に付与したテーブルとして別に持っている。
    • POST /initialize の時に一度 competition, player, player_scoreの3つをDROPした後、 init_ のついた初期データ3テーブルからSELECTしつつCREATEする。

当日の推移

Discordおよびcommitのログから辿ってますが、それなりに間違いがあるかも知れません。

  • 9:40
    • 恙なくDiscordに3名集合。
    • YouTubeを眺めていた。
  • 10:01
    • CloudFormationのスタックを作成開始。
    • チームメイトはレギュレーションを読んでいた。
  • 10:05
    • サーバにSSH接続し、構成を見て回る。
      • この時点でRedisが動いていることに気付きややビビる。
      • TCP33060がListenしていることに気付き、正体不明で焦る。(X Plugin知らなかったので調べた)
      • Nginx, MySQLはOSにそのままインストールされており、Appのみコンテナで動いていることを把握。
  • 10:08
    • 事前に準備した監視関連のツール類セットアップが完了。
  • 10:13
    • 設定ファイル類一式をgit commitする。
    • 初期実装からRustに切り替えたらcargo buildに超時間がかかる。この時点でDockerをやめる判断。
  • 10:33
    • Rustの初期状態で1回目のベンチマーク。(2,886点)
  • 10:36
    • Docker剝がしてベンチ。(3,046点)
  • 11:51
    • visit_history のレコードを圧縮。
  • 12:23
    • SQLiteのクエリログを出力できることに気が付いて試すが、集計できる気がせずそのまま捨てる。
  • 12:30
    • SQLiteのまま進むかMySQLに移行するかの協議。
    • 私としては、SQLiteのままだとその先でサーバ台数稼ごうとした時にハードルになる筈なのでMySQL移行押し。
    • テーブル構成なども何だかんだ悩みつつ、テナント毎の全テーブルに tenant_id が既に存在していることから、すべてのテナントを単一テーブルに移行する方針でMySQL移行に着手。
  • 13:25
    • player_score は最新のデータのみが必要なことに気が付いたので圧縮することにした。たしかMySQL移行の準備完了とほぼ同タイミングでマージされた筈。
  • 13:40
    • MySQL移行の実装が POST /initialize 含めて大体できる。
  • 13:57
    • MySQL移行が済んでの初ベンチ。(800点)
  • 14:27
    • SQLiteから移行したテーブルにPK作ってなかったので修正。(3,657点)
  • 14:29
    • id_generator テーブルを使っていたユニークIDを、UUID(v4)を生成するよう修正。(5,470点)
  • 15:15
    • player_handler のN+1の解消。(8,779点)
  • 15:29
    • MySQLの bind-address の修正、環境変数 ISUCON_DB_HOST の修正ファイルの準備など、AppとDBの分割を進める。
    • Nginx + Appと、DBとの2台構成。(9,111点)
  • 15:44
    • AppのCPUがもう少し使えそうだったので、ActixのWorker数を1から増やす。(9,545点)
    • 最終的に具合の良さそうなWorker数3で決め打って最後までそのまま。
  • 16:08
    • Appをサーバ2台構成にするためにflockをやめたいのでRedis使う検討をする。が、結構時間遣いそうなので断念。
  • 16:25
    • competition_score_handler をBulkでInsertするよう修正。(16,479点)
  • 16:49
    • flockやめるために普通にトランザクション使おうとするが、そんなに簡単でもなく断念。
    • competition_score_handler に若干バグがあり偶に500を返してスコアを落としていたのを修正していたと思う。(20,953点)
  • 17:06
    • どうにかAppを2台にできないか考え、リクエストURI単位でロックが必要なもののみ1台に寄せれば何とかならないかと思い至りNginxと向き合う。
      • 数回試してベンチが通らず断念。これを書いてる時点で理由は不明。
  • 17:27
    • 粛々とbilling reportまわりの最適化がされていたらしい。(27,281点/当日のベストスコア)
  • 17:34
    • 再起動試験を行った上で再計測。(24,776点)
    • さっきまでよりだいぶスコアが落ちたので何度かベンチ回すが概ね24,000点台。理由がわからず。
    • 終了に向けて監視やログ出力等を止める。
    • この頃、私物のEC2のJaegerがいつの間にか潰れていたことに気付いた(たぶん適当にdocker runしただけなのでストレージがあふれたと思う)が、もう要らないしなと思って流した。
  • 17:55
    • 提出用の最後のベンチ。(25,598点/最終スコア)
    • これ以上ガチャやってもあんまり変化なさそうなのでこれで終了。
  • 18:30
    • ビール買ってきて打ち上げ。

よかった点

  • 自分は例年、コードをあんまり読まずどういう作りなのか不明なままに時間を使う傾向にあったが、今回は最初にコードを読む時間をそれなりに取った。
    • ただしRustわからんので、大掴みするために実はPerl実装を最初に読んでいた。
    • 結果として、flockの話などもそんなにラグなく会話できていたと思うので良かった。
  • SQLiteからMySQLへの移行に際して、テーブル設計以外に、/initializeによる初期化処理をどうするかをかなり早い段階で議論の俎上に上げられて、方針が立てられていた。
    • 結果としてほぼハマらずに実現できたので良かった。
  • SQLiteからMySQLに移行したことで、AppとDBのサーバを分割できスコアが上げられた。
    • やってから気付いたがスロークエリが見えるようになった。
  • かなり心落ち着けて再起動試験の時間がとれた。

反省点

  • 再起動試験と一緒にenv-checkerを実行していなかった。これで失格もあり得るので反省。
  • アプリケーションのビルド・デプロイ方法を私が理解しておらず、Rustのソースコードを一切触らなかった。
  • 最大の心残りはAppを1台から2台以上に分散できなかった点。
    • テナント単位でのファイルレベルのロックを代替する手段に行きつけなかった。
    • Redisが動いておりこれ使えってことだよな・・・となったものの、改修量が多そうで時間的に断念。(16:08頃)
  • 別のアプローチとして、ロックをケアすべきAPIエンドポイントは限定できる筈なので、Nginxのバランシングによりそれらのリクエストのみ1台のAppに纏め、他は複数台で分散するという案をやりかけたが、ベンチが通らず断念。(17:06頃)
    • もう少し時間があれば突破できた可能性も感じるが、しかし恐らくハマりそう。
  • 競技終了後に思い付いたアプローチとして、ロックはテナント単位なので、Hostヘッダのサブドメインの文字列を見てバランシング先のAppサーバを決めれば複数台構成は取れた気がする。
    • 少なくとも100テナントのサブドメイン名は最初から完全に判っている。
    • 競技中はHostヘッダを使って何かする案を思い付けなかった。
    • アクセスログにHostの情報を出力しておけば、テナント単位のリクエスト分布が解析できた筈だがそういうアイデアも出せず。
    • ここは本当に心残り。
  • adminのDBとtenantのDBを2ホストに割る案も出せた筈だが、思いつけなかった。
    • とは言えCPUの使われ方にAppヘビーに見えたので、Appを割りたかった気持ちは強い。

基本的に私はインフラの人で、ISUCONはサーバたくさん使った方がスコアを出せる筈と考えているので、今回最終的にサーバ3台のうち1台余らせてしまった点が実に心残りでいます。

あとこの辺りの全体の構成を変更していく作業、他の開発とベンチを一時的に止める必要があって、試験しながら投入するタイミングを図るの結構難しいんだよな・・・というのが今回改めて感じた課題でもあります。

まとめ

とりあえず、優秀なチームメイトに恵まれて、なんか見ているだけで終盤みるみるスコアが上がっていく感じがして良かったです。ありがとうございました。

また例年、素晴らしい問題と運営を準備くださる主催者の方々にも改めて感謝したいと思います。
本選も頑張ります。

Discussion