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

負荷テスト

平均は同じなのに片方だけ待ち行列が溢れた

TT Labで続きを見る

目標

同じ平均リクエスト率で、一定間隔・指数分布・バーストの3つの到着を自分で作って同じサーバーに送り、待ち行列とレイテンシがどう分かれるかを測ります。続いて、クローズドループのシンクタイムが実効リクエスト率を決める法則を、実測と突き合わせ、均等な到着を仮定した容量の見積もりが何倍外れるかを数字で見せたあと、このサービスの到着モデルとワーカー数を、根拠とともに決めます。

なぜ重要なのか

容量の見積もりは、ほとんどいつも平均から始まります。1秒あたり何件、1件に何ミリ秒、だからワーカー何個。その計算には、「リクエストが均等に来る」という仮定が隠れていますが、実際のトラフィックは、cronと通知とリトライのせいで、バーストで来ます。待ち行列は、平均ではなく瞬間に反応するので、平均の利用率が25%でも、バーストが来る瞬間にはワーカーが足りなくなり、その待ちがまたタイムアウトとリトライを生みます。反対側には、クローズドループの落とし穴があります。仮想ユーザー数でテストすると、サーバーが遅くなるときに負荷も一緒に減り、レポートの「1秒あたり何件まで持ちこたえた」が、サーバーの限界ではなく、テスト設定の限界になります。この2つを一度手で測ってみれば、次からは、負荷を書くときに、平均の隣に到着の形を一緒に書くようになります。

ステップ

  1. /opt/lab/lt/lt-arrival-process/server.pyを、python3 /opt/lab/lt/lt-arrival-process/server.py 8080 40 4で起動してください(ポート8080・サービス40ミリ秒・ワーカー4)。暇なときの応答時間を、curl -s -o /dev/null -w '%{time_total}\n'で6回測って、出てきた秒単位の数字を、/root/lt-arrival-process/01-probe.txtに1行に1つずつそのまま残してください。そして/root/lt-arrival-process/01-target.txtに、5行を書きます。service_ms=(6回の中央値をミリ秒に丸めたもの)・workers=(ワーカー数)・max_rate=(ワーカー数をサービス時間で割った値、1秒あたりの件数)・chosen_rate=25・utilization=(chosen_rateをmax_rateで割った値、小数第2位まで)です。
  2. /root/lt-arrival-process/gen.pyを作成してください。python3 gen.py <even|poisson|burst> <요청수> <초당요청> <시드> <출력CSV> [URL]で呼び出します(プレースホルダーは、リクエスト数、1秒あたりのリクエスト数、シード、出力CSVです)。URLを渡さなければ、送らずに計画だけをCSVに書きます。ヘッダーi,intendedに続いて、リクエストごとに番号と予定時刻(開始を基準とした秒)を書きます。evenは間隔が常に1/リクエスト率、poissonは間隔が平均1/リクエスト率の指数分布、burstは12件を同じ時刻にまとめて送り、12/リクエスト率だけ休みます。シードを受け取って、乱数を固定してください。確認用にpython3 gen.py even 300 25 7 /root/lt-arrival-process/sched-even.csvを1回回しておいてください。
  3. ジェネレーターをドライランで3回回して、/root/lt-arrival-process/sched-even.csv・/root/lt-arrival-process/sched-poisson.csv・/root/lt-arrival-process/sched-burst.csvを作成してください(すべて300 25 7)。そして/root/lt-arrival-process/schedules.tsvに、3行を書きます。各行はタブで区切った4列<모양> <요청 수> <평균 간격> <변동계수>で(プレースホルダーは、形、リクエスト数、平均間隔、変動係数です)、形はeven・poisson・burstの順、平均間隔は小数第4位まで秒単位、変動係数は、間隔の標準偏差を平均で割った値で、小数第3位まで書きます。
  4. 3つの到着方式で、同じサーバーに、同じ数のリクエストを実際に送ってください。方式ごとにcurl -s http://127.0.0.1:8080/resetで記録を消し、python3 gen.py <모양> 150 25 7 /root/lt-arrival-process/run-<모양>.csv http://127.0.0.1:8080/workで送ったあと、curl -s http://127.0.0.1:8080/dump > /root/lt-arrival-process/srv-<모양>.csvで、サーバーの記録を受け取ります(形はeven・poisson・burst。プレースホルダーは形です)。そのあと/root/lt-arrival-process/latency.tsvに、3行を書いてください。タブで区切った4列<모양> <성공 건수> <p50> <p95>で(プレースホルダーは、形、成功件数です)、レイテンシは、run-*.csvのreceivedからsentを引いた値をミリ秒に変えて、最近傍順位で求め、小数第1位まで、codeが200の行だけを数えます。
  5. サーバーが残した/root/lt-arrival-process/srv-*.csvには、リクエストごとにqlen(そのリクエストが届いた瞬間に、システムの中にあったリクエスト数)が入っています。/root/lt-arrival-process/queue.tsvに、3行を書いてください。タブで区切った4列<모양> <기록 수> <최대 대기열> <평균 대기열>で(プレースホルダーは、形、記録数、最大の待ち行列、平均の待ち行列です)、形はeven・poisson・burstの順、平均は小数第2位までです。そして/root/lt-arrival-process/05-note.txtに、2行worst=<최대 대기열이 가장 큰 모양>とratio=<그 최대값을 even 의 최대값으로 나눈 값, 소수 첫째 자리까지>を書いてください(プレースホルダーは、最大の待ち行列が最も大きい形と、その最大値をevenの最大値で割った値で小数第1位までです)。
  6. /root/lt-arrival-process/closed.pyを作成してください。python3 closed.py <URL> <사용자수> <생각시간초> <지속초> <출력CSV>で呼び出すと(プレースホルダーは、URL、ユーザー数、シンクタイムの秒数、継続秒数、出力CSVです)、ユーザーごとにリクエスト1つを送ってシンクタイムだけ休むことを、継続時間のあいだ繰り返します。CSVは、ヘッダーuser,sent,received,codeに続いて、開始を基準とした秒を書きます。python3 closed.py http://127.0.0.1:8080/work 8 0.2 10 /root/lt-arrival-process/closed.csvで1回回して、/root/lt-arrival-process/think.txtに、6行を書いてください。users=8・think_s=0.200・mean_r_s=(平均応答時間)・predicted_rate=(ユーザー数を、mean_r_sにthink_sを足した値で割った値)・measured_rate=(成功件数を、最初のsentから最後のreceivedまでの時間で割った値)・naive_rate=(応答時間を0と見て、ユーザー数をthink_sで割った値)です。リクエスト率は小数第3位まで、時間は小数第4位までです。
  7. /root/lt-arrival-process/capacity.txtに、5行を書いてください。formula_workers=(均等な到着を仮定した計算: chosen_rate x service_sを切り上げた整数)・observed_even=・observed_burst=(それぞれsrv-even.csvとsrv-burst.csvの同時にシステムの中にあったリクエスト数の最大値)・underestimate_factor=(observed_burstをformula_workersで割った値、小数第1位まで)・verdict=<under|ok>(formula_workersがobserved_burstより小さければunder)です。同時リクエスト数は、arriveに1を足し、endから1を引きながら、時刻順に走査して、最大値を取ります。
  8. /root/lt-arrival-process/arrival-model.txtに、4行を書いてください。model=<even|poisson|burst>は、今後このサービスの容量を決めるときに使う到着モデル、workers=は、そのモデルで決めたワーカー数(整数)、evidence=は、なぜそのモデルなのかを、前のステップで作ったファイル名と数字を挙げて60文字以上で、risk=は、そのモデルが間違っていたとき、何が先に崩れるかを40文字以上で書きます。採点ツールは、workersがステップ7で観測した最大の同時リクエスト数以上か、そして均等な到着を仮定したformula_workersより大きいかを見ます。

参考

ワーカーがいくつで、1件にどれくらいかかるか

/opt/lab/lt/lt-arrival-process/server.pyを、python3 /opt/lab/lt/lt-arrival-process/server.py 8080 40 4で起動してください(ポート8080・サービス40ミリ秒・ワーカー4)。暇なときの応答時間を、curl -s -o /dev/null -w '%{time_total}\n'で6回測って、出てきた秒単位の数字を、/root/lt-arrival-process/01-probe.txtに1行に1つずつそのまま残してください。そして/root/lt-arrival-process/01-target.txtに、5行を書きます。service_ms=(6回の中央値をミリ秒に丸めたもの)・workers=(ワーカー数)・max_rate=(ワーカー数をサービス時間で割った値、1秒あたりの件数)・chosen_rate=25・utilization=(chosen_rateをmax_rateで割った値、小数第2位まで)です。

/workが仕事をする経路で、/resetと/dumpは記録を扱う経路です。暇なときとは、負荷をかけていない状態のことです。1回に1つずつ送ってください。ワーカー4個がそれぞれ40ミリ秒ずつ使うなら、1秒あたり何件を処理できるかを計算してみると、これからかける1秒あたり25件が、どれだけ余裕のある負荷かが見えます。

到着の形を選べるジェネレーター

/root/lt-arrival-process/gen.pyを作成してください。python3 gen.py <even|poisson|burst> <요청수> <초당요청> <시드> <출력CSV> [URL]で呼び出します(プレースホルダーは、リクエスト数、1秒あたりのリクエスト数、シード、出力CSVです)。URLを渡さなければ、送らずに計画だけをCSVに書きます。ヘッダーi,intendedに続いて、リクエストごとに番号と予定時刻(開始を基準とした秒)を書きます。evenは間隔が常に1/リクエスト率、poissonは間隔が平均1/リクエスト率の指数分布、burstは12件を同じ時刻にまとめて送り、12/リクエスト率だけ休みます。シードを受け取って、乱数を固定してください。確認用にpython3 gen.py even 300 25 7 /root/lt-arrival-process/sched-even.csvを1回回しておいてください。

指数分布の間隔は、random.Random(seed).expovariate(rate)で作ります。burstの予定時刻は、(i // 12) * (12 / rate)の1行でよいのです。3つの方式とも、最後のリクエストの予定時刻がほぼ同じである必要があります。同じ平均リクエスト率という意味です。採点ツールは、自分で決めた引数でこのツールを直接実行してみます。

同じ平均、違う形

ジェネレーターをドライランで3回回して、/root/lt-arrival-process/sched-even.csv・/root/lt-arrival-process/sched-poisson.csv・/root/lt-arrival-process/sched-burst.csvを作成してください(すべて300 25 7)。そして/root/lt-arrival-process/schedules.tsvに、3行を書きます。各行はタブで区切った4列<모양> <요청 수> <평균 간격> <변동계수>で(プレースホルダーは、形、リクエスト数、平均間隔、変動係数です)、形はeven・poisson・burstの順、平均間隔は小数第4位まで秒単位、変動係数は、間隔の標準偏差を平均で割った値で、小数第3位まで書きます。

間隔は、隣り合う予定時刻の差です(299個)。標準偏差は、母集団を基準に計算してください。3つの平均間隔がほぼ同じなのに、変動係数だけが、0・1の近く・1よりはるかに大きい、と分かれることが、このステップが見せたいことです。burstの間隔には、0がたくさん入っています。

3つの方式で、同じサーバーを叩く

3つの到着方式で、同じサーバーに、同じ数のリクエストを実際に送ってください。方式ごとにcurl -s http://127.0.0.1:8080/resetで記録を消し、python3 gen.py <모양> 150 25 7 /root/lt-arrival-process/run-<모양>.csv http://127.0.0.1:8080/workで送ったあと、curl -s http://127.0.0.1:8080/dump > /root/lt-arrival-process/srv-<모양>.csvで、サーバーの記録を受け取ります(形はeven・poisson・burst。プレースホルダーは形です)。そのあと/root/lt-arrival-process/latency.tsvに、3行を書いてください。タブで区切った4列<모양> <성공 건수> <p50> <p95>で(プレースホルダーは、形、成功件数です)、レイテンシは、run-*.csvのreceivedからsentを引いた値をミリ秒に変えて、最近傍順位で求め、小数第1位まで、codeが200の行だけを数えます。

3つの方式の平均リクエスト率が同じなので、総時間も同じくらいで終わります。ところが、レイテンシの分布は同じではありません。特にp50とp95の開きを見てください。一定間隔では2つの値がほぼくっついていて、バーストでは遠く離れます。3回送るのに20秒ほどかかります。

サーバーが数えた待ち行列

サーバーが残した/root/lt-arrival-process/srv-*.csvには、リクエストごとにqlen(そのリクエストが届いた瞬間に、システムの中にあったリクエスト数)が入っています。/root/lt-arrival-process/queue.tsvに、3行を書いてください。タブで区切った4列<모양> <기록 수> <최대 대기열> <평균 대기열>で(プレースホルダーは、形、記録数、最大の待ち行列、平均の待ち行列です)、形はeven・poisson・burstの順、平均は小数第2位までです。そして/root/lt-arrival-process/05-note.txtに、2行worst=<최대 대기열이 가장 큰 모양>とratio=<그 최대값을 even 의 최대값으로 나눈 값, 소수 첫째 자리까지>を書いてください(プレースホルダーは、最大の待ち行列が最も大きい形と、その最大値をevenの最大値で割った値で小数第1位までです)。

3つの記録の行数は、同じである必要があります。同じ数のリクエストを送ったからです。ところが、最大の待ち行列は同じではありません。一定間隔ではワーカー数を超えず、バーストでは、一度に押し寄せた分が、そのままたまります。平均リクエスト率が同じでも、瞬間の同時リクエスト数は、到着の形が決めるという意味です。

シンクタイムがリクエスト率を決める

/root/lt-arrival-process/closed.pyを作成してください。python3 closed.py <URL> <사용자수> <생각시간초> <지속초> <출력CSV>で呼び出すと(プレースホルダーは、URL、ユーザー数、シンクタイムの秒数、継続秒数、出力CSVです)、ユーザーごとにリクエスト1つを送ってシンクタイムだけ休むことを、継続時間のあいだ繰り返します。CSVは、ヘッダーuser,sent,received,codeに続いて、開始を基準とした秒を書きます。python3 closed.py http://127.0.0.1:8080/work 8 0.2 10 /root/lt-arrival-process/closed.csvで1回回して、/root/lt-arrival-process/think.txtに、6行を書いてください。users=8・think_s=0.200・mean_r_s=(平均応答時間)・predicted_rate=(ユーザー数を、mean_r_sにthink_sを足した値で割った値)・measured_rate=(成功件数を、最初のsentから最後のreceivedまでの時間で割った値)・naive_rate=(応答時間を0と見て、ユーザー数をthink_sで割った値)です。リクエスト率は小数第3位まで、時間は小数第4位までです。

リトルの法則を、ユーザー1人のラウンドトリップに適用すると、実効リクエスト率はN / (R + Z)です。naive_rateは、Rを抜かしたときの予測で、実測と比べてみれば、なぜ抜かしてはいけないのかが見えます。このステップは、十数秒かかります。

均等な到着を仮定した容量は、何倍外れるか

/root/lt-arrival-process/capacity.txtに、5行を書いてください。formula_workers=(均等な到着を仮定した計算: chosen_rate x service_sを切り上げた整数)・observed_even=・observed_burst=(それぞれsrv-even.csvとsrv-burst.csvの同時にシステムの中にあったリクエスト数の最大値)・underestimate_factor=(observed_burstをformula_workersで割った値、小数第1位まで)・verdict=<under|ok>(formula_workersがobserved_burstより小さければunder)です。同時リクエスト数は、arriveに1を足し、endから1を引きながら、時刻順に走査して、最大値を取ります。

qlen列をそのまま最大値として使っても、同じ数が出るか確認してみてください。サーバーが到着の瞬間に数えた値なので、たいてい同じです。ただし、自分で走査してみると、「平均の利用率が低くても、瞬間の同時実行数ははるかに大きいことがある」ということが、なぜなのか、目で見えます。割り算の分母が1になることがあるので、小数で計算してください。

このサービスの到着過程を、何として扱うか

/root/lt-arrival-process/arrival-model.txtに、4行を書いてください。model=<even|poisson|burst>は、今後このサービスの容量を決めるときに使う到着モデル、workers=は、そのモデルで決めたワーカー数(整数)、evidence=は、なぜそのモデルなのかを、前のステップで作ったファイル名と数字を挙げて60文字以上で、risk=は、そのモデルが間違っていたとき、何が先に崩れるかを40文字以上で書きます。採点ツールは、workersがステップ7で観測した最大の同時リクエスト数以上か、そして均等な到着を仮定したformula_workersより大きいかを見ます。

ステップ3の変動係数と、ステップ5の最大の待ち行列が、このサービスの到着が均等ではないという証拠です。その証拠を受け入れれば、ワーカー数は、平均ではなくバーストの大きさが決めます。余裕をどれだけ置くかはあなたが決めますが、観測した最大の同時リクエスト数より下がると、そのバーストが来るたびに待ち行列ができます。