窓を作っては壊していた人のブログ

この謎のブログタイトルの由来を知るものはもういないだろう

ISUCON14にソロ参加し惨敗した

今年もやってまいりました、ISUCONの参加記です。 個人スポンサーに今年もなったし、ちゃんと参加記も書かないとね(?

前回: ISUCON12予選にソロ参戦して敗退しました - 窓を作っては壊していた人のブログ

あれっ、ISUCON13の参加記書いてない…?

ISUCON14では、相も変わらずソロ参加を決めて、見事に惨敗しました。 スコアは 8965点で 186位という結果でした。

スコア

と思ったら、本日ブログでの発表で 0 点になりました!!!!!!うわーん

ISUCON14 受賞チームおよび全チームスコア : ISUCON公式Blog

今回はどんな感じで進めたのかメモしていきます。

今年のリポジトリ

github.com

去年とのdiff

言語選定

ISUCON13の振り返りがまさかの存在しないという状態なので、リポジトリ を確認したところ、昨年はGo言語での出場だったようです。 おそらく去年はNode.jsアプリで頑張ろうと思っていたけれども、pyroscopeの導入が上手く行かずに、easyにflamegraphを見て改善出来るGo言語に移行したようです。

ですが、今年はそのリベンジを果たし、普段書き慣れているTypeScript + Node.jsで参加しました。

プロファイリング

昨年は断念しましたが、今年はNode.jsとpyroscopeでプロファイリングを行い改善を進めました。

https://github.com/yamachu/isucon14-2024-12-08/commit/1a9f55186d7e93f302887a8d197b311feb4a0d23

ローカルにpyroscopeのインスタンスをDockerで立てて、ISUCONのアプリケーションインスタンスへのsshでremote forwardしてデータを送るようにしています。

Host a
  HostName ここにインスタンスのipアドレス
  User isucon
  IdentityFile ~/.ssh/github
  ServerAliveInterval 60
  TCPKeepAlive yes
  RemoteForward 4040 localhost:4040
$ docker run -it -p 4040:4040 grafana/pyroscope server

コンテストが終わったタイミングで燃え尽きてdockerコンテナを破棄したためデータが消失したのですが、Flamegraphを見ながら時間のかかる場所を特定し、改善に繋げられたのはとてもやりやすかったです。

Config管理

前回はシンボリックリンクなどを頑張って設定していましたが、今年はsyncというMakefileのターゲットでcpする形式にしました。 加えて、editターゲットを追加し、そのままvimが起動する形式にして、パスを覚えたり入力することを省略しました。

自作ツールの導入

techblog.cartaholdings.co.jp

こちらで紹介している @yamachu/sql-index-helper を使用して、

  • 初期実装に対してとりあえずINDEXを張る
  • どのテーブルに対してSELECT, INSERT, UPDATE, DELETEが走っているかを特定する

を行いました。 アプリケーションを全部読んで把握する前に、何のレコードが増えるのか、どのテーブルのどのカラムが途中で変わり得るのかを最初に特定しIssueなどに流し込んでおくことで、その後の改善の方針決めにとても役に立ちました。

複数台構成

今年はついに複数台構成にチャレンジできました(1730ぐらいから始めたのでギリギリでしたが)。 topコマンドで見ている感じ、初期状態はDBへの負荷がとても大きく、改善していく上でDB負荷は減っていました。

しかし、アプリケーションのCPU使用率もリクエストが増えるにつれて大きくなっていったため、今年は複数台構成にチャレンジしないとだめだなと確信し、それを実現しました。

複数台構成の練習を今まで行ってこなかったためやり方がさっぱりでしたが、ISUCON公式の過去の攻略記事を見ることで実現できました、感謝。

ISUCON11 予選問題実践攻略法 : ISUCON公式Blog

これでスコアが1.n倍程度上昇し、早くから挑戦しておけば…と思いました。

しかし、複数台構成にしたことで、slowqueryのログのダンプだったり、ベンチ前のcleanupなどが漏れることが発生したため、何かしらいい感じの手段を取りたいなと感じました。

やったこと

ツールで雑にINDEX張る

前述したツールでINDEXを張りまくります。 このツールだけじゃ足りなくて、後からdescのINDEXを張ったり、TEXT型のカラムにINDEXを張ってしまったのを削除するなどの修正を行っています。

https://github.com/yamachu/isucon14-2024-12-08/commit/168a537cb475bb6b6426d216db4df90312aeb94c

これで数千点ほど上昇したはず。

pyroscopeの導入

Node.jsでのトレーシングはpyroscopeがメジャー(?)のようなので、こちらを導入しています。 コミット時間を見るとなんでこんなに時間がかかってるの?とお思いになるでしょう。

実はこれ使うの始めてで、その1時間ぐらい前はCloud版のGrafanaを使おうとして1時間ほど時間を溶かして、Docker上のやつに投げるかみたいな紆余曲折があります。 ローカルで十分なので、これは来年の課題ですね、初手で入れられるレベルにしたいです。

https://github.com/yamachu/isucon14-2024-12-08/commit/1a9f55186d7e93f302887a8d197b311feb4a0d23

ride_statusesの最新を保持するテーブルの作成

毎回LIMIT 1を使用し最新レコードを取るコードが大量にあり、またそこがボトルネックになっているのをslowqueryや、関数の呼び出し回数で見つけていたので、最新レコードを保持するテーブルを実装しました。

INSERTされたタイミングと、特定のUPDATEが発生した時にアップデートしてほしいんだけど…とGitHub Copilotにお願いしたら書いてくれたので、何度か走らせて修正を加えながら実装。 9割Copilotが書いたので、正直わかりません、Trigger初めて見たんだけど…となりながらコードに加えました。

https://github.com/yamachu/isucon14-2024-12-08/compare/2b33397aaca2b3f84443372607f6f7f9e0d52264...46c269a92c48303c8176441c4d1fd1124093db50

getChairStatsの高速化?(失敗)

SQLとその結果を利用する箇所を見てみると、特定の状態になっているride_statusのみ対象となっていたため、ridesとride_statuesをJOINして、特定の状態のride_statuesのみを対象をwhereで絞り込むなどしました。 コード全体を眺めた感じ、すべて同じ様な状態遷移を辿っているためこれで行けると思ったのですが、不整合が発生したため、ベンチを回した後revertしています。

https://github.com/yamachu/isucon14-2024-12-08/commit/c268ce1d31ed4f35422f7672c9bfd04b1f9941f6

chair_distancesテーブルの導入

オーナーさんがイスの状態を取得するAPIで使用されているSQLが特大の呪物で、slowqueryのログのてっぺんに常に鎮座していました。 これを倒さないと先に進めないのだろうなと感じたため、改善を行いました。

方針としては、これも椅子のlocaitonが変更したタイミングでchair_distancesテーブルも更新する、というものです。

ride_statusesテーブルで採用したようなTriggerが利用できそうだと思ったため、それっぽくGitHub Copilotに聞いてTriggerを書いてもらいました。

ログ出したりする他のコミットも含まれていますが、雰囲気は以下のdiffから確認できます。

https://github.com/yamachu/isucon14-2024-12-08/compare/7f1e65c043e940c6fe669fbf1d70518cd2dcde0e...5d3a269b040ce21379d914ecb54c1d363cbf074e

authのキャッシュ

middlewareからのmysqlへの通信が多くてなんだろうと思ったら、認証周りでした。 特にアプリケーションでupdate句なども走らないため、(実アプリケーションでやったらアカンですが)キャッシュを行いました。

https://github.com/yamachu/isucon14-2024-12-08/commit/d6fd8497e930158f51a495daf6cb4c1e3607e209

chairGetNotificationの改善

1クエリで呼び出せるのに3個ほど呼び出している無駄なクエリがあったため、JOINでがっちゃんこしちゃいます。 latestテーブルを作成したおいて良かったと感じるタイミングです。

https://github.com/yamachu/isucon14-2024-12-08/commit/1987855b534d072e7899175f1f37a8d92f8bea2c

最新のridesの保持(失敗)

最新のridesを保持しておくと、いろんな箇所で使えそうだったので導入しましたが、整合性が取れずrevert。 OnMemoryに持たせることも試しましたが、これも失敗。

https://github.com/yamachu/isucon14-2024-12-08/compare/1987855b534d072e7899175f1f37a8d92f8bea2c...02609430622e1ee3032a4b7cb2106eb157926de9

getChairStatsの高速化リベンジ

さて、先程も行った施策ですが、今回はjoinの条件に入れています。 前回はwhereですが、今回は、JOINの条件です。

これで通りました…

https://github.com/yamachu/isucon14-2024-12-08/commit/0b2a8fd14efa3229b4eb80042b95bedbf9f62f0d

最新のride_statusesの保持をオンメモリも含めて(失敗)

クエリの最適化は行いましたが、やはり呼び出し回数が多いため、オンメモリで保持しようとしました。 これも不整合が起こり、失敗に終わっています。

https://github.com/yamachu/isucon14-2024-12-08/compare/0b2a8fd14efa3229b4eb80042b95bedbf9f62f0d...c41a32532390f69a508ad9454c9a73b810715fe8

ログの停止

微々たるものではありますが、ロガーの出力がFlamegraphに見えていたので消してしまいます。 ついでにpyroscopeも止めてフィニッシュです。

https://github.com/yamachu/isucon14-2024-12-08/commit/31a85f660c4c66c62b0f7d58f44c4a71532f337f

終了後

N+1のここはまだ倒せたな、とか素振りしてpyroscopeを早々に導入できるようにしておけばよかったなどの反省をしつつ、公式YouTubeをだら見して、今に至ります。

今年は今まで出来ていなかったチャレンジが出来た回ではあるので、自分としては満足ですが、スコアをもう少し上げられるように練習をしないとなと思いました。

今年もとても楽しいISUCONでした、また来年も、ソロで!参加します!!!!!!! 対戦よろしくお願いいたします。

運営の方々今年もありがとうございました。