🪑

ISUCON12で優勝しました(チーム NaruseJun)

2022/09/26に公開

8月27日に開催されたISUCON12 本選にチームNaruseJunとして参加し、スコア341,258点で優勝しました。(ISUCON12 本選の結果発表と全チームのスコア
メンバーは @sekai@takashi_trap でした。
この記事はその参加記です。

チームについて

このメンバーでのISUCONへの参加は10(予選敗退), 11(準優勝)から3回連続です。
それぞれのISUCON参加回数は @to-hutohu, @sekaiが6回目、@takashi_trapが5回目です。

予選準備

7月9日と7月17日に予選のために集まって作戦会議をしました。

  • 7/9
    • 1時間程度集まって素振りに向けた作戦会議
    • 素振りの環境準備、チートシート、初動のAnsible整備、Goを思い出すなどがそれぞれ宿題として持ち帰ることに
  • 7/17
    • 素振り会(ISUCON11予選)
    • 5時間くらいで初動→改善→再起動試験までやって100万点

これ以外にも、自分は個人的にいくつかのISUCONの過去問でカリカリになるまでチューニングして自由研究してました。

予選ざっくり

予選の参加記事は書いていないですが、この後も書きそうにないのでここに合わせてざっくりと書きます。

  • SQLiteでびっくりしたがかなり早い段階でMySQLに移行することに
  • sekaiがMySQLに移行する部分をまるっとやってくれた
    • アプリはすぐにできたが初期化周りでかなり苦労した
    • 結果的にテナントID1だけ特殊処理にして対応
    • それに加えてテナントごとのシャーディングを実装
  • クエリの修正などは to-hutohu, takashi_trap が実装
  • 全体10位で予選通過

決勝準備

予選決勝間はなかなか予定が合わず(誰も声をあげなかったからかも)、8月21日に一回素振り会をやるだけでした。ISUCON11本選を4時間半くらいやって11.5万点まで進めました。

チームのこと

NaruseJunチーム全般のことを書きます。
予選・決勝共通です。

ツール

NaruseJunチームで使ったツールを書きます。

pprotein

https://github.com/kaz/pprotein

sekaiが作った対ISUCON決戦ツールです。
1つのUIで一気通貫にpprof, http log(alp), slow query log(slp)の計測結果を見ることができるツールです。
pproteinを使うのは3年目ですが、今年はベンチマーク開始時(Initialize時)に自動で計測を開始してくれるようになってさらに便利に。




PHPMyAdmin

データベースの中身を見たり、実行計画を見たり、一時的にインデックスを張ったりするときに使います。

pprotein, PHPMyAdminは外部にインスタンスを建てておき、初動のAnsible実行時にトンネルを貼って通信ができるようにしています。
初動のAnsibleではこれらのほかに、ホスト名の変更やDNSを設定して簡単にSSH接続ができるようにするなどが行われます。

Netdata

各インスタンスのメトリクスを見るために利用します。
今のボトルネックがアプリなのかDBなのかなどを確認したり、ベンチマーク中のCPU負荷の繊維などを確認したりする目的で使うことが多いです。

上記3つのツールは外部に立てているサーバーに構築されています。(インスタンスに必要なバイナリやエージェントはAnsibleでインストールされます)
その他には各自でdstatやtopを利用して様子を見ることもありますが、8割くらいはpproteinの結果を見て修正するというのをループしています。

戦略

NaruseJunチームの大体の戦略を書きます

チーム内分担

メンバー3人ともがアプリもインフラもいじることができるので、基本的には3人がそれぞれ独立して改善を入れていく、つまり分担はない感じで進めていました。
一応 to-hutohu が司令塔ということになっていますが、どのボトルネックを解消しようとかを決定することはなく「そろそろ再起動試験をしましょうか~」と声をかけるくらいです。

とはいえ、それぞれの得意分野や興味のある改善は分かれているので以下のような改善をすることが多かったです。

  • @sekai:大きめの飛び道具的修正(シャーディングなど)
  • @takashi_trap:重いクエリの修正(スキーマの変更など含む)
  • @to-hutohu:オンメモリキャッシュやこまごまとしたもの

方針

初手はDBの分離などはせず、1台である程度のところまで改善を入れることが多いです。理由としては計測が楽なことと、大きい改善を入れるとなって細かいデバッグが必要になったときにそれぞれが1インスタンスを持って修正するみたいなことがやりやすいからです。
とはいえ、最近はpproteinのおかげで複数台に分けても簡単に計測ができるようになったので早め(~3時間)くらいで複数台にすることも多くなってきた気がします。

サーバー構成はかなり終盤まで確定させず、そのためアプリの修正もアプリが複数台になっても問題ないようにしています。例えば、オンメモリキャッシュするリソースもInitialize時に初期化され更新されない系のみを読み込み時に透過的にキャッシュするといった感じです。

Redisとかは毎年使いたいよねという話をしていますが、毎回実装をバグらせたり選択肢に入らなかったりして使えたことがないです。

リポジトリの運用的には、それぞれがブランチを作って自己判断でmasterにガンガンマージをしていくようにしています。(PRなどは作らない)

テンプレ行動

毎回ほぼ固定で動くものに関して書きます

初動

チーム内分担はしていないと書きましたが、初動だけはかなりきっちり役割を分けています。
これのおかげで実際の改善に開始10分くらいで入れるようになっています。

  • @to-hutohu
    • ドキュメント読む
    • コード読んでスプレッドシートにエンドポイントごとの特徴やテーブルのR/Wをまとめる
  • @sekai
    • Ansible流し込む
    • pprotein準備
  • @takashi_trap
    • リポジトリにコード追加
    • デプロイスクリプト準備

デプロイ

デプロイの仕組み的には素朴なシェルスクリプトを使っています。

https://github.com/narusejun/isucon12-final/blob/master/common/deploy.sh

これでも決勝に向けて少し複雑になっていて、全インスタンス共通のデプロイフローをくくりだせるようになっています。

実際のデプロイ時には make deploy BRANCH=<ブランチ名> でデプロイします。
デプロイ・ベンチをするときには「次デプロイいいですか」「じゃあその次予約していいですか」みたいな感じでロック・予約してから行います。

複数台デプロイには tmux の複数パネルに同時入力できる機能を利用していました。
デプロイするためだけのウィンドウを作って「↑」→「Enter」でデプロイできるようにしてました。

本選雑感

予選の作問がカヤックだったようなので、決勝はサイバーエージェントなのかなと思っていました。
サイバーエージェントの作問担当の方はISUCON経験的に、決勝の問題はトリッキーなものというよりも王道っぽいものになるかなとヤマを張っていました。(それに特化した対策はしていませんでしたが)

競技中はアクセスログなどを見てベンチマーカーの整合性チェックがかなりゆるい(事前の整合性チェックをすり抜けることがある&仕様を確認するようなアクセスが走行中にない)ことから、仕様を崩すような変更に気を付けることとちゃんと時間を取ってUI側で動作確認をすることを気を付けていました。
その結果、ガチャの結果が意図したランダムでないことなどを発見できたので良かったです。

やったこと

細かく書いているので、読み飛ばすのをおすすめします。

  • 10:00: サーバーが5台あって驚く。Ansibleとかは3台想定だったので修正(sekai), ドキュメント読み始める(to-hutohu)
  • 10:02: 初回ベンチ開始(250点くらい)
  • 10:03: サーバー構成把握(takashi_trap), Ansible流し込み開始(sekai)
  • 10:05: Ansible完走(sekai), リポジトリにコード追加(takashi_trap), 初期状態のUIスクリーンショットを撮る(to-hutohu)
  • 10:10: 初期実装を確認してスプレッドシートにまとめていく(to-hutohu)
  • 10:20: デプロイスクリプト、秘伝のタレ追加(takashi_trap), pprotein設定追加(sekai)
  • 10:23: 初回計測。DBめっちゃ重いね~という話に
  • 10:30: 基本的なINDEXを貼っていく(sekai), obtainPresentを修正開始(takashi_trap)
  • 10:35: INDEXを貼ってベンチ(8315点), generateIDが重い問題を調査(sekai)
  • 10:50: generateIDをsnowflakeで置き換える(24498点)(sekai)
  • 10:53: 初期実装まとめ完了(to-hutohu)
  • 11:00: obtainPresentのN+1をBulk Insertに変更(30452点)(takashi_trap)
  • 11:05: obtainItemの修正を開始(takashi_trap), 特別賞狙いでサーバーの分割を開始(sekai), alpの設定を書く(to-hutohu)
  • 11:11: interpolateParams=trueの設定追加(to-hutohu)
  • 11:20: サーバーの分割完了(1台目:Nginx/App, 2台目:MySQL)(40478点)(sekai)
  • 11:50: INDEX追加、obtainPresentのbatch update(49602点)(sekai)
  • 11:53: 計測系・ログ系をOFFにして特別賞を取りに行く(53037点)(sekai)
  • 12:10: checkBanをRedisで行うのを断念(to-hutohu), ユーザーIDごとにアプリ側でシャードする実装の開始(sekai)
  • 12:20: gachaDrawのN+1を修正、ついでにrandではなくsnowflakeを利用してガチャ結果を出すように(61546点)(to-hutohu)
    • snowflakeを利用してガチャ結果を出す実装は不具合があり17:00ごろに修正
  • 12:28: obtainItemのN+1の修正一部完了(71000点くらい)(takashi_trap)
    • アイテムの種類によってクエリの書き換え方が違うため一部のみ修正
  • 12:30ごろ: 3台想定でAnsibleを動かしていたのでDNS周りがごちゃっとしていて面倒だった
    • どっかのタイミングでスッとsekaiが修正
  • 12:44: createUserのデッキ初期化のN+1を修正(75000点くらい)(to-hutohu)
  • 12:55: obtainItemのN+1の修正完了(89000点くらい)(takashi_trap)
  • 13:10: JSONのエンコード/デコードにgo-jsonを利用するように(takashi_trap)
  • 13:45: ユーザーIDごとにアプリ側でシャードする実装完了、DBを2台に(162000点くらい)(sekai)
    • かなり長い時間masterから離れて作業していたので若干マージに手間取った。スッと画面共有してsekaiとto-hutohuでマージ作業。
  • 13:50: masterデータのキャッシュ実装完了(178000点)(to-hutohu)
    • 複数台でアプリを動かすことを想定して毎回master_versionだけは確認して、それが変更されていた場合はキャッシュを更新する実装にしていた
  • 14:08: DBを4台に変更(210000点)(sekai)
  • 14:30~50くらい
    • このあたりからパッと見で直せそうな部分が無くなってきて停滞気味に。その分沢山ベンチを回して不具合や細かい修正を行えたので良かったかも。
    • CPUも5台すべてのインスタンスでIdleが数十%出る状態になり、ネットワーク帯域やファイルIDがボトルネックになっているのでは?という話に。感想戦後思うとこの時点でひたすらにレイテンシを下げる方向に注力していればさらにスコアを伸ばせたのではと思う。
  • 14:50: ユーザーIDだけではなくセッションIDでも接続先DBを固定する必要があったのでセッションIDにユーザーIDの情報を載せて対応(sekai)
  • 15:05: master_version更新時にThundering Herdになっていたキャッシュ更新部分をsingleflightで修正(to-hutohu)
    • これで回すごとに20万~27万くらいでブレていたものが27万側で安定するように
  • 15:15: snowflakeのタイムスタンプ部分は下から23bit目からだったのでずらして使うように修正(sekai)
    • それより下のbitはノードIDと連番なので接続するDBに偏りがあった
  • 15:20: インスタンス構成を Nginx/App1台・DB4台構成に固める。この後アプリ一台専用実装を入れられるように
    • 一応、環境変数で1台専用実装と複数台対応実装を切り替えられるようにしていた
  • 15:20: App1台のとき、masterデータの確認・更新をAdmin API使用時のみにする(29万くらい)(to-hutohu)
  • 15:25: ボーナスのN+1の修正(takashi_trap)
  • 15:50: masterデータの更新を無駄に複数回行っていた部分を修正(30万くらい)(sekai)
  • 16:15: App1台のとき、BANデータをオンメモリキャッシュで持つように(34.8万くらい)(to-hutohu)
  • 16:30~: 配列などをsync.Poolで持つように(takashi_trap)
  • 17:00~: 再起動試験・ブラウザでの挙動チェックなど
    • アクセスログ的にここ数年の決勝に比べてベンチマーク走行中の整合性関連のチェック用アクセスが少ないように感じたのでかなり早い段階から入念にブラウザの挙動チェックを行った
    • 普段はページみられることを確認するぐらいだが、今回はほぼすべての操作ができるかをチェックした
  • 17:30: 不要なログ・サービスの停止。再起動試験・ベンチガチャ

いつもは競技終了5分前くらいまでコミットが入っていますが、今回はかなり早い17:31が競技中の最終コミット時刻となりました。


最終的にはこのようなスコア推移になりました。16時半くらいまではコンスタントにスコアを伸ばせていて理想的な運びになっていたと思います。

感想戦

インフラ提供のドワンゴさんのご厚意で、本選終了後もインスタンスを使用させていただきました。ありがとうございました!
あまりやらないつもりでしたが、スコアが抜かれると悔しくなってなんだかんだがっつり1週間使い切ってしまいました。

こちらはざっくり箇条書き(得点はメモしてませんでした)
他にもいろいろやったけど効いてそうなのだけ列挙します。
本選の部分にも書きましたが、ベンチマーカー的に一定数以上にはアクセスの並列数が増えないようで

  • Nginx・App間をUNIX domain socket で通信するように
  • fiberに置き換え
  • session,one_time_tokenのオンメモリ化
    • 再起動時はJSONファイルに逃がす
  • user_devices のキャッシュ
  • GOGCを大きく(2000とか)
  • 各種構造体・配列をsync.Poolから取り出す
  • GOMAXPROCS=1 にしてCPUコアを固定
  • NginxのWorkerを1つにしてCPUコアを固定
  • GOMAXPROCS=1 なので RWMutex などを消す
  • MySQLをMariaDBに置き換え
  • 各種データのオンメモリ化
  • ベンチマーカーのアクセスパターンを見てそのルートでは完全にキャッシュヒットするように実装する
  • DBへの書き込みをgoroutineで非同期化して、アプリ側でユーザーxテーブルごとにロックを取るようにする(オンメモリキャッシュにヒットする場合はロックを無視する)
    • 上2つを合わせるとベンチマーカーからのアクセスの場合はほぼ100%オンメモリのみで動くように

個人的にはCPU Affinityをちゃんと設定すると結構スコアが上がるのが推しです。
最終的な最高スコアは691921点でした。

ISUCONで勝つために

せっかくの優勝チームの記事なので少しでも来年以降ISUCONにチャレンジしようと思っている方にためになることを書けたらなと思い、自分がISUCONの練習のときに意識していることを書こうと思います。

ISUCONの練習には2種類あると考えています。1つ目は「チューニングのレパートリーを増やす練習」、2つ目は「典型的なムーブを確実にする練習」です。

1つ目の「チューニングのレパートリーを増やす練習」について説明します。
これは1つの問題に比較的長い時間(自分は1問数十時間)取り組んで、チューニングをするときの引き出しと経験を増やす練習です。これは1人でやった方が気楽だし、それぞれにスキルがつくのでいいんじゃないかと思っています。
やり方はシンプルで公式の問題講評や参加ブログ、参加リポジトリを読み漁って試せそうな改善をどんどん入れていきます。その年の参加ブログだけではなく前後の年のものやNginxやMySQLの公式ドキュメントなども参考にします。
この時気を付けるのは、改善策を入れる前に計測結果からその改善を入れるべき理由を見出すことです。ここで計測→分析→改善の手順を体に染み込ませます。
そして、改善ごとに実装の労力とスコアの上がり具合を確認して時間的なコスパを覚えておくと本番で役に立つと思います。
合わせて、この練習中に沢山計測やデプロイを行うことになると思うので、その作業の中で計測準備・後始末・デプロイを効率化できるようにスクリプトなどを整備しておくと良いと思います。
来年初参加でこの練習をするなら、catatsuy/private-isu(50万点)とISUCON11 予選(120万点)をおすすめします。(カッコ内は想定構成時に目指したい得点の目安)

2つ目の「典型的なムーブを確実にする練習」について説明します。
これは競技中必ず行う必要がある、初動・計測・デプロイ・再起動試験・ログなどの後始末などのムーブを正しく効率よく行えるようにする練習です。
時間を決めてチーム全員で行うことをおすすめします。
NaruseJunでこの練習の前に準備しておくのは以下の通りです。

  • 練習する問題を本番と同じ構成のインスタンスを準備
  • 初動やデプロイで使うスクリプトなどの準備
  • 問題のレギュレーションやドキュメントの準備

練習開始のタイミングでインスタンスのIPアドレスやドキュメントなどを共有して、本番にできるだけ近い形で練習を行うようにしています。
NaruseJunでは改善を行う時間は短め(~2hくらい)にして、初動とデプロイ、再起動試験で使うスクリプトや手順がちゃんと動くかを重点的に見るようにしています。まさしく素振りって感じです。
来年やるならISUCON12予選ISUCON11予選をおすすめします。素振りなのですでに解いたことがある問題でも問題ないはずです。

終わりに

今年もとても面白い問題で良かったです。
運営・作問の皆さんありがとうございました!
優勝できてよかった!

Discussion