サーバーが2秒止まったのに p99 は60ミリ秒だった
目標
同じサーバーを、クローズドループとオープンモデルで1回ずつ測って、p99がどれだけ開くかを自分で作ってみて、クローズドループのサンプルにHdrHistogram式の補正を手で実装したあと、補正の前後でSLAの判定がひっくり返ることを確認し、テスト設計のルールをファイルに残します。
なぜ重要なのか
クローズドループの負荷ジェネレーターは、前のリクエストの応答を受け取って初めて、次のリクエストを送ります。そのため、サーバーが止まっているあいだはリクエストをそもそも送らず、その区間はレイテンシ分布にほとんど残りません。実際のユーザーは、サーバーの事情を見ながらリクエストを遅らせたりしないので、この測定は常に、実際より良く見える方向にだけ間違います。ランダムに揺れる誤差ではなく、一方向にだけ傾く誤差なので、誰も疑わず、リリースの審査を通ってしまいます。直す方法は2つです。最初から発射時刻を固定するオープンモデルで測るか、すでに測ったサンプルを期待間隔で埋めるか。どちらかは必ず行う必要があり、レポートには、どちらだったかを残す必要があります。
ステップ
/opt/lab/lt/lt-coordinated-omission/target.pyをポート8080で起動してください(通常の応答0.05秒、停止2.0秒、150番目のリクエストで停止)。そのあと、curl -s -o /dev/null -w '%{time_total}\n'で/を6回呼び出して、その6行を/root/lt-coordinated-omission/01-probe.txtにそのまま残し、/root/lt-coordinated-omission/01-target.txtに、4行port=・base_ms=・stall_ms=・stall_at=を書いてください。base_msは6行の中央値をミリ秒の整数で、stall_msとstall_atは、サーバーのソースから読んだ値です。- リクエスト番号を戻したあと(
/reset)、hey -n 200 -c 1 -q 12.5 -t 60 -o csvで測って、元のCSVを/root/lt-coordinated-omission/closed.csvにそのまま残してください。そのCSVのresponse-time列だけを使って、/root/lt-coordinated-omission/02-closed.txtに、4行samples=・p50_ms=・p99_ms=・max_ms=を書きます。ミリ秒の整数に丸めて書いてください。パーセンタイルは、昇順に並べたあとの인덱스 = int(백분위 ÷ 100 × 표본수)の位置の値で計算します(プレースホルダーは、インデックス、パーセンタイル、サンプル数です。0から数えて、サンプル数−1を超えたら最後)。heyが使う方式と同じです。 /root/lt-coordinated-omission/schedule.txtに、200行を作成してください。1行に1つずつ、リクエストiを送ると意図した時刻を、秒単位で小数第3位まで書きます。1行目は0.000で、間隔は0.080秒で一定です。このファイルは、次のステップで、補正後レイテンシの基準になります。/root/lt-coordinated-omission/openloop.pyを自分で書いてください。schedule.txtの時刻ごとにリクエストを1つずつ送りますが、前のリクエストは待ちません。結果は、/root/lt-coordinated-omission/open.csvに、ヘッダーi,intended,sent,received,codeと200行で残し(時刻は測定開始からの秒)、/root/lt-coordinated-omission/04-open.txtに、補正後レイテンシ(received − intended)のsamples=・p50_ms=・p99_ms=・max_ms=の4行を書いてください。/root/lt-coordinated-omission/hdrfix.pyを書いて、closed.csvのレイテンシのサンプルを、期待間隔0.080秒で埋めてください。サンプル1つの値が期待間隔より大きければ、その値から期待間隔を引き続けながら、0より大きいあいだ、サンプルをさらに作って入れます(HdrHistogramのrecordValueWithExpectedIntervalがしていること)。埋めた結果を、1行に1つずつ/root/lt-coordinated-omission/closed-corrected.txtに秒単位で残し、/root/lt-coordinated-omission/05-hdr.txtに、5行expected_interval_ms=・samples=・p50_ms=・p99_ms=・max_ms=を書いてください。/root/lt-coordinated-omission/compare.tsvを作成してください。ヘッダーなしで3行、各行はタブで区切った4列<이름> <p50_ms> <p99_ms> <max_ms>です(プレースホルダーは名前です)。名前は順に、closed(ステップ2)・hdr(ステップ5)・open(ステップ4)で、数字は前のステップで書いたミリ秒の整数を、そのまま写します。/root/lt-coordinated-omission/sla.txtに、6行を書いてください。sla_p99_ms=には100と1000の間の整数のしきい値を、closed_p99_ms=とcorrected_p99_ms=には、それぞれクローズドループ(ステップ2)とオープンモデルの補正(ステップ4)のp99を、closed_verdict=とcorrected_verdict=には、そのp99がしきい値以下ならpass、大きければfailを、flipped=には、2つの判定が違えばyes、同じならnoを書きます。/root/lt-coordinated-omission/08-summary.txtに、2行worst_case_ms=(オープンモデルの補正後レイテンシの最大値)とomitted_requests=(クローズドループが止まっているあいだに送れなかったリクエスト数 = 最大のレイテンシのサンプルを、期待間隔0.080秒で割った商)を書いてください。そして/root/lt-coordinated-omission/rules.mdに、- <열쇠>: <설명>の形のルールを4行書きます(プレースホルダーはキーと説明です)。キーは順にrate・correct・report・gateで、説明はそれぞれ40文字以上である必要があります。
参考
- 作業ディレクトリは
/root/lt-coordinated-omissionです。なければ先に作成してください。 - 負荷対象とトラフィックデータは、
/opt/lab/lt/lt-coordinated-omission/にあります。target.pyが1つあるだけです。Podではコンテナを起動できないので、負荷対象は、Python標準ライブラリのHTTPサーバーを127.0.0.1で直接起動して使います。 - 測定を始める前に、
curl http://127.0.0.1:8080/resetで、リクエスト番号を戻してください。戻さないと、止まるリクエストの位置が変わります。 - よくある間違い: 最大値が大きいからp99も大きいだろうと、推測してしまうこと。サンプル200個で悪いサンプルが1つなら、p99には捕まりません。2つの数字を、常に一緒に見てください。
- よくある間違い: オープンモデルのスクリプトを、前の応答を待つように作ってしまうこと。
open.csvのsentがintendedから離れたら、それはオープンモデルではなく、遅いクローズドループです。 - heyの使い方とオプション・wrk2: coordinated omissionの補正・HdrHistogram・k6: open vs closed model・vegeta: 固定レートの攻撃
定期的に止まる負荷対象を起動する
/opt/lab/lt/lt-coordinated-omission/target.pyをポート8080で起動してください(通常の応答0.05秒、停止2.0秒、150番目のリクエストで停止)。そのあと、curl -s -o /dev/null -w '%{time_total}\n'で/を6回呼び出して、その6行を/root/lt-coordinated-omission/01-probe.txtにそのまま残し、/root/lt-coordinated-omission/01-target.txtに、4行port=・base_ms=・stall_ms=・stall_at=を書いてください。base_msは6行の中央値をミリ秒の整数で、stall_msとstall_atは、サーバーのソースから読んだ値です。
バックグラウンドで起動するには、nohup python3 ... > server.log 2>&1 &です。起動するまで1–2秒待ってください。8080がすでに開いていれば、2回起動できません。curl http://127.0.0.1:8080/resetがresetを返せば、すでに起動しています。中央値は、sort -nのあとで真ん中の行を見ればよいのです。
クローズドループで測って、元のパーセンタイルを書く
リクエスト番号を戻したあと(/reset)、hey -n 200 -c 1 -q 12.5 -t 60 -o csvで測って、元のCSVを/root/lt-coordinated-omission/closed.csvにそのまま残してください。そのCSVのresponse-time列だけを使って、/root/lt-coordinated-omission/02-closed.txtに、4行samples=・p50_ms=・p99_ms=・max_ms=を書きます。ミリ秒の整数に丸めて書いてください。パーセンタイルは、昇順に並べたあとの인덱스 = int(백분위 ÷ 100 × 표본수)の位置の値で計算します(プレースホルダーは、インデックス、パーセンタイル、サンプル数です。0から数えて、サンプル数−1を超えたら最後)。heyが使う方式と同じです。
-c 1はワーカー1つ、-q 12.5は、ワーカーごとに1秒あたり12.5件が上限という意味です(間隔80ミリ秒)。-o csvを付けると、サマリーの代わりに、リクエスト1つにつき1行が出て、1行目はヘッダーです。p99と最大値が何倍違うかを見てください。その開きが、このラボのテーマです。
意図した発射時刻をあらかじめ固定する
/root/lt-coordinated-omission/schedule.txtに、200行を作成してください。1行に1つずつ、リクエストiを送ると意図した時刻を、秒単位で小数第3位まで書きます。1行目は0.000で、間隔は0.080秒で一定です。このファイルは、次のステップで、補正後レイテンシの基準になります。
クローズドループには、「意図した時刻」というものがそもそもありません。前の応答が来たら、そのときに送るからです。オープンモデルは、その時刻を先に決めておき、サーバーの事情と無関係に守ります。seqかawkの1行で作れます。最後の行は(200 − 1) × 0.08です。
オープンモデルで測り直して、補正後レイテンシを求める
/root/lt-coordinated-omission/openloop.pyを自分で書いてください。schedule.txtの時刻ごとにリクエストを1つずつ送りますが、前のリクエストは待ちません。結果は、/root/lt-coordinated-omission/open.csvに、ヘッダーi,intended,sent,received,codeと200行で残し(時刻は測定開始からの秒)、/root/lt-coordinated-omission/04-open.txtに、補正後レイテンシ(received − intended)のsamples=・p50_ms=・p99_ms=・max_ms=の4行を書いてください。
リクエストごとにスレッドを1つ作り、そのスレッドが自分の時刻までtime.sleepしてから送ればよいのです。urllib.request.urlopenで十分です。標準ライブラリだけを使います(pipインストール不可)。sentとintendedがほぼ同じであって初めて、オープンモデルです。離れたら、ジェネレーターが追いつけなかったということです。
HdrHistogramの補正を手で実装する
/root/lt-coordinated-omission/hdrfix.pyを書いて、closed.csvのレイテンシのサンプルを、期待間隔0.080秒で埋めてください。サンプル1つの値が期待間隔より大きければ、その値から期待間隔を引き続けながら、0より大きいあいだ、サンプルをさらに作って入れます(HdrHistogramのrecordValueWithExpectedIntervalがしていること)。埋めた結果を、1行に1つずつ/root/lt-coordinated-omission/closed-corrected.txtに秒単位で残し、/root/lt-coordinated-omission/05-hdr.txtに、5行expected_interval_ms=・samples=・p50_ms=・p99_ms=・max_ms=を書いてください。
レイテンシ2.0秒のサンプル1つは、期待間隔0.08秒で1.92・1.84・…と、サンプルを複数作り出します。元のサンプルはそのままにして、作り出したものを足します。期待間隔は、ステップ2で-qで決めた間隔(1 ÷ 12.5 = 0.08秒)です。パーセンタイルの計算方式は、前のステップと同じです。
3つの分布を1つの表に並べる
/root/lt-coordinated-omission/compare.tsvを作成してください。ヘッダーなしで3行、各行はタブで区切った4列<이름> <p50_ms> <p99_ms> <max_ms>です(プレースホルダーは名前です)。名前は順に、closed(ステップ2)・hdr(ステップ5)・open(ステップ4)で、数字は前のステップで書いたミリ秒の整数を、そのまま写します。
3行のp50はほとんど同じなのに、p99だけが分かれることが核心です。p50が同じということは、サーバーが普段はうまく動いていたという意味で、p99が分かれるということは、悪い区間のサンプルが、ある側にはあって、ある側にはないという意味です。採点ツールは、3つの元ファイルから値を再計算して突き合わせます。
同じテストで、ひっくり返るSLAの判定
/root/lt-coordinated-omission/sla.txtに、6行を書いてください。sla_p99_ms=には100と1000の間の整数のしきい値を、closed_p99_ms=とcorrected_p99_ms=には、それぞれクローズドループ(ステップ2)とオープンモデルの補正(ステップ4)のp99を、closed_verdict=とcorrected_verdict=には、そのp99がしきい値以下ならpass、大きければfailを、flipped=には、2つの判定が違えばyes、同じならnoを書きます。
しきい値をどこにかけても問題ありませんが、2つの判定が分かれる場所を選ぶと、このステップの要点が現れます。判定がひっくり返ったなら、直すべきものはしきい値ではなく、「どの数字で約束したか」です。レポートに補正の有無を書かないと、半年後に誰も区別できません。
再びだまされないための、テスト設計のルール
/root/lt-coordinated-omission/08-summary.txtに、2行worst_case_ms=(オープンモデルの補正後レイテンシの最大値)とomitted_requests=(クローズドループが止まっているあいだに送れなかったリクエスト数 = 最大のレイテンシのサンプルを、期待間隔0.080秒で割った商)を書いてください。そして/root/lt-coordinated-omission/rules.mdに、- <열쇠>: <설명>の形のルールを4行書きます(プレースホルダーはキーと説明です)。キーは順にrate・correct・report・gateで、説明はそれぞれ40文字以上である必要があります。
4つのルールが答えるべき質問は、こうです。負荷をどのモデルでかけるか(rate)、すでに測ったサンプルをどう補正するか(correct)、レポートに何を一緒に書くか(report)、どの数字で合格を判定するか(gate)。各行に自分の言葉で書きますが、前のステップで見た数字を根拠にしてください。