負荷生成器がバックプレッシャーに協力する
一言でいうと
負荷テストの結果とプロダクションの指標が違うときは、たいてい負荷テストのほうを疑うべきです。実際のユーザーは、前のリクエストが遅いからといって、次のリクエストを待ってくれないからです。
なぜ必要なのか
負荷ジェネレーターを1秒あたり1,000件に設定したのに、応答1つが2秒を食ったとしましょう。同期式(closed)のジェネレーターは、その2秒のあいだに送るはずだった2,000件を送りません。システムが最も遅かった区間のサンプルがまるごと消え、負荷ジェネレーターは、自分が作り出したバックプレッシャーに「協調」してしまいます。Gil Teneがコーディネーテッドオミッション(coordinated omission)と呼んだ問題です。
症状は3つです。負荷を上げても、p99がほとんど動きません。報告されたスループットが、設定した目標値より低いのに、レイテンシ分布は目標値で測ったかのような形をしています。そして、プロダクションのほうが負荷テストよりも、裾がずっと悪いのです。
すべての実行に適用する受け入れ検査は、1行です。報告された実際のスループットが、設定した目標スループットと一致しているか。一致しなければ、その実行のレイテンシ分布は信頼できません。
どう動くのか
これが、closedモデルとopenモデルの違いです。closedは同時実行数を固定します(常にN個が処理中)。openは到着率を固定します(応答の有無に関係なく、1秒あたりλ件が到着)。実際のユーザーは、openに近いのです。
リトルの法則L = λ × Wは、2つのモデルをつなぎます。システムの中にある平均のリクエスト数は、到着率に平均滞留時間を掛けたものです。RPS 1,000、レイテンシ100msなら、同時に100個が処理中です。逆に、同時実行数を10に固定したclosedの実行で、RPS × 平均レイテンシが10の近くでなければ、負荷ジェネレーターが目標の負荷を実際には出せなかったという意味です。
コネクションプールのサイジングにも、そのまま使います。インスタンス1つが1秒あたり500リクエスト、平均80msなら、500 × 0.08 = 40で、裾のレイテンシを考慮して1.5倍の余裕を持たせれば、推奨されるプールサイズは60です。ここで止まってはいけません。インスタンス全体(フリート)の単位で、もう一度検算する必要があります。インスタンス40台 × プール60 = 最大2,400接続なのに、Postgresのmax_connectionsが200なら、デプロイ直後の接続の殺到で障害が起きます。
現場での姿
悪いベンチマークには共通点があります。ウォームアップがなくてJITが温まっておらず、CPUの周波数を固定しておらず、OSのノイズを考慮しておらず、1回しか測定しておらず、外れ値を処理していません。
良い手順は単純です。ウォームアップ10回、本測定100回以上、2σの外を除外、p50・p95・p99を併せて報告、環境の記録です。コールドスタートの実測値も知っておくと、ウォームアップの長さを決めるのに役立ちます。Python/Node 150–500ms、Go/Rust 50–100ms、JVM 1000–3000ms、GraalVM Native 50–100msです。
テストの種類も、目的によって分けます。負荷(予想されるトラフィックでの正常な動作)、ストレス(限界点を探すための漸増)、スパイク(急増への反応)、耐久性Soak(長時間でメモリリークを検知)です。
ラダーをどこまで上げるか
同時実行数を上げながら測るラダーで、人が最もよくする間違いは、早く止めすぎることと長引かせすぎることです。スループットがまだ上がっているのに止めると、限界を見つけられず、すでに折れ曲がったあとも上げ続けると、テスト自体がシステムを壊して、あとの測定がすべて使えなくなります。
止める地点を決める基準は、3つです。
- スループットがそれ以上伸びません。同時実行数を2倍に上げたのに、スループットが10%も増えなければ、すでに飽和です。その地点をニー(knee)と呼びます。
- エラーが出始めます。エラー率が上がれば、そのあとのレイテンシの数字は意味がありません。失敗したリクエストはたいてい早く終わるので、かえってレイテンシが良く見えるという錯覚まで生じます。
- 元に戻りません。負荷を下げたのにレイテンシが元どおりに戻らなければ、何かが壊れています。キューがたまったか、コネクションが漏れているか、メモリが回収されていない状態です。このときはテストを止めて原因を見るべきで、ラダーを上げ続けてはいけません。
3つ目が、実は最も価値のある観察です。正常なシステムは、負荷を下げればレイテンシもついて下りてきます。そうでなければ、そのシステムは、ピークに耐えられなかったときに自分で回復できないという意味で、それはスループットの数字よりはるかに重要な事実です。そのため、ラダーを上げたあとは、必ず下りながらも一度測る必要があります。上りのときと下りのときの同じ同時実行数で値が違えば、その差がそのまま回復力の欠陥です。
もう1つ。各ステップは、十分に長くなければなりません。30秒のステップでは、自動スケーリングも、キャッシュのウォームアップも、キューがたまることも観察されません。最低でも数分は維持して初めて、定常状態の値が出て、それ以前の値は過渡区間なので、別に印を付けておくほうがよいでしょう。特に自動スケーリングが付いているシステムは、新しいインスタンスが起動して準備ができるまでの時間がまるごと過渡区間で、その時間より短いステップでは、スケーリングが実際に役立つのかどうかさえわかりません。
次のラボですること
同時実行数1・2・5・10・20のラダーを計画し、スクリプトで自動実行します。結果を表にして、スループットがそれ以上伸びなくなるニーの地点をルールに従って見つけ、リトルの法則で実行自体が有効だったかを検証したあと、到着率を固定したopenの実行を別に回して、目標に対する実際のスループットを突き合わせます。