例年は同僚と参加していたのですが、今年は予選申し込みが始まってから去年のメンバーに打診したら申し込み締め切りに間に合わなかったために、申し込みに成功してメンバーを募集されていた @catatsuy さんにお願いしてメンバーに入れてもらいました。 @catatusy さんの記事はこちら
結果は1503点で、惜しくもなんともない完敗でした。
序盤
事前に打ち合わせしていた通りに、お互いで共通の ssh_config を作る、除外すべき大きいファイルやディレクトリをチェックしつつ初期コードをコミットする(今回は適切に .gitignore がされていて git add .
だけでいけました)、ローカルの開発環境を構築しその手順を共有する、netdata をインストールし netdata cloud に登録する、アクセスログ集計のための alp コマンドのオプションを共有するといったことをしていました。
小さいトラブルはあったものの、ここまではスムーズで良かったと思います。
中盤〜終盤
alp の結果とアクセスログを見比べて、estateとchairの検索が遅いこと、検索クエリのほとんどがシンプル(検索条件が1つ)であることなどを把握しました。
検索は LIMIT X OFFSET Y
型式のページングと、 COUNT(*)
型式の総数表示があります。前者はインデックスでソートすれば早めに打ち切れるので降順インデックスが使える MySQL8 にすることで十分な高速化が見込めますが、 COUNT(*)
の方は工夫しないとインデックス全体のスキャンになります。MySQL側で工夫するよりもアプリ側で処理する方が早いと判断してオンメモリ化を始めます。。。。が、ここからドツボにはまります。
最初に簡単な estate の検索から始めたのですが、 CSV入稿に失敗したというエラーや /api/estate/search
の結果が違うというエラーに悩まされます。安定してエラーになるのならまだ良いのですが、パスしたりエラーになったりするし、エラーの内容を教えてくれないのもあって迷走しました。
午後7時ごろ、迷子になった出前の配達員を迎えに走っていたときに、元は ORDER BY popularity DESC, id ASC
だったのにアプリでは popularity の降順ソートしかしていないことに思い当たります。しかしそれを修正してからもエラーになったりならなかったりは続きます。
最終的に、オンメモリのデータの件数だけなら利用してもエラーにならないと判断し、 SELECT COUNT(*)
を削除することにしましたが、スコアはあまり上がりませんでした。
反省点と感想
最近のISUCONだとベンチマーカーが間違っている点を詳しく教えてくれる事が多かったためにベンチマーカーをテストに使うという戦略に依存してしまい、そのためエラーの原因が最後までわからずに迷走してしまいました。せっかくローカルで動作する環境があったので自分でしっかりテストをするべきでした。
ただ、その反省を消化しようとローカルでオンメモリ版とDBを使う版でいくつかの条件でAPIを叩いてみてもAPIが出力するJSONが完全一致したので、いまだにエラーの原因がわかりません。後日ベンチマーカーが公開されたら改めてデバッグしようと思います。
またオンメモリ化も定型的な作業で自分なら楽勝だと信じきってしまっていたのもドツボにハマった原因です。次の機会ではよりシンプルでバグの少ないその他の選択肢(MySQLあるいはアプリでのクエリキャッシュを使うとか)を先に検討しようと思います。
最後になりますが、今年の問題はボリュームがとても小さいのにやらなければならないことは十分に多い良問でした。自分の実力と勉強不足を痛感させられました。ありがとうございました。