TT Lab
はじめる
学ぶ 学習パス コース

負荷テスト

平均は裾が悪くなるのを検知できない

TT Labで続きを見る

一言でいうと

同じログを見ても、平均だけを見るチームはロールバックを決め、中央値だけを見るチームは成功事例として発表し、p99だけを見るチームはインシデントを開きます。3チームとも、同じデータを見ました。

なぜ必要なのか

リクエスト10,000件のうち、9,900件が50ms、100件が3,000msのサービスを見てみましょう。平均は79.5msです。実際に79.5msで応答を受けたリクエストは、1件もありません。平均は、存在しないユーザーを描写しています。p50もp95もp99もすべて50msで、p99.9だけが3,000msです。

さらに悪いのは感度です。遅い100件が3,000 → 6,000msと2倍悪化しても、平均は79.5 → 109.5msまでしか動きません。平均は、裾が悪化することをほとんど検知できません。

実際のロールアウトの前後の数字を見ると、結論が分かれる理由がはっきりします。平均は112 → 122ms(+9%)、p50は99 → 54ms(−46%)、p95は224 → 454ms(+103%)、p99は309 → 678ms(+119%)。平均だけを見ると小幅な悪化、中央値だけを見ると大きな改善、p95/p99を見ると深刻な回帰です。

どう動くのか

p99を「1%の不運なユーザー」と理解すると、優先順位を間違えます。裾は増幅されます。リクエストをn回行うとき、少なくとも1回p99の区間に当たる確率は、1 − 0.99^nです。

呼び出し回数 p99に当たる確率
1回 1.0%
5回 4.9%
20回 18.2%
50回 39.5%
200回 86.6%

ダッシュボードの1画面が20個のAPIを呼び出すなら、画面を1回開くときにp99のレイテンシに当たる確率は18%です。1日に200回リクエストするアクティブユーザーなら、87%が1日に1回以上、最悪の区間を体験します。p99は、少数の不運なユーザーではなく、ほぼすべてのユーザーの日常です。そのため、1画面が20個を呼び出すなら、サービス1つの目標は99%ではなく99.9%でなければ、画面レベルで98%になりません。

パーセンタイルは、合算することもできません。サーバーA(9,900件すべて50ms)のp99は50、サーバーB(100件すべて3,000ms)のp99は3,000です。単純平均は1,525ms、リクエスト数で重みを付けた平均は79.5ms、本当の統合p99は3,000msです。3つの数字がすべて違い、前の2つには何の意味もありません。

現場での姿

負荷テストのレポートで最もよく見られる欠陥は、条件がないことです。RPSとp95だけが書かれていて、同時実行数も、総リクエスト数も、いつ測ったかもありません。そうした数字は、来月に比較できないので、ベースラインではありません。

2つ目は、1回しか測らないことです。同じ条件で3回回すと、変動幅が見え、その変動幅より小さい差は、改善と呼べません。

パーセンタイルを正しく保存する方法

p99を合算できないという問題は、保存方式を変えれば、ほとんどが解決します。鍵は、パーセンタイルの値ではなく、分布を保存することです。ヒストグラムは「0–10msに何件、10–25msに何件」のように、バケットごとの件数を持つので、複数のインスタンスのバケットをただ足せば、全体の分布になります。その合算した分布からパーセンタイルを求めれば、それが本当の値です。時間軸でも同じで、5分単位のヒストグラムを1日分足せば、その日のp99が出ます。

その代わり、ヒストグラムには固有の誤差があります。パーセンタイルがどのバケットの中にあるかまでしかわからず、その中での正確な位置は、補間で推定します。そのため、バケット境界を目標値の近くに細かく置くことが、精度を左右します。目標が300msなのに、境界が100msの次が1秒なら、その間のすべての値が1つの欄に固まって、p99を判定できません。逆に、誰も見ない10秒より上は、粗く置いても損がありません。

そして最大値は、ヒストグラムでは求められません。最後のバケットは「1秒以上」のように開いていて、その中に1.1秒のものがあるのか30秒のものがあるのか、区別できません。最も遅いリクエストがいくつだったかが重要な調査では、最大値を別にメトリクスとして出力するか、遅いリクエスト自体をログやトレースで探す必要があります。

何を測るかが、数字より先

パーセンタイルを正しく読んでも、そもそも間違った条件で測った数字なら、何の役にも立ちません。負荷テストが現実とずれる場所は、たいてい決まっています。

ウォームアップをしない。JITコンパイル、コネクションプールを満たすこと、キャッシュのウォームアップ、そして自動スケーリングが反応するまでの時間。開始直後の30秒は正常な状態ではないので、その区間を集計に入れると、p99が実際よりずっと悪く出ます。逆にウォームアップが長すぎても問題です。実際のユーザーはデプロイ直後にもリクエストを送るので、その区間の性能は別に測って把握しておく必要があります。

データがきれいすぎる。すべてのリクエストが、同じユーザー、同じ商品を照会すると、最初のリクエスト以降はすべてキャッシュから出ます。スループットの数字は美しいものの、現実には存在しない値です。逆に、毎回完全にランダムなキーを使うと、キャッシュのヒット率が0になり、悲観的すぎます。実際のアクセス分布は、たいてい少数の項目に集中しているので、その偏りを真似る必要があります。

1か所だけを叩く。エンドポイント1つに負荷を集中させると、その経路のボトルネックしか見えません。現実には、複数の機能が同じデータベースと同じスレッドプールを分け合って使うので、単独では問題なかったものが、一緒に動くと崩れます。シナリオは、実際のトラフィックの比率どおりに混ぜる必要があります。

負荷ツールが先に疲れる。クライアント側のCPUが飽和したり、ポートが枯渇したりすると、サーバーではなく、ツールが限界です。このとき出てくるレイテンシの増加は、サーバーの性質ではありません。負荷ツールが動いているマシンのリソースも一緒に記録して初めて、この思い違いを避けられます。

最後に、結果を残す形式も、決めておくほうがよいでしょう。負荷、継続時間、総リクエスト数、エラー率、p50/p95/p99、そしてそのときのサーバーのCPU・メモリ・コネクション数です。この組み合わせがなければ、来月に同じテストを回しても比較できず、比較できない数字は、ベースラインではなく、その日の記録にすぎません。

次のラボですること

nginxのコンテナを負荷の対象として起動し、heyで、スモーク・ウォームアップ・本測定に分けて実行します。ツールの出力から、RPSとp50/p95/p99とエラー率を自分で取り出してJSONにまとめ、平均に対するp95の倍率を計算したあと、3回繰り返して、変動幅まで含めたベースラインを確定します。