😇

ISUCON12で惨敗しました

2022/07/24に公開2

リポジトリはこちらです。アプリケーション担当とインフラ担当の2人で参加して、私はアプリケーション担当で参加しました。チーム名は「帰ってきた鍋部」というチーム名です(同じメンバーでISUCON10本選の並行チームで「鍋部(2人前)」というチームで参加したのと、ウルトラマンの年ということでこのチーム名にしました)。

https://github.com/catatsuy/isucon12q

1万点を超えていたタイミングはありましたが、最終的にベンチマークが通らなくなり0点で終わるという惨敗だったので反省ブログです。

反省点1:手元の開発環境にこだわった結果、2時間消費してしまった

ISUCONだとどうやって開発するかが重要です。今回の問題のつらかったのがサーバー上に謎のバイナリが起動していて、そちらが認証を行っていた点です。認証を行うために同じドメインでAPIを提供する必要があるのでnginxでproxy_pass先を変える実装になっていました。ここで大きく2つの問題がありました。

  • nginxが静的ファイルの配信とproxy_passを行っており、Go単体で開発できるようになってない
  • 謎のバイナリのソースコードは非公開のため手元で動かす方法がない

ここで以下のことを行いました。

  • Go側で静的ファイルの配信とproxy_passの実装を追加
  • ポートフォワードして認証サーバーを手元でもアクセスできるように

しかしこれだけでは認証サーバーが返すJWTのドメイン名のチェックが突破できません。マニュアルに書かれているドメインの.devはHSTSの設定があるのでHTTPでアクセスができません。そこでドメインは.testドメインを利用しつつ、ISUCON_ADMIN_HOSTNAMEISUCON_BASE_HOSTNAMEの2つを指定してやっと手元の開発環境が動くようになりました。

ここまでのことをやるためにコードを読んだり変更する必要があったので、これをやるために2時間を費やしてしまいました。

これについては多分本番のサーバーで開発するのが正解だったのだと思いますが、ISUCONだとインスタンスのメモリが足りなかったり、変な設定を追加するとレギュレーション違反を指摘される可能性もあるので、あまり本番のサーバーで開発したくなく、どうしても手元の開発環境を作りたいという気持ちと、あと少しで開発環境が作れるはずと思い込んでいたのでなかなか決断することができずにズルズルと時間を浪費してしまいました。

手元で開発できるようになったのは中盤の開発では効果を発揮したものの、後半のドラスティックな変更に手元が付いていけず、結局使い物にならなくなってしまいました。この辺りの判断は難しいところですが、思い切りの悪さが敗因でした。

反省点2:SQLiteの時点でパフォーマンスの確認やコードの変更を行うべきだった

今回の問題はMySQLとSQLiteの両方を利用していることが非常に特徴的でした。MySQL側のボトルネックは比較的簡単に剥がせた(後述)のですが、SQLiteのボトルネック解析方法が分かっておらず、とりあえずMySQLに移してから確認する方針にしました。

この判断は移行用のスクリプトがコードに含まれていたので、そういう想定なのかと思って特に疑うことなく進めてしまいました。

しかしコードを見る限り明らかにインデックスが貼られていない部分とN+1クエリがありました。それらは別にSQLiteのままでもアプリケーション側は改善することができたのでSQLiteのままで調査して進めるべきでした。というのも移行したことでinitialize処理の30秒制限と戦うことになったからです。

  • initializeの処理がSQLiteのファイルを削除した上で元のファイルをコピーするという豪快なinitialize処理が行われていたので、MySQL側もレコードを全部削除して元のデータを持ってくる必要があった
    • データをimportする時間はないのでo_competitionのようにo_というプレフィックスで元データを持っておき、initialize時にテーブルをコピーする実装にした
    • インデックスのコピーもしたかったのでCREATE TABLE competition SELECT * FROM o_competition LIMIT 0みたいにしてテーブルを作成してからINSERT INTO competition SELECT * FROM o_competitionをしたり、最初からインデックスを貼ると間に合わないのでINSERTしてからALTER TABLEをすることでインデックス作成処理を最後にまとめて行うなどのいくつもの工夫を行うことでギリギリ突破したが、小手先のテクニックを駆使することになって時間を浪費してしまった
  • SQLite側のファイルがテナント毎に分かれていたのでテーブルを統合したかったが、テナントIDを含むように変更するのは大変すぎるという判断をして、データベースを分けるという判断をしてしまった
    • 実は全テーブルになぜかtenant_idが含まれていたので、この心配はなかったのに気付いていなかった
    • データベースを分けるためにシェルで直接mysqlコマンドをGoのアプリケーションから実行するという最悪なコードを書いてしまった
      • そしてinitializeの時に追加されたテナントのデータベースを全部削除するコードも追加していた
  • 中盤でインメモリ化していたvisit_historyの初期データをインメモリにimportする処理も時間がかかりすぎていたので、GROUP BYの結果を別テーブルに保存した上でそのテーブルからimportする処理に変更
  • それでも間に合わないのでテナントIDの偶奇で処理するMySQLサーバーを分けてギリギリ完了するように
    • そもそも毎回コネクションを貼る手抜き実装だった

この問題と戦わない限り、私が入れようとしてたN+1クエリ改善のコードのリリースが行えない状況になり、本質ではない部分を全力で戦う羽目になりました。

既存のSQLite側のコードの時点で改善を加えてたら以下のことに気付けたし、N+1クエリ改善のコードもリリースできて、その後MySQLへの移行もすんなりできた気がします。

  • tenant_idが含まれているから同じテーブルへの移行が可能
  • player_scoreテーブルの仕様変更の必要性に気付くことでデータを圧縮してinitialize処理の時間を縮められた

それとinitializeの時間がかかるならMySQLを停止した上でdata_dirをそのままコピーするという手段も取れました。MySQLに詳しくなければ思いつかない手法な気がしますが、自分のチームは2人ともMySQLのこの辺りのオペレーションには慣れていたのでチーム的には普通に取れる作戦だったのに、それをやる発想が出てきませんでした。

またSQLiteのファイルをtmpfsに置くことで8000点近いスコアを早い段階で出せていたのに、MySQLに移行してしまうとN+1クエリ改善を行わないと元のスコアに戻せないので何の作戦もなくMySQLに移行するのは悪手でした。もっと作戦を考えてから戦略的にやるべきでした。

反省点3:複数台構成にできなかった

MySQLはinitializeを通すために2台構成になっていましたが、無理矢理でしたし、nginxでアプリケーションを分散することは失敗しました。

ちゃんとコードを追いかけられてなかったのですが、認証サーバー周りも考えないと分散できなかったような気がしています。この辺りのコードを追いかけ切れてなかったのは致命的でした。やはりISUCONはアプリケーションの挙動理解が重要です。

反省点4:ラスト10分でベンチマークが通らなくなったのにエラーログを見ずにベンチマークを再実行し続けた

ラスト10分でベンチマークが通らなくなったのに、特に原因を確認していませんでした。最終的にラスト1分前くらいにulimitの設定周りでエラーが出ていることに気付きましたが、後の祭りで個人的に初めて0点でフィニッシュしてしまいました。

ulimit周りの設定は最初から何も考えずにやるべきだったというのはありますが、やはりベンチマークが通らなくなったらまずはエラーログを見るべきだったのに、時間がないからという言い訳で基本を無視した行動をしてしまったのが完全な敗因でした。

どんなときでも基本に立ち戻り、常に冷静に正しいことをやる必要があるということをISUCONから再認識しました。出直してきます。

よかったこと

逆に良かったことは以下です。

  • 2人チームでインフラはチームメイトに丸投げできたので自分はアプリケーションに集中できた
  • 中盤の最適化は割とうまく行っていた

中盤の変更は以下の感じです。

  • dispenseIDsync/atomicで適当に採番
  • visit_historyはインメモリキャッシュで捌く
  • SQLiteはtmpfsに置いてとりあえずスコアをある程度上げる

中盤の変更は割と短時間に実装が完了し、ちゃんと成果が出せていたので良かったのですが、やはり序盤の遅れと終盤の迷走がマズかったです。

アプリケーション構成への感想

アプリケーションだけDockerにする構成は実は私が運営だったISUCON9予選でも私が提案した構成でした。結局当時は使わなかったのですが、運営目線だとISUCONのように多言語の環境を用意しないといけないのは非常に面倒なのでDockerファイルを置くだけで解決する構成はかなり合理的です。しかしMySQLやnginxなどすべてをDockerで提供しようと思うと、かなりファイルを作り込む必要が出てきて難易度が上がってしまいます。なのでアプリケーションのみDockerでそれ以外はホストにインストールする構成は今後も使われる構成ではないかと感じています。

実はDockerは私が運営だったISUCON6本選で初めて使われて(インフラ周りの設定を用意したのは私でした)、そのときはかなり苦情が出ました。その後は本選で何度か使われていますが、予選でDockerが使われたのは今回が史上初です。今となってはDockerに対して苦情が出ることもないので、時代は変わったなぁと思ったりします。

ちなみに自チームでは取り回しの用意さを考えて早々にDockerを剥がしました。これはパフォーマンスを意識したものではなく、単純にアプリケーションだけならsystemdで直接起動するようにするのはすぐできるのでサクッと変えておきたいという理由でした。

認証サーバーを外に出すこと自体はいい試みだと思っていますが、各サーバーに謎のアプリケーションが起動している構成を理解するのが難しく悩む時間が長かったです。認証周りについては私自身もどうするのが正解なのかよく分かってないですが、ここについては今後も色々な試みがありそうだなと感じています。

今回の問題はインフラもアプリケーションもどちらも重量級で難易度が高く、総合格闘技と呼ぶにふさわしい問題になっていました。ISUCON運営の難易度がまた上がってしまったなと思いつつ、期待を更に超えていった今年のISUCON運営の人達には本当に感謝の気持ちでいっぱいです。本当にありがとうございました。

最後に

後半時間が足りず、N+1クエリ改善のコードが一部しか実装できなかったですが、一応1万点は超えていたので、もう少し時間が確保できていればもう少し成績を残せたかなと思いつつ、やはり判断ミスが多く、勝つのは難しかったなと感じました。

今回の問題はマルチテナントをSQLiteのファイルを分けることで実現するという、とち狂った実装(褒め言葉)になっており、時間内に正しい方針で実装しきるのは非常に難しかったです。ただ近年のISUCON参加者のレベルでは突破できる人も多数いて本当にすごい世界だなと思っています。

昔のISUCONではベンチマーカーを実装するのが大変というのがよく言われていましたが、最近はベンチマーカーの実装もノウハウが貯まってきて、運営側が更に新しいチャレンジができるフェーズに突入したと感じる壮大な問題でした。いつまでも進化をし続けるISUCONの運営は大変ながらも本当に素晴らしいなと感じています。

この問題の感想戦だけでもかなり色々話せる気がするので、機会があれば問題を解いた人同士で色々話したいなと思っています。本選に参加できないのが残念ですが、これからもISUCONに参加して自身の成長に繋げていきます。運営の皆様本当にありがとうございました。

Discussion

WispWisp

ISUCON12予選に今回初参加したものです。catatsuyさんの今までの記事が本当に役に立ちました。
この記事でもtmpfsなど新しいことを勉強させていただきました!
この場を借りてお礼させていただきます。ありがとうございました!

catatsuycatatsuy

ありがとうございます。これからも細々と発信していくと思うのでよろしくお願いします!