使用率 8 割から先が急に遅い
読了目安 約4分
リソースの使用率が上がると待ち時間が跳ね上がる仕組みを、待ち行列のシミュレーションで確かめます。
- 仕事が到着 リクエスト
- 使用率が上昇 空きが減る
- 待ち行列 応答が遅れる
この章の目次
前章の数字は、どれも順番待ちがないときの値です。 実際のサーバーには同時に何件も届くので、処理が始まるまで待たされます。 利用者が見る応答時間は、この待ち時間と処理時間の合計です。
そして待ち時間は、負荷に比例しては増えません。
使用率と待ち時間
CPU、メモリ、ディスク、ネットワークのように、処理に使われて奪い合いになるものを計算リソース(以下リソース)と呼びます。
リソースが仕事をしている時間の割合が使用率で、ここでは と書きます。 CPU が 1 秒のうち 0.8 秒動いていれば です。
窓口が 1 つで、到着も処理もランダムに散らばる行列では、平均待ち時間が平均サービス時間の何倍かが次の式で決まります。
が 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 を別の値に変え、
の結果が実行ごとにどれだけ揺れるかを見てください。
飽和の近くでは、同じ条件でも測るたびに結果がばらつきます。
答えを見る
0.99 を足した実行例です。
使用率 平均待ち時間 ρ/(1-ρ)
0.50 0.99 1.00
0.80 3.94 4.00
0.90 9.04 9.00
0.95 19.59 19.00
0.99 84.81 99.00理論値は 99 ですが、シミュレーションは 84.81 で止まっています。 飽和に近いほど収束が遅く、20 万件でもまだ足りていません。
シードを変えた の平均待ち時間は、1、2、3、42 で 19.59、15.58、18.73、16.75 でした。 使用率 0.5 の行はほとんど動かないのに、0.95 では 2 割以上揺れます。 本番の計測でも、飽和に近い系ほど 1 回の測定を信用できなくなります。