こんばんは、さんぽしです。 タイトルは打ち上げに送られてきたビールをたくさん開けた後の僕が勢いでつけたようです。プレモルがいっぱい送られてきて 💕 になりました。
2/6 に NTT Performance Tuning Challenge に参加してきました。ISUCON のようなパフォーマンスチューニングの腕を競うイベントです。
今回も ISUCON10 に一緒に出た@masibw くんと@tosa_now くんと一緒に参加しました。
ISUCON と同様に以下の役割分担で頑張りました
- さんぽし : インフラ周り、デプロイ&ベンチ&各メトリクス取り係
- まし : DB 周り →app
- とさ : app
僕はインフラをちょびっっっといじった後は二人の変更をせこせこデプロイしてベンチ回してメトリクス取るだけをやって働いている感を出していました。
メトリクスまわりは具体的には
- alp(アクセスログ解析してくれる君)
- pt-query-digest(スロークエリ解析してくれる君)
- pprof(Go のどの関数で時間がかかってるか調べてくれる君)
あたりを使用しました。定番ですね
やったこと時系列
(僕が多分全然把握していないので、他のチームメンバーがやったことの詳細は他のチームメンバーがかいてくれたブログを見てください…) https://mesimasi.com/ntt-perfomance-tuning-challenge/ https://harukitosa.com/posts/ntt-performance-tuning
apache→nginx
とりあえず apache は慣れてないのでサクッと nginx に乗り換えました
index をはる
pt-query-digest のスロークエリログからましくんが index を貼ってくれて一気に点が伸びました。さすが どこに貼ったのかとかの詳細は知りません(ぇ
jwt 周りでなんか遅かったのをなんか直してくれた
とさくんがなんかを直してくれました。点数爆伸びしました。さすが
複数台構成(app1, DB1)
この時点で DB が CPU を食っていたのでキャッシュや index は他のメンバーに任せるとして DB を別のサーバーに移す作業に取り掛かりました。 そして謎に詰まったので後回しにしました。(後に init.sh だけ書き換えただけではサーバー内の DB の向きが切り替わらないのか!という初歩的なミスに気がつきます)
DB から base64 の画像を引っこ抜いて nginx から配信
DB に画像が base64 形式で突っ込まれていました。これを引っこ抜くスクリプトをとさくんが書いてくれたので僕は nginx から配信するように変更しました。 画像更新系もとさくんが書いてくれました。さすが
/login だけ別サーバーへプロキシ(→ app2, DB1)
/login が重いらしいという話を聞いて 1 台余っていたのでそっちに振り分けました。
色んなとこにキャッシュを取りまくる
この時点で残り時間が 1 時間ちょっとでした。 今回負荷のレベルを指定ができ、ずっとレベル 2 でやっていて 1 万ちょっとの点数が出ていました。
この時点でも DB がかなり負荷が高くなっていました。 login 以外の app は 1 台で行こうという話でまとまっていたのでオンメモリでキャッシュ戦略を取りました。ちょっとずつ点数が伸びました。僕は何もしてません。さすが
ダメもとで負荷のレベルを 3 にする
一気に 27000 点が出ました。これが最高スコアになりました。
イベントの画像の更新系が不安定に
終了 20 分前あたり イベントの画像取得まわりが不安定で稀に fail していることに気が付きました。 これは画像の更新がベンチの終了時に戻されず、次に同じ event_id に Get のリクエストが来た際に返される image が異なることによるものでした。 終了直前で/initialize で全てが元の状態に戻される変更を突っ込みましたが fail したので諦めて、再起動試験で同じエラーが起きないように祈り日々積み上げている徳を信じてメッカの方に礼拝をしながら revert しました。(レギュレーションには抵触しないので)
再起動試験対策
再起動試験対策で DB のサーバーが立ち上がるまで app からの接続を待機する変更をとさくんが入れてくれました。
今回は明記されていませんでしたが、ISUCON では再起動でのサーバーの順番は指定/保証されておらず、DB のサーバーよりも先に app のサーバーが立ち上がると DB への接続に失敗し、その時点でサーバー自体が落ちてしまいます。 全サーバーの起動をしばらく待ってくれるみたいな話もちらっと目にしましたが、大事なのはベンチを回す際に DB が起動しているかどうかではなく、app サーバーを立ち上げた時点で DB が接続可能な状態になっているかどうかなので対策を入れる必要があります。
ということで以下のようにsqlx.Open
で接続されるまで待つという変更を入れています。
for {
dbx, err = sqlx.Open("mysql", fmt.Sprintf(
"%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=true&loc=Local",
user,
password,
host,
port,
dbname,
))
if err == nil {
break
}
time.Sleep(1 * time.Second)
}
結果
再起動試験により 0 点を叩き出し終了しました
一応最高スコアでは全体で 2 位でした。(↓ チーム I です)
スコアを安定させているチームや 4 万点出しているチームがいるので完敗ですね…
原因
後に DB の接続で再起動試験に失敗したらしいという話を聞いて対策したぞ??となっていましたが、色々調べていると
> https://t.co/apEdSnPXnk() はコネクションプールを初期化するだけでDBに接続しないので接続エラーも起こらないのだが、このAPIの結果 err == nil だと接続できるように錯覚しがち
— さんぽし (@sanpo_shiho) February 6, 2021
いや、https://t.co/SYgZAx5ZLE()しなきゃ行けなかったっぽい、知見だhttps://t.co/hU404IxYfx
僕たちの入れた変更はまさに Open で接続を待っていたつもりでした、知らなかった〜
感想
前回の ISUCON では僕も途中から app の実装に入ったりとかなり役割が曖昧になっていましたが、今回は僕は サボっていたので インフラ周りやデプロイ周りの管理に集中していたので、アプリのコードは最後まで読むことはありませんでした。
かなり明確に事前に決めた役割通りの動きができて、計測 → 改善 → デプロイ → ベンチ → 計測…のサイクルを回すのがかなりスムーズだったので良かったです。
このおかげでお互いの作業がブロックすることもなく割とパフォーマンスチューニングらしいパフォーマンスチューニングができたのではないかと思ってます。
最後は悔しい結果になりましたが、割と常に 1~3 位あたりにいたので個人的には満足な結果でした。僕は何もやってないのでチームメンバーのおかげです!ありがとう〜〜〜〜〜
次回最終章「ISUCON11」絶対見てくれよな!
再起動試験で0点になってもプレモルは美味い
— さんぽし (@sanpo_shiho) February 6, 2021