毎秒500件をかけたと書いたが実際は48件だった
目標
クローズドループのジェネレーターのスループット上限を、リトルの法則で予測して、実際に測って突き合わせたあと、heyのレートオプションがワーカーあたりの上限であることを実測し、目標レートに届かなかった結果が、サーバーの飽和なのかジェネレーターの限界なのかを見分ける手順と、レポートの検査スクリプトを作ります。
なぜ重要なのか
負荷テストのレポートで、最もよく間違う数字は、レイテンシではなく、負荷そのものです。同時実行数10で応答200ミリ秒のクローズドループは、どれだけ高い目標を書いても、1秒あたり50件を超えられません。ところがツールは、警告なしにきれいなパーセンタイルを出力し、実際の負荷の10分の1しか体験していないサーバーは、当然健康に見えます。レートオプションの定義がツールごとに違うので、1語を読み間違えると、実際の負荷がワーカー数の分だけ膨らんだり縮んだりし、開いているファイル数のようなジェネレーター自身のリソースが限界になると、その失敗がサーバーのエラー率に混ざり込みます。そのため、結果を読む前に、要求した負荷と実際にかかった負荷が同じだったかを確認する必要があり、その確認を、人の記憶ではなく、レポートの様式と検査スクリプトに任せる必要があります。
ステップ
/opt/lab/lt/lt-generator-limits/slow.pyを、ポート8090で応答0.2秒で起動してください。そのあと、curl -s -o /dev/null -w '%{time_total}\n'で/を5回呼び出して、その5行を/root/lt-generator-limits/01-probe.txtにそのまま残し、/root/lt-generator-limits/01-service.txtに、2行port=とservice_ms=を書いてください。service_msは、5行の中央値をミリ秒の整数で書きます。/root/lt-generator-limits/predict.tsvを作成してください。ヘッダーなしで4行、各行はタブで区切った2列<동시성> <초당 요청 수 상한>です(プレースホルダーは同時実行数と、1秒あたりのリクエスト数の上限です)。同時実行数は順に1・2・5・10で、上限は동시성 ÷ 서비스 시간(초)を小数第2位まで書きます(プレースホルダーは同時実行数とサービス時間です)。サービス時間は、ステップ1で測った値です。hey -t 60 -o csvで2回測ってください。同時実行数1は-n 30、同時実行数5は-n 125として、元のCSVを、それぞれ/root/lt-generator-limits/c1.csvと/root/lt-generator-limits/c5.csvにそのまま残します。そのあと/root/lt-generator-limits/03-measured.tsvに、2行をタブで区切った3列<동시성> <예측 RPS> <측정 RPS>で書いてください(プレースホルダーは、同時実行数、予測RPS、測定RPSです。小数第2位まで)。総RPSは、요청 수 ÷ (max(offset + response-time) − min(offset))で計算します(プレースホルダーはリクエスト数です)。heyのCSVの2つの列だけあれば、求められます。hey -n 100 -c 5 -q 2 -t 60 -o csvで測って、元のデータを/root/lt-generator-limits/q.csvに残してください。そのあと/root/lt-generator-limits/04-q.txtに、5行q_flag=・workers=・expected_total_rps=・measured_rps=・per_worker=を書きます。expected_total_rpsは、-qの値とワーカー数で予測した総レート、measured_rpsは、q.csvから再計算した値(小数第2位)、per_workerは、-qがワーカーあたりの上限ならyes、総上限ならnoです。hey -n 100 -c 2 -q 25 -t 60 -o csvで測って、元のデータを/root/lt-generator-limits/shortfall.csvに残してください。-q 25でワーカーが2個なので、要求した負荷は1秒あたり50件です。/root/lt-generator-limits/05-shortfall.txtに、3行requested_rps=・achieved_rps=・achieved_ratio=を書いてください。達成率は、achieved_rps ÷ requested_rpsを小数第3位まで書きます。/root/lt-generator-limits/verdict.txtに、6行を書いてください。baseline_service_ms=はc1.csvの応答時間の中央値、loaded_service_ms=はshortfall.csvの中央値(ミリ秒の整数)、service_ratio=は2つの比(小数第2位)、rps_ratio=はステップ5の達成率、verdict=は下のルールで選んだ値、reason=は60文字以上の根拠です。ルールは、rps_ratioが0.8以上ならok、そうでなくservice_ratioが1.3以上ならserver、どちらでもなければgeneratorです。- 開いているファイル数の上限を64に下げたシェルで、
hey -n 400 -c 200 -t 5を実行して、人が読む出力を/root/lt-generator-limits/fdlimit.txtにそのまま残してください(-o csvなしで)。そのあと/root/lt-generator-limits/07-fd.txtに、5行nofile=・concurrency=・ok_count=・error_count=・limit=を書きます。成功数とエラー数は、fdlimit.txtで数え、limit=には、この失敗がどちら側の限界だったかを、serverまたはgeneratorで書きます。 /root/lt-generator-limits/check-report.shを作成してください。レポートファイルのパスを第1引数として受け取り、requested_rps=・achieved_rps=・achieved_ratio=の3行がすべて数字とともにあれば0で、1つでもなければ0以外の値で終了する必要があります。そして、その検査を通るレポート/root/lt-generator-limits/report.mdを書いてください。3つの値は、ステップ5で測った値と同じである必要があります。
参考
- 作業ディレクトリは
/root/lt-generator-limitsです。なければ先に作成してください。 - 負荷対象は
/opt/lab/lt/lt-generator-limits/slow.pyが1つあるだけです。Podではコンテナを起動できないので、Python標準ライブラリのHTTPサーバーを127.0.0.1で直接起動して使います。 - heyのCSVは、1行目がヘッダーで、最初の列が
response-time、最後の列がoffsetです。総RPSは、요청 수 ÷ (max(offset + response-time) − min(offset))で求めます(プレースホルダーはリクエスト数です)。 - よくある間違い: レートオプションを総目標レートとして読んでしまうこと。ステップ4が、その確認です。
- よくある間違い: 目標に届かなかった結果を、そのまま「サーバーの限界」として書いてしまうこと。サーバー側の処理時間がそのままなら、限界はジェネレーター側です。
- hey: オプション-n・-c・-qの定義・k6: open vs closed model・k6: scenariosと到着率・vegeta: 固定レートの攻撃・wrk2: 目標レートと補正
サービス時間が一定の負荷対象を起動する
/opt/lab/lt/lt-generator-limits/slow.pyを、ポート8090で応答0.2秒で起動してください。そのあと、curl -s -o /dev/null -w '%{time_total}\n'で/を5回呼び出して、その5行を/root/lt-generator-limits/01-probe.txtにそのまま残し、/root/lt-generator-limits/01-service.txtに、2行port=とservice_ms=を書いてください。service_msは、5行の中央値をミリ秒の整数で書きます。
この対象はロックがないので、同時に入ってきた分だけ並行して処理します。このラボで、限界はサーバーではなく、負荷を作る側にあります。バックグラウンド実行はnohup ... &で、起動するまで1秒ほど待ってください。中央値は、sort -nのあとの真ん中の行です。
リトルの法則で上限を先に予測する
/root/lt-generator-limits/predict.tsvを作成してください。ヘッダーなしで4行、各行はタブで区切った2列<동시성> <초당 요청 수 상한>です(プレースホルダーは同時実行数と、1秒あたりのリクエスト数の上限です)。同時実行数は順に1・2・5・10で、上限は동시성 ÷ 서비스 시간(초)を小数第2位まで書きます(プレースホルダーは同時実行数とサービス時間です)。サービス時間は、ステップ1で測った値です。
クローズドループでは、ワーカー1つが応答を待っているあいだ、次のリクエストを送りません。そのため、ワーカーC個が作れる最大のレートは、C ÷ 応答時間です(リトルの法則)。この上限は、サーバーの性能と無関係です。サーバーがどれだけ速くても、待っているあいだはリクエストが出ていかないからです。
予測が合っているか、実際に測ってみる
hey -t 60 -o csvで2回測ってください。同時実行数1は-n 30、同時実行数5は-n 125として、元のCSVを、それぞれ/root/lt-generator-limits/c1.csvと/root/lt-generator-limits/c5.csvにそのまま残します。そのあと/root/lt-generator-limits/03-measured.tsvに、2行をタブで区切った3列<동시성> <예측 RPS> <측정 RPS>で書いてください(プレースホルダーは、同時実行数、予測RPS、測定RPSです。小数第2位まで)。総RPSは、요청 수 ÷ (max(offset + response-time) − min(offset))で計算します(プレースホルダーはリクエスト数です)。heyのCSVの2つの列だけあれば、求められます。
heyのCSVは、1行目がヘッダーで、最初の列がresponse-time、最後の列がoffset(テスト開始から、そのリクエストを送った時刻)です。測定値が予測値を超えないのが正常です。超えていたなら、サービス時間を誤って測ったか、対象が応答を早く切ったのです。
-qは総目標レートではなく、ワーカーあたりの上限である
hey -n 100 -c 5 -q 2 -t 60 -o csvで測って、元のデータを/root/lt-generator-limits/q.csvに残してください。そのあと/root/lt-generator-limits/04-q.txtに、5行q_flag=・workers=・expected_total_rps=・measured_rps=・per_worker=を書きます。expected_total_rpsは、-qの値とワーカー数で予測した総レート、measured_rpsは、q.csvから再計算した値(小数第2位)、per_workerは、-qがワーカーあたりの上限ならyes、総上限ならnoです。
-qが総上限なら、総レートは2の近くになるはずです。実際にいくつ出るかを見て、判断してください。heyのドキュメントに、このオプションがどう書かれているかも確認してみてください。1語の違いが、テスト全体を別の実験にします。同時実行数5の上限(25)より低くかけないと、制限が実際にかかりません。
目標をかけたのに、半分もかからなかったテストを作る
hey -n 100 -c 2 -q 25 -t 60 -o csvで測って、元のデータを/root/lt-generator-limits/shortfall.csvに残してください。-q 25でワーカーが2個なので、要求した負荷は1秒あたり50件です。/root/lt-generator-limits/05-shortfall.txtに、3行requested_rps=・achieved_rps=・achieved_ratio=を書いてください。達成率は、achieved_rps ÷ requested_rpsを小数第3位まで書きます。
同時実行数2で応答0.2秒なら、リトルの法則の上限は1秒あたり10件です。レートの上限をどれだけ高くかけても、それより上には上がれません。レートオプションは「これより速く送るな」という意味であって、「これだけ送れ」という意味ではありません。
サーバーの飽和か、ジェネレーターの限界か
/root/lt-generator-limits/verdict.txtに、6行を書いてください。baseline_service_ms=はc1.csvの応答時間の中央値、loaded_service_ms=はshortfall.csvの中央値(ミリ秒の整数)、service_ratio=は2つの比(小数第2位)、rps_ratio=はステップ5の達成率、verdict=は下のルールで選んだ値、reason=は60文字以上の根拠です。ルールは、rps_ratioが0.8以上ならok、そうでなくservice_ratioが1.3以上ならserver、どちらでもなければgeneratorです。
サーバーが飽和すると、処理時間が増えます。処理時間がそのままなのに、総レートだけが目標に届かなければ、サーバーはまだ遊んでいて、限界は負荷を作る側にあります。この区別をしないと、何の問題もないサーバーにインスタンスを追加することになります。
ジェネレーター自身が限界になる瞬間
開いているファイル数の上限を64に下げたシェルで、hey -n 400 -c 200 -t 5を実行して、人が読む出力を/root/lt-generator-limits/fdlimit.txtにそのまま残してください(-o csvなしで)。そのあと/root/lt-generator-limits/07-fd.txtに、5行nofile=・concurrency=・ok_count=・error_count=・limit=を書きます。成功数とエラー数は、fdlimit.txtで数え、limit=には、この失敗がどちら側の限界だったかを、serverまたはgeneratorで書きます。
ulimit -n 64は、そのシェルと子プロセスにだけ適用されます。bash -c 'ulimit -n 64; hey ...'のように、1行にまとめてください。heyの出力には、Status code distributionとError distributionの2つのセクションがあります。エラーの文言を読めば、この失敗を誰が出したかがわかります。
レポートの様式が、自分で尋ねるようにする
/root/lt-generator-limits/check-report.shを作成してください。レポートファイルのパスを第1引数として受け取り、requested_rps=・achieved_rps=・achieved_ratio=の3行がすべて数字とともにあれば0で、1つでもなければ0以外の値で終了する必要があります。そして、その検査を通るレポート/root/lt-generator-limits/report.mdを書いてください。3つの値は、ステップ5で測った値と同じである必要があります。
採点ツールは、このスクリプトに、わざと項目を抜いたレポートを渡して、失敗するかを見ます。無条件に0で終了するスクリプトは、合格できません。こうした小さな検査1つが、「1秒あたり500件をかけました」を、自分で証明させます。レポートには、テストの条件も一緒に書いておいてください。