こんにちは、@p1ass です。
前回のブログに引き続き、ISUCON 12 の本選に @km_conner と @atrn0 で参加しました。
結果は、参考スコア 218,852 点で失格になりました 😭
PASS してたら総合 5 位のスコアのようです。 僕のミスのためとても悔しいし、申し訳ない気持ちでいっぱいです。
この記事ではいつも通りやったことを書こうと思います。
GitHub はこちらです。
なお、スコアグラフはこのようになりました。
事前準備
予選終了後、ISUCON 10 の本選をみんなで解きました。 本番は作業を分担しますが、このときはペアプロを行い、お互いの知見を共有し合いながら進めていきました。
各自初動の対応をする
競技がスタートしたら、予定通り初動の対応をしていきました。 今回は予選と異なりいつも通りのオーソドックスな構成でした。 そのため、特に詰まることもなく準備ができました。
その過程で、user_presents
のデータ数が数百万オーダーで飛び抜けて多いことを確認しました。
また、インスタンスが 5 台もあって、「複数台構成が想定解なんだろうな〜」と思いながら作業してました。
DB の接続を待つようにする (@atrn0)
アプリが DB より先に立ち上がっても問題ないように Ping ループを追加してくれました。
MySQL を 2 台目に移す (@KMConner)
メトリクスを見ると DB が支配的なことは分かっていました。 ソシャゲがテーマであることから Write ヘビーな特性であることが予想され、メトリクス的にもユーザーで DB のシャーディングが良いんじゃないかという話になりました。
とはいえ最初の一手としては重たいので、まずは DB を 2 台目に移すことにしました。
obtainPresent の N+1 を潰す (@atrn0)
user_present_all_received_history
の SELECT が N+1 になっていたので、IN
で取ってこれるようにしてくれました。
この時点で 8000 点くらいです。
DB のインデックスをたくさん貼る (@p1ass)
初期状態ではほとんどインデックスが貼られていなかったため、ペタペタと貼っていきました。
ID 生成をアプリ内でやる (@KMConner)
予選と同様に DB で採番する仕組みになっていたので、アプリケーションで採番できるようにしてくれました。
obtainPresent の N+1 を潰す (@atrn0)
user_presents
と user_present_all_received_history
の INSERT が N+1 になっていたので潰してくれました。
スコアは 12,000 点ほどです。
receivePresent の改善 (@p1ass)
receivePresent では複数のアイテムを同時に受け取れますが、一つ一つ INSERT するようになっていたので、ON DUPLICATE KEY UPDATE
を使ってアイテムの種類ごとに Bulk INSERT or UPDATE できるようにしました。
ここでバグを埋め込んでしまい後々失格になる原因になりました。 しかし、ベンチは普通に PASS していたため気づくことができませんでした。
スコアは 20,000 点くらいでした。
drawGacha の N+1 を改善 (@atrn0)
user_presents
の INSERT を Bulk Insert 化してくれました。
Nginx のチューニング (@p1ass)
ここまで改善してきたのにスコアが 20,000 点でサチっていいて何かおかしいよねという話になりました。
色々と調べてみたところ、/var/log/nginx/error.log
に Too many open files
のエラーがたくさん出ていたので、ファイルディスクリプタの上限などをガツンと上げました。
これによりスコアが 60,000 点ほどまで上がりました。
DB を 2 台にシャーディングする (@KMConner)
DB はシャーディングするべきだよねという結論になったため、時間を取ってシャーディングしてくれました。 シャーディングの仕組みは userID の余剰を用いています。
スコアは 120,000 点ほどになりました。 この時点で 15:30 を回っていました。
versionMaster をインメモリでキャッシュする (@atrn0)
このあたりからアプリケーションを複数台構成にすることはなく、DB を 4 台にする構成を検討し始めました。 そうなると、アプリ側で適当にインメモリキャッシュしても問題ないという話になり、インメモリキャッシュを作りはじめました。
user_devices
や user_bans
でデータを引っ張ってこないようにする (@p1ass)
存在チェックすれば十分なところでは以下のように 1 を返すようにしました。
SELECT 1 FROM user_bans WHERE user_id=?
user_sessions
をインメモリでキャッシュする (@atrn0)
良い感じにインメモリでキャッシュしてくれました。
DB4 台構成にする (@KMConner)
今まで 2 台だったところを 4 台構成にしてくれました。
このあたりで 160,000 点くらいになってきました。
listPresent の COUNT を消す (@KMConner)
わざわざ COUNT を打たなくても計算可能だったため、クエリを 1 つ消してくれました。
ログなどを消す (みんな)
終了時間が近づいてきたので、ログや netdata などを無効にしました。
ベストスコアの 220,000 点をマークして競技は終了しました。
結果発表
私は結果発表時にさいたまスーパーアリーナにいて、 @atrn0 は映画館でトップガン マーヴェリックを観ていたので、 @KMConner 以外リアルタイムで確認できなかったのですが、結果は失格でした。
失格理由は次の通りです。
ブラウザのゲーム画面からガチャを引き、プレゼントを獲得後にアイテムを開いたのち、「素材アイテム」「時短アイテム」を確認しても、獲得したものが 1 件も表示されない。
後々確認したところ、プレゼントを受け取る処理で早期 return によって INSERT してなくても成功レスポンスを返していることが判明しました。
受け取ったプレゼントを取得すると所持品に追加されるのはオーソドックスなシナリオなので、ベンチマークで検証してくれるだろうとたかをくくっていました、、、😇
自分でもちゃんとブラウザ動作確認をする重要性を実感しました。
やりたかったけどやれなかったこと
実はやれなかったことはあまりなく、後半の方はあまり大きな手を打てず手詰まりになっていました。 そのため、地道な改善をちょっとずつ入れていくことになりました。
今後の課題としては、インスタンス数が多くてデプロイに時間がかかっていたのでデプロイの並列化の方法を模索したいなと思っています。
感想
チームの役割分担
今回は@atrn0 と@KMConner が大きめを実装をしてくれ、僕はメトリクスを見たり判断することが多い分担になりました。
そのおかげか二人より余裕があったため、二人が詰まっているときに横からペアプロしてバグをスムーズに見つけることができました。これはとても良い動きだったと思っています。
一方で僕はコンテキストスイッチが多い立ち回りになってしまい、失格のバグを埋め込んだところは実装中に 3 回以上実装を中断していました。 もし集中できていたらバグに気づけてたかもな〜と思ったりしましたが、代わりに他の人のバグが見つかってスコアの上がりが早くなっていたので悩ましいところです。
来年はベンチでは検出できないブラウザ動作チェックでのミスを機械的に見つける方法を模索したいです。
上位に近い参考スコアが出たことへの驚き
ここ数年 ISUCON に参加していましたが、こんなに高いスコアが出ることはチームメンバー全員なかったです。 この 1 年で ISUCON に向けた練習をたっぷりしたわけでもないし、なんでなんだろうね〜という話をチームメンバーとしていました。
社会人になっていつの間にか学んでたことが多かったのかもしれません。
来年はちゃんと PASS してスコアを残せるようにしたいです!
おわりに
初の本選でしたが、とても楽しい 8 時間を過ごすことができました。
チームメンバー、ISUCON の運営の皆さん、本当にありがとうございました!