使用率 8 割から先が急に遅い
読了目安 約4分
リソースの使用率が上がると待ち時間が跳ね上がる仕組みを、待ち行列のシミュレーションで確かめます。
この章の目次
前章の数字は、どれも順番待ちがないときの値です。 実際のサーバーには同時に何件も届くので、処理が始まるまで待たされます。 利用者が見る応答時間は、この待ち時間と処理時間の合計です。
そして待ち時間は、負荷に比例しては増えません。
使用率と待ち時間
CPU、メモリ、ディスク、ネットワークのように、処理に使われて奪い合いになるものを計算リソースと呼びます。 以降は単にリソースと書きます。
リソースが仕事をしている時間の割合が使用率で、ここでは と書きます。 CPU が 1 秒のうち 0.8 秒動いていれば です。
到着間隔も処理時間もばらつく場合、待ち時間はおよそ次の値に比例します。
が 1 に近づくと分母が 0 に近づくので、値がいくらでも大きくなります。
| 使用率 | |
|---|---|
| 0.5 | 1 |
| 0.8 | 4 |
| 0.9 | 9 |
| 0.95 | 19 |
使用率を 0.5 から 0.95 に上げると、リソースが働く量は 1.9 倍にしかならないのに、待ち時間は 19 倍になります。 この表が頭にあるかどうかで、使用率 9 割の読み方が変わります。「余裕があと 1 割」ではなく「待ち時間 9 倍」です。
実際に行列を作ってみる
窓口が 1 つの行列を再現します。 到着間隔と処理時間をばらつかせ、客が待たされた時間を平均します。
package main
import (
"fmt"
"math/rand"
)
func simulate(rho float64) float64 {
const n = 200000
const service = 1.0 // 1 件の処理にかかる平均時間
arrival := service / rho
r := rand.New(rand.NewSource(1))
var clock, free, totalWait float64
for i := 0; i < n; i++ {
clock += r.ExpFloat64() * arrival // 次の客が来るまでの間隔
if free < clock {
free = clock // 窓口は空いて待っていた
}
totalWait += free - clock // この客が待たされた時間
free += r.ExpFloat64() * service
}
return totalWait / n
}
func main() {
fmt.Println("使用率 平均待ち時間 ρ/(1-ρ)")
for _, rho := range []float64{0.5, 0.8, 0.9, 0.95} {
fmt.Printf(" %.2f %12.2f %12.2f\n", rho, simulate(rho), rho/(1-rho))
}
}シミュレーションの平均待ち時間が、右端の理論値とほぼ一致します。 待ちが消えないのは、空いていた時間を貯めておけないからです。 客が来ない間の余力はその場で捨てられ、客が重なった時間の遅れだけが後ろへ持ち越されます。
使用率だけでは判断できない
0.9 から 0.95 は、使用率としては 5 ポイントの差でも、待ち時間としては 2 倍の差です。 同じ「使用率が高い」から、残りの余裕は読み取れません。 しかも先に悪化するのは平均ではなく、いちばん遅い数パーセントの応答時間です。
見るべきなのは、リソースがどれだけ忙しいかではなく、行列がどれだけ長いかです。
- ロードアベレージ:実行したいのに CPU を待っているプロセスの数を含めた指標
- run queue:いま実行を待っている数そのもの
- データベースの接続待ち、ロック待ち、コネクションプール待ち
ロードアベレージがコア数を超えていること自体は、悪ではありません。 瞬間的な山を行列で吸収しているだけのこともあります。 手を打つべきなのは、行列が長く、かつ応答時間やタイムアウトの規定を破っているときだけです。
いま何件がサーバーの中にいるか
行列の長さは、すでに測っている 2 つの数字から出せます。
毎秒 500 件を返していて、1 件がサーバーの中に平均 200 ミリ秒とどまるとします。 1 件が 0.2 秒ぶん居座るので、掛け算して 100 件が常に中にいることになります。
- :いま中にいる件数の平均
- :毎秒処理できている件数
- :1 件あたりの平均滞在時間
入ってくる数と出ていく数が釣り合っているあいだ、この関係は必ず成り立ちます。 これをリトルの法則と呼びます。
100 件が常時どこかで処理中か待機中なので、接続数やワーカー数がそれに足りているかを確かめられます。 なぜ遅いのかを説明する式ではなく、測った数字どうしの辻褄を合わせる式です。
試してみよう
シミュレーションの に 0.99 を足して実行してください。 待ち時間が 99 に近づきます。
次に rand.NewSource(1) の 1 を別の値に変え、
の結果が実行ごとにどれだけ揺れるかを見てください。
飽和の近くでは、同じ条件でも測るたびに結果がばらつきます。