こんにちは,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の設定をするのはミスりがちなので,あると便利.
-
- 事前講習でもalpが紹介され,流行っているらしいですが,弊チームは使い慣れたkataribeを使っています.
-
- 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から配信
最後に
今年も本選に行けることになり,とても嬉しいです. このような面白いコンテストをずっと行っておられる,運営の皆さんに感謝いたします. 本選もがんばります!!!