私が歌川です

@utgwkk が書いている

ISUCON13に参加した #isucon

チームSmiling Face with Halo、メンバーはいつも通り id:nonylene id:wass80 で出ました。

isucon.net

結果は0点でFAILでした。サヨナラ!! 73000点ぐらい出ていたのですが最後の最後にどこかでエンバグしたようですね。

リポジトリは公開済です。

github.com

サーバー構成

  • 1: app
  • 2: db
  • 3: app, dns, nginx

やったこと

自分がやったことをだいたい時系列順に書いていきます。

予想する

9:30より前に集合していたので、どんな問題が出るかね、さくらインターネットと言えばDNS水責め攻撃だけどさすがに出さないでしょう、と言っていたら本当に出てウケました。

トレーシンググッズを入れる

いつも通りGitHubにソースコードをpushして、まずは初手でトレーシンググッズを入れました。今年のGoの初期実装ではちゃんとcontextが引き回されていたので安心ですね。

interpolateParams=true

例のアレです。最後のほうに設定しがちだけどもう初手で入れてよいでしょう、ということで interpolateParams=true を有効にしました。

dsas.blog.klab.org

N+1クエリをなんとかする

配信検索のエンドポイントが見るからに重いことが分かり、内部では激しくN+1クエリを発行していたのでなんとかすることにしました。あちこちで filLivestreamResponse 関数を呼んでいたので、一括でLivestreamModelからLivestreamに変換できるように関数を切り出して作りました。

配信のタグ一覧が正しくついていない、というバグがありましたがそれを直したらPASSしました。空スライスと nil はJSONシリアライズすると別物になるんよ。

ユーザー・配信ごとの統計情報のクエリをなんとかする

全ユーザーのランキングを文字通り全部なめて構築している箇所があって、明らかに引きずられているのでなんとかしました。これがユーザーの順位を計算するクエリだ!!

WITH reaction_per_user AS (
    SELECT l.user_id, COUNT(*) AS reaction_count FROM reactions r
    LEFT JOIN livestreams l ON l.id = r.livestream_id GROUP BY l.user_id
), tip_per_user AS (
    SELECT l.user_id, IFNULL(SUM(lc.tip), 0) AS sum_tip FROM livecomments lc
    LEFT JOIN livestreams l ON l.id = lc.livestream_id GROUP BY l.user_id
), ranking_score AS (
    SELECT reaction_per_user.user_id, (IFNULL(reaction_count, 0) + IFNULL(sum_tip, 0)) AS score FROM reaction_per_user LEFT OUTER JOIN tip_per_user ON reaction_per_user.user_id = tip_per_user.user_id
), ranking_per_user AS (
    SELECT users.id AS user_id, IFNULL(ranking_score.score, 0), ROW_NUMBER() OVER w AS 'ranking' FROM users LEFT JOIN ranking_score ON users.id = ranking_score.user_id WINDOW w AS (ORDER BY ranking_score.score DESC, users.name DESC)
)
SELECT ranking FROM ranking_per_user WHERE user_id = ?

他の統計情報もN+1クエリにならないように書き換えてなんとかしました。上述したクエリはほぼそのまま配信ごとの統計情報に流用してあります。

このあたりで16:30ぐらいになっていたようで時が経つのが早すぎる!!

配信枠にインデックスを足す

ついにpt-query-digestでスロークエリを見はじめました。配信を作るところのクエリが重く、インデックスが効いていないようだったのでインデックスを足しました。

ALTER TABLE reservation_slots ADD KEY `start_at_end_at` (`start_at`, `end_at`);

N+1クエリをなんとかする (再)

配信のコメント一覧を取得するエンドポイントにもN+1クエリが残っていたのでそちらも滅ぼしました。ここで1万点ぐらいスコアが伸びました。よく叩かれるエンドポイントのN+1クエリを最適化しておくのは大事なんやね~

N+1クエリをなんとかする (再2・切り戻した)

自分の配信一覧のエンドポイントにもN+1クエリが残っていたのでそちらも滅ぼしましたが、競技終了10分前ぐらいでスコアが微減したので切り戻しました。

祈る

切り戻した実装でベンチを走らせたらFAILする!! 競技終了ギリギリに再度ベンチマークをenqueueして、ベンチマーク結果を確認することなく競技が終わりました。

反省

だいたいCloud Traceを見て判断した

Cloud Traceにトレーシング情報が上がってくるので、searchが遅いね、とか、配信コメント一覧がだんだん遅くなっているね、などを見て順番にボトルネックを潰していました。

配信コメント一覧のエンドポイントで最初にSQLを投げるまでの間に時間がかかっているけど何を待っているんだろう、というのがピンと来るのが遅かったのが反省点ですね。おそらくN+1クエリでDBのコネクションが掴めていなかったのではないか。

終了間際に駆け込みで実装修正を入れまくった

チームメンバー全員が書いていた実装修正を競技終了20分前ぐらい? に一気に入れていった結果、点数が30000点ぐらい上がって、どこまでいけるか? というのを試していました。たぶんここで入れた実装たちのどれかがバグっていたのでしょう。

どんどん点数が上がると調子が出てしまうけど、点数を残すという観点だと、終了間際は点検をするほうに振るのがよかったかもしれません。一発勝負じゃなくて予選だったらそうしていたかもしれないけど……。

もうちょっと手数があるとよかった

N+1クエリや統計情報のクエリなど、順当にボトルネックになっているところを順当に潰すことはできていたけど、全てのN+1クエリを潰すところまではいけなかったし、もう1手を打つことができなかったのが悔やまれます。統計情報を完全になんとかした時点で残り時間が1時間半ぐらいになっているけど、もうちょっと早くできるとよかったかも?

比較的アプリケーションの様子が分かっていた

例年はだいたい他のチームメンバーに立ててもらった作戦を順に実装していく、という感じであまりマニュアルを読まずに実装していることが多かったのですが、ベンチマークの待ち時間もあったのでけっこうマニュアルを熟読していました。

追試できたら試したいこと

何が原因でFAILしていたのか

まずこれを特定したいですね……。

N+1クエリを全部潰すと何点まで上がる (上がらない) のか

N+1クエリを潰す仕組みはおおむねできていたけど、全てのN+1クエリを潰せていたわけではないので、全部潰したらどこまでいけるかは見ておきたいです。

コメント投稿部分の最適化

今回の問題のスコア計算では投げ銭の金額が使われていたので、投げ銭の動線となるコメント機能を最適化できるとよかったのかも? ということをあとで考えましたが、そこまで手が回っていませんでした。

感想

やはり最終スコアを残せなかったのが悔しいですね。これが予選だったらスコア的には予選通過しているだろうけど最後のベンチマーク結果によるな~というところで競技終了になってしまったので、競技終了が近づいてきたら本当にコードフリーズを行う (重大な不具合だけ直す) ほうに振ってもよかったかもしれません。

ベンチマークの待ち時間が気になる瞬間が何度かあって、今やっている実装をどこまで進めてからベンチマークに回すべきか迷って手が止まってしまうことがありました。参加者側としては1秒でも早くベンチマークが走ってほしいと思うけど、とはいえベンチマーカーを作るのは本当に大変だと思う、毎年ありがとうございます。

いや~来年はぜったい上位入賞したいな~

追記

ポータルのベンチマークジョブ一覧を開放していただいたので記念にスクショを残しておきます。最高で73,936点、最後にpassした時点で手を止めていれば70,079点だったようです。

gyazo.com