最悪の瞬間が結果から消える
一言でいうと
クローズドループの負荷ジェネレーターは、サーバーが止まっているあいだ、リクエストをそもそも送りません。送っていないリクエストはレイテンシ分布に残らず、そのため最も悪い瞬間が、結果からまるごと消えます。これをコーディネーテッドオミッション(coordinated omission)と呼びます。
なぜ必要なのか
決済APIの前で、こんなレポートを受け取ったことがあります。「1分間に12,000件を送り、p99は60ミリ秒でした。目標の500ミリ秒を大きく下回っています」。ところが、同じ時間のサーバーログには、2秒の停止が1回記録されていました。2つの文は、同じデータから出たものです。
不思議なことではありません。負荷ジェネレーターが同時実行数1で動いていたなら、サーバーが2秒を握っているあいだ、そのジェネレーターは応答を待つのに忙しく、何も送っていませんでした。2秒が終わって戻ってきた応答1つだけが、2秒として記録されます。サンプル200個のうち1個です。200個のうち1個は99.5パーセンタイルなので、p99の位置には、その隣の無事な50ミリ秒が座ります。
本物のユーザーは、そう行動しません。ユーザーは、サーバーが止まったからといって、リクエストを遅らせません。2秒のあいだも入り続け、そのリクエストはすべて列の後ろにたまって、2秒に近い時間を待ちます。つまり、実際に悪い体験をしたリクエスト数十個が測定から抜けたまま、レポートが作られたのです。
この欠陥が恐ろしいのは、失敗する方向がいつも同じだからです。コーディネーテッドオミッションは、結果をランダムに揺らすのではなく、常に良く見える方向にだけ間違わせます。そのため誰も疑わず、リリースの審査を通り、障害が起きてから初めて、「テストでは問題なかったのに」という言葉が出ます。
どう動くのか
負荷をかける方式は、大きく2つあります。
| モデル | 次のリクエストを送る時点 | サーバーが遅くなると |
|---|---|---|
| クローズドループ(closed) | 前のリクエストの応答を受け取ったあと | 送る量が自然に減ります |
| オープンモデル(open) | あらかじめ決めた時刻になったとき | 送る量はそのままで、列が長くなります |
クローズドループは、同時ユーザー数を真似るときには合っているモデルです。問題は、その性質が測定にもそのまま染み込むことです。サーバーが遅くなると負荷も一緒に減るので、ジェネレーターとサーバーが示し合わせて(coordinated)、悪い区間を一緒に飛び越えます。名前がここから来ています。
直す方法は2通りです。1つ目は、最初からオープンモデルで測ることです。リクエストiの意図した発射時刻をあらかじめ固定しておき、レイテンシを응답 시각 − 의도한 발사 시각で測ります(プレースホルダーは、応答時刻と、意図した発射時刻です)。この値を補正後レイテンシ(corrected latency)と呼びます。発射が遅れたなら、遅れた分がそのままレイテンシに加算されます。
2つ目は、すでに測ってあるクローズドループのサンプルを、事後に埋めることです。HdrHistogramのrecordValueWithExpectedIntervalがしていることが、これです。サンプル1つの値が期待間隔より大きければ、その間に送るはずだったのに送れなかったリクエストを、期待間隔ずつ引きながら作って入れます。レイテンシ2.0秒のサンプル1つは、期待間隔80ミリ秒で1.92秒、1.84秒、…というように、さらに25個のサンプルを作り出します。wrk2がwrkから分かれて生まれた理由も、この補正を入れるためでした。
2つの方法の結果は、同じ方向を指します。そのため、どちらかは必ず行う必要があります。ただし、事後補正には、期待間隔を知らないと使えないという限界があります。目標のレートを決めてかけたテストなら、期待間隔はそのレートの逆数ですが、レートなしで同時実行数だけを決めてかけたテストには、そもそも期待間隔というものがありません。そうしたテストの結果は、事後に直す方法がなく、測り直すしかありません。
現場での姿
最もよくある姿は、「最大値だけが大きくて、p99は小さい」という結果です。この組み合わせを見たら、まずコーディネーテッドオミッションを疑う必要があります。最大値がp99の30倍だということは、悪い区間のサンプルが1つか2つしか取れなかったという意味で、それはたいてい、その区間にリクエストを送れなかったからです。
2つ目は、「同時実行数を上げたらp99がかえって良くなった」という結果です。同時実行数が高いと、止まった区間にも他のワーカーのリクエストがいくつか入って列を作るので、サンプルが少し生まれますが、それでも実際の到着率よりはるかに少ないのです。数字が良くなったのではなく、間違い方が少し減っただけです。
3つ目は、ツールを変えたら数字が悪くなったという報告です。クローズドループのツールからオープンモデルのツールに移ると、p99が何倍にも跳ねます。そのとき「新しいツールが不正確だ」と結論して元に戻すことが、実際にありました。元に戻した側が間違っていました。新しい数字が、初めて正しい数字だったのです。
最後に、補正をすると、SLAの判定がひっくり返ることがよくあります。補正前は合格、補正後は未達です。このとき必要なのは、しきい値をいじることではなく、どの数字で約束したのかを、あらためて合意することです。テストのレポートに補正の有無を書いておかなければ、半年後に誰も、その数字がどちらだったかわかりません。
次のラボですること
150番目のリクエストでだけ2秒を握るPythonサーバーを起動し、同じサーバーを2回測ります。1回はheyでクローズドループ、もう1回は80ミリ秒間隔の200件があらかじめ書かれた固定スケジュールで、オープンモデルです。2つの元ファイルから、p50/p99/最大値を自分で再計算して表に並べ、クローズドループのサンプルにHdrHistogram式の補正を手で実装して、3つ目の分布を作ります。最後に、SLAのしきい値を1つかけて、補正の前後で判定がひっくり返ることを確認し、二度とこの罠にはまらないように、テスト設計のルールをファイルに残します。