ISUCON 10 予選通過してきた

ISUCON 10 に参加して、ISUCON 5 以来の予選突破を果たした。

チーム名は「へしこず」。福井の珍味の複数形である。ISUCON 5 からずっと同じ名前・同じチームメンバーで参加し続けている。

ここ数年はだいたい予選突破の 5 位くらい届かずな感じの位置につけていた。が、去年は最後までスコアが伸びず一か八かのお祈り実装で最終スコア 0 というやらかしをしてしまった。今回は、そこで反省したことが活きた感もある。

以下、自分が担当した部分を中心に書く。

予習: node.js でいくぞ

予習した結果、node.js を選択した。おそらく予選通過チームでは珍しい。

ISUCON 5 から 8 までは ruby で参加していて、去年は「ISUCON ではやたらと golang が強い」という周りの空気に流されて、とうとう golang で参加した。アプリケーション側の実装は毎年そこまで重くはないという判断もあった。

が、これは正直かなりのミスだった。いくらコードの実装が重くないと言ってもゼロではないし、何よりそのちょっとしたコードを書くときに実装を読んで何かに気付くということが激減していた。

今回は 9 の予選を WSL2 上で動かしてみて、node.js コードが typescript で書かれているのに気づき、かつクラスター対応を入れてやれば golang と大差ないスコアになるということに気づいたので、これで行こう!となった。

また NewRelic もすんなり入り、これはいける!という感が高まっていた。

はじまり: まさかのDeno

SSH 接続に少々苦労したけど、基本的にはレギュレーションに書かれていた方法で接続できた。

SSH Config を書けば VSCode SSH で接続できるようになるので、すんなり開発スタート。ちなみに、VSCode は結構メモリを食うらしいので、もしかしたらよろしくなかったかもしれない。ただ、少なくともうちでは、1人1台割り当てにしてたからか知らないが、特に問題にはならなかった。ベンチが低く出てた可能性はなくもないけど。

メンバーが github にコミットできるようにしてくれたので、バックアップを取りつつ基本はサーバ直書き。

ブラウザから見れるようにするところは、下記のようなコマンドにした。運営から案内されてるやつではつなげなかった。ngrok 使ったという random の投稿をみて「なるほど~」とおもった

ssh -N isucon-server -J isucon-bastion -L 8080:localhost:80

で、早速問題が。 TypeScript じゃねーじゃん!ってか Deno?!?!

……たしかに、JavaScript は node.js, TypeScript は Deno とすれば、TypeScript でやりたくない人の希望にも添えるんだろうけど、マジかーとなった。今思うと、運営は JavaScript といっているのだから TypeScript である保証はどこにもないのだ(保証あったほうがいいとは思う)。
で、Deno で勝てるとは到底思えない(マニュアルにも不具合があるって書いてあったし)ので、node.js 実装を TypeScript 化するアプローチをとった。

  • app.js を app.ts にする
  • tsc --init で tsconfig 作って target を esnext に
  • VSCode の quickfix で型定義を全部インストール
  • tsc -w を回しっぱにする

これで 20 分くらいロスしたけど、運営の実装のバグを見つけたので良かったと思う。

あとは、CPU が 1 core だったので、予習でやってたクラスター対応は必要なくなったとか、NewRelic なぜかログ入らないし捨てようとか、そういう細々とした確認をした。

前半: ベンチを回せない戦い

前半は運営の問題でベンチを回せない時間が続いた(運営の皆様、お疲れさまでした。。

自分はひたすらコードを読んで怪しいところを決め打とうとしていた。で、見つけたのが なぞって検索 API。 lat, lon をそれぞれカラム保存していて、なぞった軌跡を全部含む正方形で最初に MySQL から select し、そのあと N+1 でポリゴンに各点が入っているかを MySQL に投げ直してる。 どうみてもワンクエリで結果を返せる。 しかもインデックスも何も効いていない。
「なぞって検索は目玉機能で、ユーザーはこの検索をトリガーにして椅子を購入したり資料請求したりするのだろう。とすれば、ここの改善は得点につながる」という何の根拠もない裏読みもカマし、ベンチが回せないまま、geometory カラムに変更・ワンクエリ化・SPATIAL INDEX設定までをやっておいた。
random みてると、おそらくこれは効いていたんだと思う(ベンチがまともに回る前にやっちゃったから実感ないけど)

中盤: 問題が見えてきた

ベンチ回せるようになってからしばらくは計測して重かったところをキャッシュしたりの繰り返し。ちまちまと細かい改善をしていたら、500 を突破したからか?マニュアルに書いてあった bot がやってきたので、それを nginx とアプリケーション側で弾いたら、769 まで上がった。
(nginx であの正規表現は辛かったので、一部はアプリケーション側で弾いた)

そうしているうちに「DB がヘビーな結果、どの API も万遍なく重い」という今回の問題の主要な問題点が見えてきた。よくよく考えたら 1 core 2 GB メモリで MySQL を抱えているのだ、重くないわけがない。DB の負荷を下げるために下記のような対応をした。

  • DB とアプリケーションサーバを分ける
  • キャッシュ用の Redis を立てる
  • low_price のキャッシュ実装(自分担当)

ここで 1293 に。周りのチームのスコアを見るに、そこそこ上位にいるっぽかったので、この方針で DB 負荷を下げていくことにした。

終わった後、random みてて estate と chair で DB を分けるってのを見て、うわーーーってなった。リードレプリカの話は出たけど、1mm も思いつかなかった。。

終盤: スモスモスーモが脳内ループ

ベンチのスコアがリアルタイムに出るからついつい眺めちゃうんだけど、眺めてる間ずっと スモスモスーモが脳内ループ するのでつらかった。

initialize で入れ込む資料請求の ID が連番で 29500 まできっちり入っていることに目をつけ、資料請求 API で DB 接続とかなしに29500 以下だったら ok を返すようにした。
資料請求 API でやってることが存在チェックして ok を返すだけだったからできた裏技で、出題者の意図ではなさそうだなーと思う。
ちなみに、物件は後からどんどん CSV で追加され、かつ ID が CSV に入っているという謎仕様なので、29500 以上はちゃんと Redis にキャッシュするようにした。
(たぶんこれかなり効いてる……w)

また、GET /api/estate/:id すら重かったので、Redis にまるごとキャッシュした。estate が更新とか削除がない仕様だったからこういうキャッシュはやりやすかった。

そして複数台構成に変更し、redis は 1 に、mysql は 2 に、アプリケーションサーバは 1, 3 に置いた。これでそんなにスコアが変わらなかったのも、DB ヘビーということの裏付けになった。

あとは、メンバーが「椅子がドアを通るか」の判定を工夫してクエリを短くした。あんまり効いてないとのことだったけど、たぶん時間があればそのうえでインデックス工夫したりクエリ分けたりすれば効いた気がする。どうでもいいけど 200x200 の椅子ってなんだ。スーモが座るのか。

さらに、再起動試験もして、本当に最後の最後に、なぞって API の NAZOTTE_LIMIT が DB に対してかかってないことに気づき修正。のこり数分だったので「直前にこんなシンプルなの気づくのやめてよーやめてよー」と変なテンションになっていた。

ここで 2624 をマークしてタイムアップ。1時間前に止まったリーダーボードをみると20位にはいそうだった。

まとめ

最終的には一般 15 位で予選通過できた。

とにかくやることがどんどん出てくる中、仲間と相談しながらうまくトリアージできたのが、予選通過できた一番の勝因じゃないかなーと思う。そもそも「やることが山のように出てくる」ってのは正直自分の力ではなくて、分析が得意な仲間のおかげ。ISUCON 5 からずっと感じてることだけど、バランス取れたチームだなーと思う。

最後になりますが、運営の皆様ありがとうございました! 決勝もよろしくお願いします。