All Articles

ISUCON11予選を1位通過した(shallowverse)

こんにちは,y1rです. ark, akkyと,チームshallowverseでISUCON11予選に参加し,予選で1位を獲得しました. このチームでISUCONに参加するのは3回目で,前回に引き続き本選に進出できることになり,嬉しく思っています. 本記事では,shallowverseが行ったISUCON11予選「IsuCondition」の高速化について紹介します. 一つ一つの高速化について細かく説明することはできませんでしたし,高速化のネタバレしかないので,一度問題を解いてから読むことをおすすめします.

得点のログ.最後1時間で爆発したので,延長戦がなければ厳しかったです. 得点のログ

公開リポジトリはこちら

準備

前回のISUCON(shallowverseのブログ)から1年が経っており,チーム皆,高速化の勘どころを忘れていたので,入念に練習をしました. 今回は,ISUCON 9予選と,ISUCON 11の事前講習で使用された問題を使いました. ISUCON 9予選は非常に解きごたえがある問題で,弊チームでは,ISUCON 9予選はスコア「79240」まで高速化を行えました. ISUCON 11 予選も非常に面白い問題でしたが,ISUCON 9予選もぜひ一度挑戦されることをおすすめします!

ツールの準備

今回使用したツールを列挙しておきます.

  • Ansible

    • 開発環境を整備するために使いました.
  • お手製deploy script

    • git pull して systemctl restart ... をコマンド一発でやります.
    • nginxのアクセスログの削除も併せて行っています.
  • お手製mysql slowqueryくん

    • コマンド一発でmysql slowqueryの設定をします.
    • 本番焦っているときにslowlogの設定をするのはミスりがちなので,あると便利.
  • kataribe

    • 事前講習でもalpが紹介され,流行っているらしいですが,弊チームは使い慣れたkataribeを使っています.
  • notify_slack

    • mysql slowlogやkataribeのアクセスログ解析結果をチームのSlackに流します
  • Slack

    • はい
  • Discord

    • オンラインでやったので,常時画面共有・音声通話するために使いました.
    • 高画質かつ安定しており,無料で使えるので最高.

前日

有給を取得して練習しまくった.

当日

おはようございます.かなり眠いですが,チームで朝会をしました.今年は中継があって楽しかったですね!

10:00 ~ 11:00

全員でマニュアルを音読しつつ,Ansibleでサーバ側の基本的な設定をしました. マニュアルのグラフに関する説明が複雑でよく分からず,結局その部分は読み飛ばしました. 初期スコアは3200点でした.

12:30 頃まで (6000点まで)

おもむろにkataribeを使います.

Top 20 Sort By Total
Count    Total    Mean  Stddev    Min  P50.0  P90.0  P95.0  P99.0    Max   2xx  3xx  4xx  5xx  TotalBytes   MinBytes  MeanBytes   MaxBytes  Request
  338  218.214  0.6456  0.4298  0.000  0.933  1.001  1.001  1.002  3.000   149    0  189    0      380448          0       1125       4663  GET /api/isu HTTP/2.0
   51   43.015  0.8434  0.3348  0.020  0.997  1.001  1.001  1.003  1.003     9    0   42    0       41955          0        822       4720  GET /api/trend HTTP/2.0
 1412   12.518  0.0089  0.0250  0.000  0.001  0.027  0.100  0.101  0.133  1335    0   77    0           0          0          0          0  POST /api/condition/94eae521-c6eb-4a5a-9317-ea284f8b771d HTTP/2.0
 1394   12.062  0.0087  0.0251  0.000  0.001  0.018  0.100  0.101  0.109  1320    0   74    0           0          0          0          0  POST /api/condition/1683b63a-2375-494c-983b-ed6ee09fc39c HTTP/2.0
 1411   11.989  0.0085  0.0241  0.000  0.001  0.024  0.100  0.101  0.102  1338    0   73    0           0          0          0          0  POST /api/condition/75f163fb-7498-4899-bdb3-7b3fa952da46 HTTP/2.0
 1403   11.935  0.0085  0.0251  0.000  0.001  0.014  0.100  0.101  0.111  1318    0   85    0           0          0          0          0  POST /api/condition/af037ffc-b453-4d54-ab76-8697dca52149 HTTP/2.0
 1401   11.935  0.0085  0.0247  0.000  0.001  0.017  0.100  0.101  0.106  1322    0   79    0           0          0          0          0  POST /api/condition/f57eecff-421f-4c89-b625-0c27f04ba844 HTTP/2.0
 1405   11.910  0.0085  0.0241  0.000  0.001  0.022  0.092  0.101  0.108  1340    0   65    0           0          0          0          0  POST /api/condition/6bb3f815-ce06-4a73-a34d-93062c413c92 HTTP/2.0
 1400   11.825  0.0084  0.0248  0.000  0.001  0.020  0.099  0.101  0.102  1327    0   73    0          14          0          0         14  POST /api/condition/26613fd9-666c-4b0a-962c-8e05608e55c5 HTTP/2.0
 1402   11.702  0.0083  0.0244  0.000  0.001  0.021  0.099  0.101  0.104  1331    0   71    0           0          0          0          0  POST /api/condition/43b31a0d-9578-485a-a9fe-5efd58f26a54 HTTP/2.0
 ...

idが違うリクエストが多数あり,POST /api/condition/{id}/ の合計の方が大きいですが, 弊チームは読み間違えて以下の高速化に取り組みました.

  • インデックス貼る

    • 18942 になった
  • GET /api/isu

    • getIsuListのN+1クエリを解消
  • GET /api/trend

    • getTrendのN+1クエリを解消

GETのクエリを早くしたところ,スコアが6000まで下がりましたが,レスポンス自体は速くなっていたのでrevertはしないことにしました.

13:30 頃まで (10000点ぐらい)

  • getIsuConditionsFromDB

    • よく呼ばれている「IsuCondition」を取得する関数.
    • 全件取得し,アプリケーションでフィルタしていたため,LIMITをSQLでやるようにして負荷を下げることを狙う.
    • 実はWHERE句をSQLに移行するのを忘れておりバグらせていたが,この時点では気づかず.
  • postIsuConditionのBulk INSERT化  - 得点源となるエンドポイント.

    • 複数件のINSERTをまとめることで,DBにデータを挿入する際のレイテンシを削減する.
  • getUserIDFromSessionの高速化

    • SQLを発行せずにUserの存在を確認する.
    • COUNTの代わりにSELECT 1 ~ LIMIT 1にしてみた.
  • 複数台構成の導入

    • よく呼ばれるgetTrendを分離してみた.
  • 静的ファイル (asset) の諸々

    • Goの代わりにnginxでレスポンスする.
    • gzip_static を使って,CPU負荷を上げずにネットワーク負荷を下げる.

色々一気にmergeしすぎて,複数台構成に切り替えたあたりでFAILするようになった.しんどい.

14:30 頃まで (10000点ぐらい)

手動 git bisect の結果,getIsuConditionsFromDBの高速化に問題があることに気づきrevertする. かなり肝が冷えた.直してもスコアが上がることはなく,15000ぐらいまで戻る.

15:30 頃まで (FAILしまくり)

getTrendをrevertするべきかとか,まだ気づいていないSQL slow queryがあるのではないかとか,pprofみたりとか. 「IsuCondition」の数を制御するdropProbalibityをいじったが,スコアが下がったりFAILしたりするので,とりあえず放置する. postIsuConditionが多数実行されないと,点が増えづらいことがマニュアルから分かった.

17:00 頃まで (35000点からFAIL)

  • postIsuConditionを遅延書き込みにする

    • 1秒ためてBulk INSERTする
    • さらにpostIsuConditionがさばけるようになることを期待!
  • 点の付け方を読み直す(2回目)

    • 「postIsuConditionが多数実行される -> 精密なグラフが書ける -> 点の倍率が大きくなる」ことが分かった
    • データポイント内のIsuCondition数の最小値によって,点の倍率が変わるので大事そう.
  • postIsuConditionの間引く場所を変えてみた

    • リクエストを確率的に落とすのではなく,リクエスト中のレコードを確率的に落とす.
    • idごとのデータポイント内のIsuCondition数が下がりにくくなるんでは!?
  • getIsuConditionsFromDBの正しいWHERE実装をやる

    • ひどいSQLを書いた.
    • 序盤に正規化できるよね,という話をしていたので,もしそうするなら早く着手してればできたかも.
  • 画像をキャッシュする

    • 静的ファイルにしたいとy1rが言い出したが,残り時間やバグらせる可能性を鑑みて断念.
    • キャッシュの方が実装が簡単なので,とりあえずキャッシュでごまかしておく.

スコアは上がらなかったが,DBの負荷は下がっていた. apiserverの負荷がきつそうなので,複数台構成のやり方を見直し, getTrendを分離していたのを,全てのクエリで分散するようにしたところ,ベンチがFAILするようになった. http2: client connection force closed via ClientConn.Close で悩まされる.

18:10 頃まで (ベンチが停止中)

  • 秘伝のタレを流し込む  - sysctl, nginx, MySQLあたり.

    • http2のエラーが解消されることを狙ったが,解決しなかった.
  • パラメータチューニングしやすいように定数まとめたり

    • 終盤のベンチ & チューニングの時間短縮につながった.
  • 未マージのブランチが色々あったので整備する

    • ベンチ明けに全部テストできるようにしておく
    • GitHubのInsightタブでマージしてないブランチを探しておく
    • 忘れてたブランチがあるとかなしいので.

18:25 頃まで (FAILから70000点)

エラー http2: client connection force closed via ClientConn.Close を減らせない. 「http2 max connection」で検索したところ,パラメータhttp2_max_requestsを見つけた. 20000まで増やすとFAILしなくなるので,予選通過できるのではと期待が高まる. 70000点がでた.

予選終了まで (70000点から1500000点まで)

client request body is buffered to a temporary file が出るので調整. 未マージのブランチなどもテストしながらマージしていくと,どんどんスコアが上がる. サービスの設定は変えてないので,問題はないはずだが再起動試験を行っておく. 最後に0.9だった dropProbalibity を0.4に調整すると150万点が出たので終了.ありがとうございました!

試したくてできなかったこと

当時思いついていて,(優先度・実装難度をふまえて)できなかったことを挙げておきます. 意外とやりたかったことはできたので,練習の成果は出せたのかなと思います.

  • Conditional GET
  • GetIsuGraphのトランザクション外す
  • IsuConditionの正規化
  • DBから画像を引き剥がしてnginxから配信

最後に

今年も本選に行けることになり,とても嬉しいです. このような面白いコンテストをずっと行っておられる,運営の皆さんに感謝いたします. 本選もがんばります!!!