チーム 😇😇😇 として、id:nonylene と id:wass80 と一緒にISUCON10の本戦に参加しました。最終得点は1380114206点で16位でした*1*2。
最終ベンチマーク時点は final-submissionタグのcommit*3 にcheckoutしてました。いちばん点数が奮っていたときの結果を最終提出にしよう、ということで、残り10分でやりました。
お題
今回のお題は、パフォーマンスチューニングコンテスト「XSUCON」のポータルサイトをよくする、というものでした。ついにこのお題が出たか、というのと、これは大変なお題だなあ、というのを思いました。実際かなり重たい問題で、お題の発表直前にmirakuiさんが「やりすぎた」って言ってたのは間違ってなかったな、と思いました。
サーバー構成
サーバー番号 | 役割 |
---|---|
1 | envoy (ロードバランシング), nginx (静的ファイル配信), api (Golang) |
2 | web (Golang) |
3 | DB (MySQL) |
自分がやったこと
他のチームメンバーがやったことは、チームメンバーのブログ記事を参照してください。
@utgwkk のコミットログもあわせてどうぞ。
最初はNew Relicを導入してベンチマークを回してボトルネックを把握し、後半はpprofでプロファイルして作戦を立てる、という方式で作業しました。
New Relic導入 (~11:04)
予選と同様に、Git管理が完了したらまずNew Relicを導入しました。今度はどのライブラリを使うかを調べることなく、予選よりもスムーズに導入できました。gRPCサーバー・クライアント向けのNew Relicインテグレーションが存在して流石だなと思いました*4。
clarificationsのN+1解決 (~11:39)
GET /api/{contestant,admin}/clarifications
で、clarificationに紐づくteamを取ってくる部分がN+1になっていたので、INクエリで取ってくるようにしました。スコアは200点ぐらい下がったけど、これが効かないはずはない、やってはいけないわけがないのでそのままマージしました。
ところでこの実装をしているときに、つい最近ブログに書いた事象にちょっとハマっておもしろかったです。ブログに書いててよかった。
GET /api/audience/dashboard
のキャッシュ (~12:04)
本戦当日マニュアルを見ると、 GET /api/audience/dashboard
は1秒までキャッシュしてよい、と書いてあります。
アプリケーションは、データの更新から最大 1 秒古い情報を返すことができます。ただし、ベンチマーカーが検知しない限りはそれより古い情報を返しても構いません。
dashboardのクエリは明らかにやばく、また、ここを負荷低減できると効くことが分かっていたので取り組みました。GET /api/audience/dashboard
にリクエストが来るたびに、キャッシュを作ったのが現在時刻より1秒以上前ならキャッシュを作って返す、というのを仕込みました。
このPR時点で9357点でした。
pprof導入 (~12:33)
echo向けのpprofラッパである sevenNt/echo-pprof を導入して、pprofによるプロファイリングができるようにしたかったのですが、なんとecho v4系には対応していませんでした。仕方ないのでコピペしてimportを書き換えて対応しました……。
ダッシュボードのgRPCのmarshal結果をキャッシュする (~13:04)
GET /api/audience/dashboard
のキャッシュについて、gRPCのmarshalが重たいというのをpprofによって突き止めたので、marshalした結果をそのままキャッシュするように書き換えました。
GET /api/audience/dashboard
のキャッシュをさらによくする (~14:41)
そろそろ大会規模を大きくして点数ボーナスを狙おう*5、ということでTeamCapacityを10から60にすると、14000点ぐらい出るけどリーダーボード上の最終 ID 検証に失敗しました
というcriticalエラーが出てベンチマークが通らなくなりました。
dashboardのクエリでテーブル全体がロックされて、キャッシュの更新が間に合わなくなる、という仮説のもと、goroutineで一定間隔ごとにキャッシュを更新する仕組みを入れましたが、同じcriticalエラーでベンチマークが通りませんでした。最終的にTeamCapacityを大きくするのは諦めることになりました。これはどうしたらよかったんだろうな……。
プッシュ通知の実装 (~17:33)
GET /api/contestant/notifications
へのリクエストが多いのでWeb Pushを実装しよう、ということになりました。参考実装となるアプリケーションを参照しつつ(コピペで)実装したところ、とりあえず動いてはいるようでした。しかし全く動作確認しないままでベンチマークは通ってて、push-notifications の値は正なのでプッシュ通知されているのかな……? という感じでした。Web Pushの仕組みを理解するのに時間がかかって手こずってしまいました。
16:24 時点のcommitで最終ベンチマークを行ったので、この実装は結局採用されないこととなりました。
TeamCapacityを調整する、祈る (~17:58)
TeamCapacityを60にはできないけど15とか20ぐらいにはできないか、というのを試していました。20だとベンチマークが通らず、15だと通るけど点数が7000点ぐらいで打ち止めになってしまいました。
16時ぐらいに最高得点が出ていたので、このときのcommitに戻ってベンチマークガチャを回そう、ということになりました。16:24 時点のcommitにcheckoutして、TeamCapacityを15にしたらfailしたので、すぐに10に戻して最終ベンチマークを回しました。13801点が出たので、これで作業を終了することにしました。
感想
おもしろかったけどうまくいかなかった!!! というのに尽きます。毎年学生がどんどん強くなっていってて、上位3位は学生チームですごすぎる。全く手が回らなかった、という感じなので精進しようと思います。
dashboardのキャッシュさえうまくいったら先に進めたのに……という念が強いです。リーダーボード上の最終 ID 検証に失敗しました
というcriticalエラーについては、Discordの感想戦チャンネルで ORDER BY teams.id
を入れたら安定した気がするというのを聞いて、最新スコアと記録時間以外にも順序が仮定されていたのかが気になりました。競技終了まで全く心あたりのないエラーということで苦戦していて、いま思うとclarを投げたほうがよかったのかもしれません。LEFT JOIN地獄になってるのをバラしたりできなかったかな、というのも思います。プロファイルしてボトルネックを解消する、という動きにはなっていたはずなので、実装方針がよくなかったのだろうか。
当日マニュアルにConditional GETについての言及があり、すごく丁寧だと思いました。ISUCON7の予選でConditional GETに苦戦したのを思い出しました。
Go言語、さっと書けるけどcontextを引き回さないといけないというのがストレスだなと思いました。それ以外はコンパイル通ればヨシ! でデプロイ可能なので楽でした。
gRPCは出題されるだろうなと思いつつ知見がなく、手元に動作環境を用意するのを諦めたのでなかなか開発のボトルネックになっていました。クックパッドのスプリングインターンでgRPCに触れたのを思い出しました。
今までで一番おもしろくヘビーな問題になっていたと思います、運営の皆様ありがとうございました!! 来年もISUCONに出たいし本戦にはもっと出たいです。
最後にベンチマークのご様子をお知らせして記事を締めます。(巨大な画像なので「続きを読む」を入れています)
*1:ISUCON10 本選の結果発表と全チームのスコア : ISUCON公式Blog
*2:本選スコアデータおよび順位の誤りについて : ISUCON公式Blog
*3:16:24 時点のcommit
*4:結局gRPCサーバー側は眺めてないけど……
*5:コンテストの参加人数によって点数の倍率が変わる