ISUCON13で優勝しました(チーム NaruseJun)
11月25日に開催されたISUCON13でチームNaruseJunとして参加し優勝しました。
メンバーはここ4年同じで、大学時代のサークル仲間の@sekai・@takashi・とーふとふの三人です。
昨年のISUCON12でも優勝したので、チームNaruseJunは二連覇となります。
最終スコアは468,006点でした。
スコアの推移は以下の通りです。
かなり順調にスコアを伸ばしていますね。後述しますが17時直後にめちゃくちゃ伸びているのは、ログを止めた結果です。
その他のスコアは↓
ISUCON13 受賞チームおよび全チームスコア : ISUCON公式Blog
事前準備
今年はチーム全員が忙しかったので、チームで最初に集まったのは11/14でした。
その日は30分くらいで今年の流れの確認と、素振りの日(11/18)を確定して解散しました。
ありがたいことに過去優勝チームとしてLodgeでのオフライン参加に招待していただいていたので、素振りもsekaiさんの会社のオフィスに集まって素振りをしました。お昼すぎに集まって14時〜19時半くらいのスケジュールで初動→改善いくつか→再起動試験の手順を確認しました。
その後は素振りで出た改善点をテンプレートリポジトリやpproteinにフィードバックして当日を迎えました。
個人としては ISUCON精進鯖 で勉強会したり、毎年のごとくですがprivate-isuで50万点目指すやつとかやっていました。今年のprivate-isu 50万点RTAは10時間くらいでした。private-isuは何回解いても学びがあるので本当にいい問題だと思います。おすすめです。
今年の事前準備における成果物としてはprivate-isu素振り用 Cloud Formation、ISUCON11 予選 Cloud Formation、ISUCON9 予選素振り用 CloudFormation 簡易ポータル付きなどがあります。
特に最後の簡易ポータル付きテンプレートは、これ一ファイルポンとAWSコンソールから立ち上げるだけでWeb上からのベンチマーク起動とベンチマークログの確認とかまでできるのでおすすめです。作り方がわかったので、他の回のテンプレートを作りたいですね。
戦略
去年とほとんど変わっていません。
去年の記事 → ISUCON12で優勝しました(チーム NaruseJun)
役割分担
初動の分担は後述するので、主にメインの改善時のことを書きます。
このメンバーになってからほとんど変わらず、とーふとふが一応指揮ということになっているものの基本的にはメンバーそれぞれが自分で計測結果を見て改善点を見つけて「ここ直します〜」と宣言して直していく感じでした。
改善点がコンフリクトした場合は次点の改善点を見つけて直します。
今回はオフラインで集まったのでここらへんのコミュニケーションがスムーズで良かったですね。
回ごとにメンバーが注力する部分が異なるので面白く、今回は例年インデックスを貼ってくれるsekaiが積極的にオンメモリの改善を入れていて、逆にオンメモリ改善入れるのが好きな僕がインデックスとかを貼るのをやっていました。
計測
毎年おなじみsekaiさん謹製の対ISUCONツールpproteinを利用しました。
これは、 /initialize
の最後にフックを挟むことで自動で計測を開始し、Web上からpprof,alp,slpの結果をインタラクティブに見ることのできるツールです。
これを利用すれば、ベンチ前のログローテートなどをしなくてもベンチごとの計測結果を溜めて見ることができます。
実際にどうなっているのか触ってみないとわからないと思うので読み取り専用にしたものを、 https://pprotein.isucon13.to-hutohu.com/#/group/ に公開しています。
本選中は一切コメントを書いていませんでしたが、各ベンチマークのスコアとベンチマーカーログを追記しています。参考にしてください。
(モダンに行くならPyroscopeとかOpenTelemetryもいいよね〜という話をしていたのですが、時間が足りない&ベンチ走行ごとに分けて見やすいというのは利点だということになったのでpproteinで行くことにしました)
また、各サーバーにNetDataを配置し、チーム全員が見られるモニターに映していました。
個人的には、itermのウィンドウを以下のようにして、ベンチマーク中のプロセスごとのCPU負荷やログを観察していました。
Ctrl+Shift+iで全パネルに対して入力できるようにして、全サーバーを同時に操作すると楽でした。
分析
基本的にはpproteinを見て分析していきます。
僕が最初に見るのはalpの結果です。適切に設定を追加して、それぞれのハンドラが一つの行にまとまるようにしておくことが大事です。
その時の得点や時間にもよりますが、項目としてはSumを見ることが多い気がします。pprotein上ではインタラクティブに順番を入れ替えることもできるので、必要に応じてAvgやCount、9X%タイルなども見ています。
次に見るのはslpの結果です。
こちらも多くの場合はSumを見ますが、最序盤などインデックスを追加したい場合はRow Sent/Row Examinedの比を見ています。適切にインデックスが設定されている場合はこの比が5倍以下くらいになりますが、インデックスがない場合は100倍とかになるので適切なインデックスを考えて設定します。(今年はChatGPTに考えてもらったりもしました)
序盤はalpとslpの結果だけを見て改善していました。
DBの負荷が抜けて来るとpprofの出番となります。
大体フレームグラフを表示して、負荷を減らせそうな場所を探します。
改善
リポジトリにはGoのアプリケーションコードと全言語共通のあれこれと環境変数のenv.sh及びミドルウェアの設定を入れています。
実際にコードを変更する作業はそれぞれがブランチを切って手元のPCで行います。
ただし、手元でコードのビルドを行ったりローカルの開発環境を準備することはせず、基本的にベンチマークによって動作確認などは行っています。(ドツボにはまったときも実際にデプロイしてブラウザから動作確認していました)
普通にビルドできないようなtypoやimport忘れとかも、デプロイ時に発覚する感じです。
ミドルウェアの設定変更もリポジトリのコードを変更しデプロイすることで反映しています。
エディタはsekaiがVS Code、takashiと僕がJetBrains IDEを使っています。
みんなGitHub Copilotも利用しているはず...
ブランチをデプロイして計測したあとにmasterへのマージを行い、マージはそれぞれの手元で行ってpushします。
デプロイ
これも例年とほとんど変わらないですが、makeファイルのコマンド make deploy
でmasterブランチをデプロイできるようになっており、 make deploy BRANCH=hoge
でブランチを指定してデプロイできるようになっています。
前回からの改善点として、pproteinで各ホストごとに最新の計測成功したpprofのプロファイル結果をダウンロードできるようにしたので、それを引っ張ってきてGoのPGOでビルドできるようにしたりしていました。
pprotein以前はMakefileに色々仕込んでいたんですが、だいぶスッキリした感じになっていると思います。
初動
ここに関しては3人で明確に役割分担をしています。
- sekai: Ansible流し込み・各種ツール設定・pproteinなど計測準備
- takashi: Cloud Formation操作・リポジトリ準備
- to-hutohu: マニュアル・ドキュメント・実装把握・UI操作
今回は大体10時40分くらいには、実際の改善が開始できるような状態になっていました。
終盤
17時すぎくらいに再起動試験を行います。
再起動試験時には以下の点に気をつけています。
- サーバー再起動してもベンチが通る
- アプリの再起動を挟まず連続してベンチを行っても通る&スコアが変わらない
- 正しくすべてのサービスが active になっているか(
sudo systemctl status
) - 各タイミングでフロントエンドの機能が壊れていないか
実際の操作としては、ベンチ → フロントチェック → サーバー再起動 → sudo systemctl status
→ フロントチェック → ベンチ → フロントチェック → ベンチ → フロントチェック みたいな感じだと思います。
フロントチェックはそこまで網羅的にチェックしているわけではなく、ざっくりという感じでした。本当はPlaywrightとかでやりたかったんですが、準備時間が足りなかった…
やったこと
順番にザクザク書いていきます。
(臨場感のために常体で)
全ログを見たい人は https://pprotein.isucon13.to-hutohu.com/#/group/ に全スコアとその時のベンチマーカーのログを公開しているので見てみてください。
実際のリポジトリはこちら
8:00 起床
ホテルで起床。
風呂に行って軽くサウナで体を温める。
わがままでモニターを持ち込むことにしていたので、でかい荷物を持ってタクシーでホテルから紀尾井町まで行った。
sekaiさんが先に到着していてびっくりした(イベントのときは大体sekaiさんが早いのだけど)
大会の日はレッドブルと2Lの水を飲んでいるのだけど、確保していなかったので外のコンビニに買いに行く。
9:30~10:00 開始前
僕が設定を間違ってチートシートが公開状態になっていたので、そこに載っかっていたpproteinのドメインを変更するためにsekaiさんがてんやわんやしていた。申し訳ない…
来年はTailscaleとかWARPとか使ったほうがいいんでしょうね。
(あのチートシートを見ていた他チームの人がいたらすいません…(?))
問題は動画サイトでびっくりした。
最初広告のところで、チケットサービスかな?とか思った。
いすこちゃん可愛すぎ。Vtuber好きなのでめっちゃ刺さった。
10:00~11:00 初動 → 5261
前述の役割分担で初動をする。
サーバーはc5.largeが3台らしい。
マニュアルを読んでDNSとその水責め攻撃にビビる。
iconの仕様が特殊っぽい。
得点はリクエスト数じゃなくてtipの合計らしい。
/etc/hosts
を書き換えないと動作確認できないし、各ユーザーページに行くなら追記も必要そうだ。
ユーザーごとにサブドメインを払い出す感じになっているんだなあとなった。
ざっくりマニュアルを読んで動作確認したあと、いつもの interpolateParams=true
をいれる。
ざっとコードを眺めてマニュアルで言及されていたところを見てみる。
とりあえずアイコンのハッシュを計算して304を返すような実装を追加してみる。が、ダブルクォートを考慮していないので普通にバグっている。
slpの結果を見てインデックス追加。
他二人がやったこと
- sekai
- 計測準備
- 画像挿入時のハッシュ計算
- スキーマ適用周りの改善
- takashi
- 環境構築
- リポジトリ準備
- Nginx秘伝のタレ追加
11:00~12:00 5261 → 18761
デプロイ時にファイルが適切に配置されていなかったので直す。
slpの結果 を見ると tags がめちゃくちゃ呼ばれている。
しかし、他のクエリやコードを見てもtagsへの書き込みは存在しないので完全にオンメモリキャッシュができることがわかり、オンメモリキャッシュした。
これ、実際はID含めて決め打ちで良かったが、若干パニクっていたので一回DBに入れたあとに取り出してオンメモリキャッシュしてしまった…
合わせてインデックスもいくつか追加してtagsへのアクセスをなくした。
これで1万点越え。
その後もslpの結果を見てRow Sent/Row Examined比が悪いものを見てインデックスをいくつか追加した。
pprofの結果を見てJSON周りの負荷が高くなってきたので、json/v2に変更
一応負荷は下がってる…かな?
この時点(12:11ベンチ)で23632。
他二人がやったこと
- sekai
- livestreamTags,userのオンメモリキャッシュ
- takashi
- いくつかのN+1や不要なJOINの削除
- getLivestreamStatisticsHandlerの改善
12:00~13:00 18761 → 特別賞狙い → 76951
12時過ぎにsekaiさんがUserModelをオンメモリキャッシュして、1台で2.3万点を越えたので単純に分散したらそろそろ特別賞を狙えるんじゃないかという話になった。
そこでsekaiさんはDBを別のサーバーに移す作業を開始した。
takashiはiconをDBから読んで返していた部分をファイルに書き出して、それを返す作業を進めている。
自分はjson/v2への移行を済ませたあと、更にインデックスを追加してPGOでビルドする準備をしていた。
iconのファイル書き出しが3.6万、DBの分散が4.6万だったのでマージすれば5万を越えるのではとなり、マージするが4.8万…
最後の一押しとして、PGO付きビルドと各種ログのオフをして6.4万。無事特別賞を取ることができた。
このタイミングのサーバー構成は以下の通り。
- S1: Nginx, App
- S2: MySQL(App)
この時点でかなりPowerDNSが重くなってきていたので、S3にPowerDNSを移動することに。
sekaiさんがシュッとやってくれて7.6万点。
このタイミングのサーバー構成は以下の通り。
- S1: Nginx, App
- S2: MySQL(App)
- S3: PowerDNS, MySQL(PowerDNS)
これでもすぐにPowerDNSが足を引っ張りそうということになり、sekaiさんがDNSをGoで実装するわ〜って言って作業を開始していた。
このタイミングで雑にSankey図を作るためのログを吐く設定を入れて↓みたいな図を作ってたりした。
正直、この図自体はそこまで参考にならなかったけどこれを作るためのユーザーごとのアクセスログはシナリオを推定するために大分参考になった。
ログを見たあとにマニュアルとかを読むと、あーここのこの文章はこの意図があるのかな〜みたいなのが結構分かる。
これを通して、ユーザーの視聴満足度が上がるような改善を入れていくのが良さそうだと思い、NGワード周りの実装を見ていくことにした。
ベストスコアの76951は、DBのスロークエリがオフになっている状態でS3へのPowerDNS分離がされてベンチが走ったからだと思う。
他二人がやったこと
- sekai
- userModelのオンメモリキャッシュ
- DBの分散
- PowerDNSの分散
- takashi
- iconのファイル化(配信はGo)
13:00~14:00 76951 → 76951
NGワードのスパム判定処理をアプリで行うようにした。
これによって [一般エラー] POST /api/livestream/7532/livecomment へのリクエストに対して、期待されたHTTPステータスコードが確認できませんでした (expected:400, actual:201)
が大量に出るようになって若干得点が下がったが、性能が向上していることは確かなのでそのままmasterに入れた。
これによる悪影響を把握するのはもう少しあと。
13時半くらいからベンチマークが動かなくなってそこそこ困った。
NaruseJunは動作確認もベンチマーカーに任せているし、大体改善は前の改善を前提にしているのでどうにもならない時間が続いた。
その後もベンチ開始までの時間はまちまちだったので、即開始したらガチャ成功、ちょっと待ちだったらハズレ、Abortにかかったらドボンみたいな感じでガチャしてました。
他二人がやったこと
- sekai
- DNSをAppに組み込む
- takashi
- iconをNginxから配信する
- iconのハッシュをオンメモリキャッシュ
- userRankingをSingleFlightに
- N+1改善
14:00~15:00 76951 → 132390
ずっとベンチが止まっていたので改善はできず。
ログを見たりマニュアルを読み直したり、フロントエンドが壊れていないかチェックしていたりした。
14:50くらいからベンチが動くようになって、sekaiとtakashiの改善が入って記録は伸びていた。
他二人がやったこと
- sekai
- DNS on Appの改善
- takashi
- userRankingをSingleFlightに
- N+1改善
- 不要なトランザクションの削除
15:00~16:00 132390 → 204805
NGワード追加時の削除処理の効率化とMySQLの設定・Nginxの設定を追加した。
ここでNGワード追加の処理を早く終わらせすぎると [一般エラー] POST /api/livestream/7532/livecomment へのリクエストに対して、期待されたHTTPステータスコードが確認できませんでした (expected:400, actual:201)
が増えることに気づいて500ミリ秒待つようにした。
MySQLはinnodb_buffer_pool_sizeの設定を追加すると何故かスコアがかなり下がってしまったので、追加しなかった。
これに関しては終盤でも同様だったので原因を知りたい。
Nginxは
ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-SHA';
みたいな感じで計算が軽そうなcipherを指定してたけど効果はあったんだろうか...?
DNSをAppに入れてPowerDNSを処理していたS3が空いたので、そこにAppを持っていきたいということになった。
登録の処理はDNSのサーバーと同じである必要があるのでS1に残したままにして、アイコン周りもNginxで配信していてアイコンの書き出しはローカルにしかできないのでS1に残すことになった。
構成はこんな感じ
- S1: Nginx, App(DNS, /register, /icon, /user/:username/icon)
- S2: MySQL(App)
- S3: App(その他のエンドポイント)
しかし、アイコン周りのアクセスが非常に多いのでS1のCPU負荷が100%に張り付いていた。
icon周りをS3に移すためにiconの変更エンドポイントはS3で受けて、実際にファイルに書き出す部分は内部リクエストでS1に移譲する感じにした。
それを実装して204805
他二人がやったこと
- sekai
- Appの一部処理をS3に
- 水責め攻撃のクエリに返答しないように
- If-None-Matchヘッダーの比較バグを修正
16:00~17:00 204805 → 277310
16時までの分散で全体的にCPUが余るようになったので、負荷の軽減というよりはレイテンシの改善をしていくことにした。
自分は不要なカラムの取得を減らそうとしたけど何故かバグって沼にハマってしまった。
最終的に変更入れられなくて諦めた。
他二人がやったこと
- sekai
- livestream, ngwordのオンメモリキャッシュ
- /registerをS1に
- takashi
- userStatisticsバグの修正
- iconハッシュ計算の改善
17:00~18:00 277310 → 468006
基本的に再起動試験とログ周りの整理をしていた。
一回echoのログを切ってベンチを回したと思ったのに、まだアクセスログが出ていてなんでだろうと思っていたら、元々のコードに含まれていたアクセスログ出力設定の他にpproteinで設定されていた。
そのログも切ってベンチを回すと40万点を越えた。
また、15時台に追加したNGワード追加時のSleep時間を増やしていくと得点が上がったりしたのでそこらへんを調整したりした。最終的には1秒待つことにした。
その後は再度再起動試験をしたりpgoを回したり、innodb_buffer_pool_sizeを変更したりした。(innodb_buffer_pool_sizeは効果がなかったのでRevertした)
最後にpprofの計測も切って再起動して実行したベンチマークがその日のベストスコアでフィニッシュ。(468006点)
あとは祈るだけ。
最終的なベンチ結果一覧はこんな感じでした。
18:00~結果発表
再起動試験が最大の敵だと思っていたので、ひたすら祈ってた。
緊張するとお腹痛くなるので当日発表でありがたい…運営の皆さんありがとうございます。
モニターを返したり反省会したり酒を飲んだりして結果発表を待つ。
オフラインでみんなで集まって結果発表を見るのは楽しかった。
優勝の他に特別賞、New Relic賞、ウォンテッドリー株式会社「伸びしろすごいで賞」、はてな賞を受賞した。
どれも賞品が楽しみ。ありがとうございます!
所感
問題について
めちゃくちゃ面白い問題でした。
事前のインタビュー(本選のみ開催のISUCON13、問題はどう変わる? 作問チームが語る”問題に込めた思い” - #FlattSecurityMagazine)でも言及されていたように、初心者から上級者まで幅広く楽しめるような問題になっていたと思います。
例えば、インデックスに関しては各~~_idにインデックスを追加するだけでは十分ではなく、各クエリに対するカバリングインデックスになるようにインデックスを貼ったほうが良かったと思います。
N+1に関してもただ闇雲にコードを見てN+1を発見して潰していけばいいわけではなく、ちゃんと計測結果を見て優先度を決めて改善していく必要がありました。
NaruseJunチームでいうと、競技終了時点でもN+1になりうるようなコードはたくさんあるのですが、それをN+1を潰す形で実現するのかオンメモリキャッシュで実現するのかの判断なども大事だったように思います。(N+1を潰したほうが速度としては早くなるかもしれないが、全部潰すには時間が足りないので)
N+1になりうるコード例(それぞれオンメモリキャッシュしているので十分温まればクエリ回数は少ない)
- https://github.com/narusejun/isucon13-final/blob/master/app/go/reaction_handler.go#L79-L86
- https://github.com/narusejun/isucon13-final/blob/master/app/go/livecomment_handler.go#L117-L124
- https://github.com/narusejun/isucon13-final/blob/master/app/go/livestream_handler.go#L132-L141
-
https://github.com/narusejun/isucon13-final/blob/master/app/go/livestream_handler.go#L171-L179
(他にも5・6個ありました)
一方で、stats系のハンドラはガッツリ書き直されています。
DNSについてもびっくりポイントではありましたが序盤のうちはあまり問題にならず、水責め攻撃が目立つようになったとしてもいくつか解決策があるのがとても面白かったです。
チームとして
長く同じメンバーでやっているのでコミュニケーションもスムーズに進めることができました。
チームとして良いなあと思う点は
- 3人共が独立して計測→分析→改善(アプリもインフラも)を回せる
- 実装している内容についてかなり抽象度高く共有しても、想起されている実装が近い(はず)
- 得意不得意の差はあれど、実装とかの入れ替えが可能
とかかなあと思います。
個人として
チームの中で一番改善量が少ないしDNSを実装するぞ!みたいな筋肉もなくてまだまだ精進が必要だなあと思いました。(結構凹んでいる)
幸いなことに仕事でもたくさんGoを書かせてもらっているので、今後も精進していきたいです。
LLMとか
いくつかの記事で言及されていたLLMの活用について書きます。
事前準備
アクセスログの可視化系でいくつかPythonスクリプトを作ってもらいました。
とか
とかです。
本当はISUCON13にツールの力で勝ちたかった(mazrean)で言及されていたようなユーザーの行動遷移メトリクスを作りたかったのですが、やり方と時間が足りませんでした…
当日
当日のChatGPTの履歴はこんな感じでした。
実際に役に立ったのは追加インデックス勧告とNginx SSLパフォーマンス設定、Optimize Golang SQL Queryあたりです。
特に最後の会話は、全く変更すること無くN+1が解決されていて素晴らしかったです。
コード全体の説明などはまだまだ自分が読んだほうが正確かつ早いなあと思いました。不正確で微妙な情報を修正しながら読むよりは、ガッツリ0から読んだほうが良いと感じます。(プロンプトとかが良くなかったのかもしれない)
N+1とかに関しては、適切にコンテキストを小さくしたり必要な情報を与えたりすることが大事だなあと感じました。「〜〜を使えば良いと思うのですが」みたいなのを与えるのも大事だと思います。
ISUCONで勝つために
https://zenn.dev/tohutohu/articles/8c34d1187e1b21#isuconで勝つために を読んでください。
自分の実力を試したり楽しむだけではなく、ちゃんと勝ちに行くのであれば↑の内容に加えて以下の点が大事かなと思っています。
- しっかり時間をかけて複数の過去問を解ききる
- NaruseJunチームはそれぞれ10問は解いてそうです(重複含む)
- 練習・素振りでは本番と同等の環境で行う
- 複数台構成・ミドルウェア・ネットワークなどのあれこれになれるのも大事だと思います
- 過去の上位のチームの参加記を読む
去年も書いたので今年も書こうと思ったのですが去年の読んだらこれでいいじゃんってなってしまった…
ただ、なかなか初手で参加記を読むのは選択肢にならないと思うので別途「ISUCONをはじめからていねいに」みたいな記事が書けたらいいですね。
traPが公開しているISUCON初心者向け講習会の資料もおすすめです。(ポータルなどは使えないですが)
終わりに
今年も勝てて良かった!!!
運営・作問の皆さんありがとうございました!!! ISUCON最高!
(去年に比べるとめちゃくちゃ太ってるな…来年は健康に生きたいですね…)
Discussion