🏖️

ISUCON 12の予選を突破した話

2022/07/30に公開

@tosa_nowです。
ISUCON12の予選に、今年も@masibw@sanpo_shihoと出場して17位で予選を突破しました。
チームでのISUCON参加も三回目です。

ISUCON12 オンライン予選 予選結果と本選出場者決定のお知らせ

この記事では雑に自分の当日の改善や考えてたことについて紹介します。

リポジトリ
https://github.com/Zatsu-X-Club/isucon12q

練習

チーム練習は軽い初動確認一回と、チューニングまで時間をかけて練習の一回の二回行いました。
夏なのでビールが飲みたいといった話をしていた記憶しかありません。

競技開始

開始直後は45分くらい時間をかけて環境整備に費やしました。
githubにコードをあげたりpprofを導入したり再起動試験対策のコードを仕込んだりは初動確認の際に複数回行っていたため非常にスムーズに環境整備できたと思います。
「できたと思います」と言っても自分は本大会から環境整備の役割がなくなり無職になったのでこの間にアプリケーションマニュアルやレギュレーションを読んだりしていました。
以下、自分がやったこととcommitを紹介します。

目に見えるボトルネックになりそうな場所にコメントを記載

add comment #2

最初の計測が終わる前にざっとコードをよんで明らかなN+1になっている部分やbulk-insertできそうな部分にコメントを記載しました。

再起動対策の導入

feat(isucon12q) 再起動対策にDBpingを導入mysqlのみ #8

pingをして接続を確認するように追加しました。

err := adminDB.Ping()

この辺りでsqliteが使用されていることにチームの人が気がついていて移行する必要あるかもね〜と話していました。が一旦はボトルネックの改善に集中することに。

retrieveCompetitionにキャッシュの導入

retrieveCompetitionにキャッシュの導入 #11

この時点でpprofの計測が終了しており、全体的にsqliteがボトルネックになっていそうなことを把握していました。自分はとりあえず計測して一番重そうなところから改善するように動いていたと思います。(当日のデータを撮り忘れていたためうろ覚え)全体的にlockがかなり重くなっていたのであんまりdbに触りたくないなとtenant_idからデータを取得している関数にidをkeyにしてcacheを仕込みました。

playerscoreのキャッシュを行う

retrieveCompetitionにキャッシュの導入 #11

playerscoreにもキャッシュを導入しました。UPDATEやDELETEで値をnilに置き換えるようにしています。

playerCacheの作成

playerCacheの作成 #15
retrievePlayerが重かったのでこちらもキャッシュを導入しました。idで引っ張ってくる関数はkeyが指定しやすくキャッシュさせやすいです。(ここまで書いて思いましたが今回cacheしかしていない...)

この時点でscore 3343と記載されているのでスコアとしては微増です。

VS billingReportByCompetition

add tenant id cache

次に支配的だったbillingReportByCompetition関数の改善に取り組みました。

billingの関数を分割で関数を分離して

引数から渡されている値を再度取りに行くことをやめて引数の値を使用するように。
feat(isucon12q) competitionsRowを引数のやつを使用する

競技が終了前であれば計算せずに値を返却。
feat(isucon12q) 終了する前だったら値を計算しない

一回一回SQLで取得していた関数を外出しにする。
feat(isucon12q) vhqをgroupbyでまとめて取得

player_idもまとめて取得するように変更。まとめて取得してもtenantDBごとにsqliteファイルが分かれていたためそこまで重くならないだろうと推測していました。
feat(isucon12q) pidlistを外に出して取得する

billingReportByCompetitionのN+1を改善し切ったので別場所から呼び出されていた部分も移行しました。
fix(isucon12q) billingReportByCompetition2

いきなり全てを改善しようとすると方針が立たず手が止まってしまうので、できる限り小さな改善に分割して行うようにしました。分割ごとにベンチを回すことでデバッグしながら、最終的にどう言った関数のIFになってくれると嬉しいかを考えると方針が立ちやすかったです。

ここらへんで@masibwplayersAddHandler bulk #25が入りスコアが1万点を超えました。

サーバー分割の方法を考え始める 16:10頃

時間も迫ってきたのでサーバー分割の方法をチームで考え始めました。
2つの案が上がっていたと思います。

  1. sqlite3をmysqlに移行して分割する
  2. tenant_idでサーバーを割り振る

1の案はinitializeが不安でうまく行くかわからない、2の方が現実的なのではないかとチームで考えて2で分割する方法を考えることにしました。(競技終了後にmysqlへの移行scriptが用意されていることを知りました...)
2の方法ではadmin系のapiが複数のtenant_idを触るため分割が厳しく、逆に他のendpointは複数tenant_idに触ることがなさそうだったのでadminのみ一箇所に集める、sqliteはidをシーケンシャルに分割しておいておき必要になったら分割したサーバーに問い合わせるという案が上がり一旦その方向で実装しようとなりましたが残り時間が短くて途中で断念。最終的にmysql + appの2台構成になりました。この分割で2万点を超えました。

参加者向けAPIのrankingのキャッシュ 17:30頃

分割も終えて終了間近でできそうなことを探していたらrankingの再計算の必要なさそうだとわかりキャッシュを導入しました。大体最終スコアのあたりになりました。

if competition.FinishedAt.Valid {
	rankCache.Set(competition.ID, CompetitionRankingHandlerResult{
		Competition: CompetitionDetail{
			ID:         competition.ID,
			Title:      competition.Title,
			IsFinished: competition.FinishedAt.Valid,
		},
		Ranks: pagedRanks,
	})
}

競技終了

残り20分くらいはチームで感想を話しあっていました。
(時間が余ったら感想戦の前に再起動試験対策の確認はきちんと行いましょう)

結果

全体20位で本戦に出場することができました。昨年は学生枠だったので一般枠でも出場することができてとても自信になりました。

感想

自分の中の反省点としては、ボトルネックを見つけて逐一改善はできたものの本質的な改善(lock)には手が出なかったり3秒まではまってよいなどのマニュアルの指示もあまり利用できずまだまだ未熟さを感じました。
また、サーバー分割ももう少し早い段階でappの構造を理解して方針を立ててれば3台にうまく分割できたかもです。
初めにsqliteからmysqlに移行するかしないかの話になった時にまずはボトルネックを改善する方針を取ったのは結果論(mysqlに移行しようとして苦戦していたチームが多かった印象)としてはよかったとは思っています。ただ、明確にボトルネックとなっている箇所を改善しない限りは入賞を狙えないと思うので本戦ではもっと大規模な実装でもひるまず挑戦していきたいと思います。

今回のISUCONの良かったところはベンチマーカーやportalがとても安定していたところです。
おかげで競技にとても集中することができて楽しかったです。運営の方々ありがとうございました。

予選を突破できて嬉しかったです!
来月の本戦も楽しみにしています。

自分のギャグはバズらないのに人のネタでバズってる様子
https://twitter.com/tosa_now/status/1550461524955004928?s=20&t=vJgqfT8NeSzvYrlyEc8l3w

Discussion