生成器が作れなかった負荷はサーバーの限界ではない
一言でいうと
負荷ジェネレーターが作り出せなかった負荷は、サーバーの限界ではありません。テスト結果を読む前に、要求した負荷と実際にかかった負荷が同じだったかから確認する必要があります。
なぜ必要なのか
注文APIの容量レビューのレポートに、こう書かれていました。「1秒あたり500件を1分間かけて、p99は210ミリ秒、エラーは0件でした」。ところが同じ時間のサーバーのリクエスト数の指標は、1秒あたり48件で平らでした。500件は、かかったことがありませんでした。
原因は単純でした。ジェネレーターを同時実行数10で動かしていて、応答は200ミリ秒でした。クローズドループでこの組み合わせが出せる最大スループットは、10 ÷ 0.2 = 1秒あたり50件です。500件は、コマンドラインに書かれた希望にすぎず、物理的に出せない数字でした。ところがツールは、何の警告もせず、「p99 210ミリ秒」をきれいに出力しました。
こうした結果が危険なのは、サーバーについて誤った信頼を生むからです。実際の負荷の10分の1しか体験していないサーバーは、当然健康に見えます。その数字を根拠に容量を決めると、リリース当日に、ちょうど10倍の開きの分だけ崩れます。
どう動くのか
クローズドループのジェネレーターのスループット上限は、リトルの法則からそのまま出ます。待機中のリクエスト数L、到着率λ、滞留時間Wの間にL = λ × Wが成り立つので、同時実行数をCに固定すると、λはC ÷ 응답시간を超えられません(プレースホルダーは応答時間です)。
| 同時実行数 | 応答200ミリ秒のときの上限 |
|---|---|
| 1 | 1秒あたり5件 |
| 5 | 1秒あたり25件 |
| 10 | 1秒あたり50件 |
ここで重要なのは、この上限がサーバーと無関係だという点です。サーバーがどれだけ速くても、ジェネレーターが待っているあいだは、リクエストが出ていきません。そのため、目標のレートを満たしたいなら、同時実行数を、目標レート × 応答時間以上に取る必要があります。
2つ目の落とし穴は、レートオプションの意味です。heyの-qは、ドキュメントに「Rate limit, in queries per second (QPS) per worker」と書かれています。つまり、ワーカーごとにかかる上限です。-c 5 -q 2は、1秒あたり2件ではなく、1秒あたり10件です。逆に、総目標レートを書き込んだと信じていると、実際の負荷は、ワーカー数の分だけ膨らんでいます。ツールごとにこの定義が違うので、オプション1つを読み間違えると、テスト全体が別の実験になります。
3つ目は、ジェネレーター自身のリソースです。リクエスト1つにソケット1つが必要なので、同時実行数が開いているファイル数の上限を超えると、その分が黙って失敗します。プロセス数、CPU、そしてジェネレーターが動いているホストのネットワーク帯域も、同じやり方で上限になります。ジェネレーターが対象より小さいマシンで動いていれば、そのテストはジェネレーターを測るテストです。
そのため、テストを設計するときには順序があります。まず、かけたい目標レートを決め、そのレートに応答時間を掛けて、必要な同時実行数を求めます。1秒あたり500件で応答200ミリ秒なら、最低100個のワーカーが必要です。そのあと、その同時実行数がジェネレーター側の上限の中に収まるかを確認します。開いているファイル数、エフェメラルポートの範囲、メモリの順です。この2つの計算を先にしておけば、「なぜ目標がかからなかったのか」をあとで調査することが減ります。
現場での姿
最もよくあるサインは、「同時実行数を2倍に上げたら、総スループットもちょうど2倍になり、応答時間はそのまま」という結果です。サーバーが飽和したなら、スループットは平らになって応答時間が上がります。どちらでもなければ、サーバーはまだ遊んでいて、限界はジェネレーター側にあります。
そのため、判定の手順は、2つの数字だけを見ればよいのです。サーバー側の処理時間がそのままなのに、総レートだけが目標に届かなければ、ジェネレーターの限界です。逆に、処理時間が増えながらレートが平らになれば、それが本当の飽和点です。この区別をしないと、何の問題もないサーバーにインスタンスを追加する羽目になります。
エラーの分布も手がかりです。「connection refused」や「too many open files」は、サーバーではなく、ジェネレーターが出したエラーのことが多いのです。特にファイルディスクリプターの上限は、同時実行数を大きく上げた瞬間に突然現れますが、ツールはこれを単なる失敗リクエストとして数えて、エラー率に混ぜてしまいます。すると「サーバーが5%失敗した」という結論が出ます。
もう1つよくある姿は、ジェネレーターと対象が同じマシンで動くテストです。このラボのPodが、まさにその状況です。2つが同じCPUを分け合うと、負荷を上げるほど、ジェネレーターの取り分が減り、そのため、対象が遅くなっているのか、ジェネレーターが遅くなっているのか、区別できなくなります。実際のテストでは、ジェネレーターを別のマシンに置く必要があり、それができないなら、少なくともこの事実をレポートに書いておく必要があります。
最後に、これらすべてを防ぐ最も安い方法は、レポートの様式です。要求した負荷と実際にかかった負荷を、並べて書く欄を作っておけば、2つが違うときに誰でも気づきます。欄がなければ、誰も尋ねません。
次のラボですること
応答に200ミリ秒かかるPythonサーバーを起動し、同時実行数ごとのスループット上限を、リトルの法則で先に予測したあと、heyで実際に測って突き合わせます。-qがワーカーあたりの上限であることを実測で確認し、目標レートに大きく届かないテストをわざと作って、それがサーバーの飽和なのかジェネレーターの限界なのかを見分ける手順を立てます。開いているファイル数の上限を下げて、ジェネレーター自身が限界になる姿も自分で作ってみて、最後に、要求した負荷と実際にかかった負荷が一緒に書かれているかを確認する検査スクリプトを作って残します。