ISUCON13 参加記(最終Fail, 最高スコア: 37,364点)
はじめに
ISUCONとは、与えられたサーバアプリケーションの高速化を目指して競う競技です。
3人1チームで参加でき、毎年開催されています。上位入賞者には賞金もあります。
競技は8時間かけて行われ、その間に各チームが各々の方法で与えられたアプリケーションを高速化します。
計測は独自のベンチマーカーが行い、アプリケーションに大量の負荷をかけてスコアが算出されます。
改善するアプリケーションや詳細なレギュレーションは競技開始と同時に公開されるため、
当日にいかに素早いフットワークで動けるかが勝負になっています。
メンバー
チーム「うどんきれいなGo」で参加しました。
ISUCON8から毎年同じメンバーで参加しています。
takutaka1220: インフラが得意、主にインフラ担当
iceman5499(私): iOS が得意、主にアプリケーション担当
yush1ga: 機械学習/データエンジニアリングが得意、主にアプリケーション担当
3人がそれなりに触れる最大公約数な言語としてGoを使用しています。
やったこと
「はじめに」で前置きを書きましたが、ここから先の内容は参加した人向けです、アプリケーションの詳細は割愛します。
事前準備
去年は計測にDatadogを使っていましたが、今年はDatadogのクーポンはなかったのでDatadog APMの代わりとしてAWS X-Rayを使ってみることにし、軽く素振りをしました。
また、デプロイツール郡が去年までシェルスクリプト製だったのですがメンテが辛かったので今年はmitamaeになりました。
mitamaeのセットアップとnginxやmysql、Go製アプリケーションのデプロイを行うrecipeを準備しておきました。
(このへんは全部takutaka1220がやってくれました)
あとISUNARABEを使って去年の問題をちょっとだけやりました。
当日
自分がした変更を中心に書きます。サーバ分割とか書かれてない部分の改善を他のメンバーがやってくれてます。
なるべく作業箇所が衝突しないように独立して動いてましたが、そのために他のメンバーがやってたことをよくわかってないです。
各種実装や設定をサルベージし、デプロイできるようにする
いつもは丁寧にscpしてましたが、今年はGUIを使いグリグリひっぱってこれて楽でした
fillLivestreamResponseのN+1を修正
タグをJOINする
フォールバック用のアイコン画像のハッシュを事前計算しておく
デフォルト画像は常に同じなので、ハッシュ値をコードに埋め込みました
10_schema.sqlをinitialize時に毎回実行するようにして、かつ毎回DROP TABLEする
初期状態では全くといっていいほどインデックスがなかったので貼ろうと思ったのですが、
今年はDBの初期状態が軽めなので毎回1から構築しても問題なさそうでした。
これでインデックスを貼りやすくなりました。
インデックスをぺたぺた貼る
スロークエリとか重いっぽいエンドポイントの実装を見ながらそれっぽいカラムに貼っていきました
users
テーブルに格納し、毎回取り出して計算しないようにする
アイコン画像のハッシュを何故か初期状態のユーザは誰もアイコンを持ってなくて、あとからAPI経由でアイコン設定されるのみでした。
その際にアイコン画像のハッシュを計算してカラムに保存しました。
この手の変更っていつもは大変な印象なのですが、今年は上述の仕様により簡単にできました。
fillUserResponseを消滅させる
ユーザ情報を取得するためにJOINではなくテーブルを1つ1つクエリしてアプリケーション側で合体していた処理を、JOINして一撃で取れるようにしました
getIconHandlerを一撃にする
ごちゃごちゃしていた処理を1つのSELECTにまとめました。
確実に速くなるでしょうが、特別遅いわけじゃなかったここを改善をしたのは謎です。
アイコン画像がDBに保存されてることも良くないですが、ボトルネックではないと思ってたのと、304を返せる手段が用意されていたことからファイルからの配信は目指しませんでした。
今思えば、既存画像がない分簡単そうなのでやればよかったです。
このあたりで13時を回りました
NGワードの削除ロジックのN+1修正
やたら複雑なロジックになっていたのですがシンプルなDELETE文でできそうに思ってやってみたらベンチに怒られなかったので、よしとしました。
DELETE FROM livecomments
WHERE
livestream_id = ?
AND comment LIKE ?
fillLivestreamResponse呼び出しのN+1を修正
fillLivestreamResponseが内部でN+1してましたが、さらにこれ自体がN+1で呼び出されていたので、まとめて複数のLivestreamを取得できる関数を用意しました。
IN句を使うのがsqlxだとちょっとむずかしくてChatGPTくんに聞きました。
Tagをバルクインサート
N+1のINSERTだったのでバルクでできるようにしました。
シンプルだったので、周辺コードをコピペしてChatGPTに丸投げしたらいい感じにやってくれました。
ただINSERTがゼロ件だった場合を考慮できておらずシンタックスエラーになっていて、その原因究明に時間がかかってしまいました。
If-None-Matchに対応
ハッシュはDBに保存されてるので、それを見て304を返すだけでした。
スパム判定のN+1対応
NGワードと同様にやたら複雑でしたが単なるSELECT文に置き換えられました。
結果
最終的に、
サーバA: App(名前解決後)
サーバB: MySQL
サーバC: App, DNS
という構成で最高37,364点が出ました。
途中、DNSに関わるエンドポイント以外を全てサーバAに向けてみたのですが、何故かスコアが10,000点ほど下がってしまいました。
サーバCが過負荷でAは余裕があったのでスコアが上がると思ったのですが、そうはいかず不思議でした。
ここまでは良かったです。
最後の残り15分でセルフ再起動試験をした結果、DNSが謎の状態に陥ってエラーとなり、解決できずそのままFailしました。
整合性チェックに失敗しました
Post "https://jGNJiMsMcy.u.isucon.dev:443/api/livestream/reservation": 「jGNJiMsMcy.u.isucon.dev」の名前解決に失敗しました (rcode=3): ベンチマーク走行が継続できないエラーが発生しました
あとから気づいたのですがDNSが変な状態になったときのリセット手段がマニュアルに書いてありました。
それを試せば回復したかもしれません。
当日は焦ってそれにたどり着けませんでした。
感想
初Failでフィニッシュしました。
PowerDNSについて私は全くの未知で、変にトラブったときに何もできませんでした。
今年は計測をうまく活用できてなくてがむしゃらに改善する形になってしまったのを反省しています。
kataribeとslowlogsを見て遅そうな部分を改善していったのですが、以下のようなエンドポイントを改善できませんでした。
- それらには集計されない、パスパラメータが異なる小粒の大量リクエスト
- スコア計算の仕様上重要なリクエスト(ユーザ登録、長時間配信者の枠取り)
- P95くらいで高速だけど残りが遅いエンドポイント
kataribeとslowlogsの遅いエンドポイントを概ね潰したのにスコアはほとんど伸びてない、という状況になり、一時改善の方向性を見失ってました。
X-Ray
あまり活用できませんでした。
X-Rayは1つのエンドポイントのうちどの部分が遅いかを可視化してくれますが、
実装をみれば遅い理由はだいたい分かるので得られる情報がありませんでした。
X-Rayの高度なクエリを使いこなせば何か変わったのかもしれない・・・
コーディング
今年はChatGPTとCopilotのおかげで、普段書かないGoでもそれなりの速度でコードが書けました。
特にCopilotには驚かされるばかりで、自分がイメージした通りのクエリがそのまま出てきて便利でした。
Goの弱みである冗長な記述がCopilotで解決されてて未来を感じました。
読む分には相変わらずダルいのですが。
Discussion