🔩 ねじき教室 Go と Web の教室

使用率 8 割から先が急に遅い

読了目安 約4分

リソースの使用率が上がると待ち時間が跳ね上がる仕組みを、待ち行列のシミュレーションで確かめます。

この章の目次

前章の数字は、どれも順番待ちがないときの値です。 実際のサーバーには同時に何件も届くので、処理が始まるまで待たされます。 利用者が見る応答時間は、この待ち時間と処理時間の合計です。

そして待ち時間は、負荷に比例しては増えません。

使用率と待ち時間

CPU、メモリ、ディスク、ネットワークのように、処理に使われて奪い合いになるものを計算リソースと呼びます。 以降は単にリソースと書きます。

リソースが仕事をしている時間の割合が使用率で、ここでは ρ\rho と書きます。 CPU が 1 秒のうち 0.8 秒動いていれば ρ=0.8\rho = 0.8 です。

到着間隔も処理時間もばらつく場合、待ち時間はおよそ次の値に比例します。

待ち時間    ρ1ρ \text{待ち時間} \;\propto\; \frac{\rho}{1-\rho}

ρ\rho が 1 に近づくと分母が 0 に近づくので、値がいくらでも大きくなります。

使用率 ρ\rhoρ1ρ\dfrac{\rho}{1-\rho}
0.51
0.84
0.99
0.9519

使用率を 0.5 から 0.95 に上げると、リソースが働く量は 1.9 倍にしかならないのに、待ち時間は 19 倍になります。 この表が頭にあるかどうかで、使用率 9 割の読み方が変わります。「余裕があと 1 割」ではなく「待ち時間 9 倍」です。

実際に行列を作ってみる

窓口が 1 つの行列を再現します。 到着間隔と処理時間をばらつかせ、客が待たされた時間を平均します。

Go
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 件が常に中にいることになります。

L=X×W L = X \times W
  • LL :いま中にいる件数の平均
  • XX :毎秒処理できている件数
  • WW :1 件あたりの平均滞在時間

入ってくる数と出ていく数が釣り合っているあいだ、この関係は必ず成り立ちます。 これをリトルの法則と呼びます。

100 件が常時どこかで処理中か待機中なので、接続数やワーカー数がそれに足りているかを確かめられます。 なぜ遅いのかを説明する式ではなく、測った数字どうしの辻褄を合わせる式です。

試してみよう

シミュレーションの ρ\rho に 0.99 を足して実行してください。 待ち時間が 99 に近づきます。

次に rand.NewSource(1) の 1 を別の値に変え、 ρ=0.95\rho = 0.95 の結果が実行ごとにどれだけ揺れるかを見てください。 飽和の近くでは、同じ条件でも測るたびに結果がばらつきます。