ISUCON12予選に参加した
ISUCON12予選に参加しました。 最終ベンチ結果は6815点で、最高ベンチ結果は10625点、failの可能性は高いです。 もし結果発表でfailしていたらブログを書く気に多分ならないと思う(あと記憶力が弱い)ので、今のうちに当日やったことを中心に振り返っていきます。
追記 (2022/7/25): failしていました :cry:
追記 (2022/11/26): プロファイラ的なツールを公開しました。 github.com
自分がやったこと
ファイルロックがボトルネックになっているっぽいことを見つけた
alpのログを見ると、ランキングやプレイヤー詳細のAPIがボトルネックになっていることが分かりました。
alpだとエンドポイント単位でしか見られないんですが、pprofはポートを立ててWeb UIから見なければ詳しくは見られない上にちょっと見づらい・・・ということで、このときから作っていたツール1 を使って、関数ごとにかかった時間を計測しました。
ISUCON用ツールを作ってたらISUCONの練習をする時間がなくなりそう
— シバニャン (@_6v_) 2022年7月16日
alpで一番重かったランキングAPI (competitionRankingHandler) を分割して見たところ、一番時間がかかっているのはSQL文ではなく、ファイルロックをしている部分(competitionRankingHandler 4)でした。
ファイルロックをする関数(flockByTenantID)も、時間がかかっていることが分かります。
ファイルロックを消した (failの戦犯)
SQLiteのロック適当に外したらたまにfailするようになってずっと泣いていた #isucon
— シバニャン (@_6v_) 2022年7月23日
お題のアプリケーションはマルチテナントSaaSで、管理側はMySQLを使っており、テナントに入っている側はそれぞれ別のSQLiteを使っていました。
ロックをかけている部分のコードには、
// player_scoreを読んでいるときに更新が走ると不整合が起こるのでロックを取得する
とコメントがあるのですが、DBの読み込み時にかかっている排他ロックが、読み込みが非常に遅い原因になっていました。
読み込み時には共有ロックをかけると読み込みが同時に行えて性能が上がるだろうということで、ファイルロックをやめてSQLiteのトランザクションを使おうとしました。
トランザクションに書き換えると、error retrievePlayer: error Select player: id=395f19764, database is locked
というエラーが出てしまうものの、スコアは5000点から10000点近くまで上昇しました。
追記 (2022/7/25): 正しくトランザクションが貼れていてもこのエラーは出るので無視してよかったらしい。そんな・・・
いろいろ試行錯誤した結果、このエラーが出たり出なかったりしてスコアが0になったり10000になったりしました。これが失格の原因になるのは困るということで、⚠️ロックを一切かけない⚠️ことにしました。このあたりで既に完全に沼に入っているので、冷静な判断ができていればrevertして5000点から再開していたと思います。失格になったらこの変更のせいなのでごめんなさい(泣)
テーブル単位で徐々にSQLiteからMySQLに移していってるのすごいなあhttps://t.co/8JcnHWHn1m
— シバニャン (@_6v_) 2022年7月23日
リーダーボードで上位だった方のコミット履歴を見ると、SQLiteからMySQLにテーブル単位で移していました。 SQLiteのロックを外すことを考えるよりも先にその戦略を取れば、普段使っているMySQLのトランザクションが使えるのでやりやすかったと思います。 SQLiteからMySQLへのリプレースは弊チームではリスクが高いと判断して取り組まなかったことを考えると、部分的に置き換えるというのはやはりISUCONにおいて重要戦略であると思いました。
追記 (2022/7/24): ファイルロックをsync.RWLockとsync.RLockにする案は思いついたんですが、tenant IDごとにロックを分けるためにmapを作る必要があって、そのmapを管理するためにロックが必要で・・・となって面倒くさそうでやめてしまいました。tenant IDは固定でmapはreadだけなので、本当はそんなに難しくなかっただろうなと思います・・・こっちでやればよかった:cry:
GoDocを誤読
— シバニャン (@_6v_) 2022年7月24日
ファイルロックをRLockにもしてみたんですが、failしたのでロックが取れるまで待つ実装は自前でやらなければいけないのかなと勘違いしました。It will wait until it is able to obtain the shared file lock.
とあるので、その方針でも自信をもってやればできただろうと思います。
https://pkg.go.dev/github.com/gofrs/flock#Flock.RLock
チームメンバーがやったこと
IDジェネレータをUUIDにした (AokabiC)
スロークエリログを見ると、id_generatorという謎テーブルへのINSERTが一番遅いことが分かりました。被らない連番IDを取るために、MySQLにわざわざレコードを追加して採番していたので、これをUUIDに変更してくれました。
REPLACE INTO id_generator (stub) VALUES ('S')
AokabiC氏の目の付け所はいつも洗練されているのですごいなあと思います。
App, DBの2台構成にした (zatton)
ISUCON12予選では3台のサーバーが与えられますが、AppとDBを分離するのは、 - htopをするとAppとDBのどちらがボトルネックになっているかわかる - 負荷が軽くなる の2点のメリットがあるため、早い段階で分離しました。
弊チームは、/etcなどの設定ファイルは権限周りが面倒なのでGit管理せずにzatton氏がVimでゴリゴリ直接書いていく方針で、zatton氏がサクッとやってくれました。
visit_historyへの書き込みを減らす (AokabiC)
visit_history
はランキング取得のタイミングで履歴を書き込むのですが、このテーブルは、最新のものしか取得されていません。そこで、visit_history_summerize
というテーブルを作って、最新のものだけを書き込むようにしました。インデックスも貼りました。
自分がやろうとしたこと
player_score を剥がす
ファイルロックがボトルネックから外れた後は、player_scoreテーブルを使う関数がボトルネックになってきたのでこれをRedisに移そうとしました。しかし、整合性チェックでfailしてデバッグする時間もなく断念。RedisではなくMySQLにInsertしている方が整合性に関して安心できるという点で良い方針だったのかもしれません。
チームメンバーがやろうとしたこと
3台構成にする (zatton)
3台構成にする際は、Appを2台(nginx+AppとApp)にするかDBを2台(リードレプリカや垂直分割)にするか、その他の構成にするかが悩みどころです。 今回は、2台構成にしたところAppのCPU負荷が100%に張り付いていたので、Appを2台にすることを考えました。しかし、Appサーバーのファイルシステム上にはSQLiteがあり、Appを2台にするならNFSなどで同期しなければいけないことが分かります。ということで、これは一旦放置することにしました。
player_scoreへの書き込みを減らす (AokabiC)
player_scoreもvisit_historyと同様に書き込みが減らせそうということで、取り組んでいましたが時間が足りず断念。
感想
既存のISUCON攻略法が使えない感じになっていてすごい問題だった #isucon
— シバニャン (@_6v_) 2022年7月23日
既存のISUCON攻略法を封じてくるような問題だったので、面白かったです。 具体的には、DBがSQLiteとMySQLに分離されていたので、MySQLのスロークエリログには出ないボトルネックがありました。
余談
当日は、SQLiteやMySQLがトレンド入りして盛り上がりました。
ISUCON界隈の外からはMySQLトレンド入りの原因が謎だったっぽい。
MySQLがトレンドに入っていて、
— 大和(で)哲 (@deyamato) 2022年7月23日
「なぜトレンドに? 」と思ってトレンドを見ると
大量の「なぜMySQLがトレンドに?」で構成されていた。
なんという再帰的な……。
— シバニャン (@_6v_) 2022年7月23日
ここは違うよ、というのがあれば、TwitterのDMで教えてもらえると助かります。