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

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

読了目安 約4分

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

30秒でつかむ 使用率が上がると、仕事が列を作る
使用率が上がると、仕事が列を作る到着する仕事が処理能力へ近づくと待ち行列が伸び、応答時間が急に増えます。1仕事が到着リクエスト2使用率が上昇空きが減る3待ち行列応答が遅れる
  1. 仕事が到着 リクエスト
  2. 使用率が上昇 空きが減る
  3. 待ち行列 応答が遅れる
この章の目次

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

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

使用率と待ち時間

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

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

窓口が 1 つで、到着も処理もランダムに散らばる行列では、平均待ち時間が平均サービス時間の何倍かが次の式で決まります。

平均待ち時間平均サービス時間    ρ1ρ \frac{\text{平均待ち時間}}{\text{平均サービス時間}} \;\approx\; \frac{\rho}{1-\rho}

ρ\rho が 1 に近づくと分母が 0 に近づくので、値がいくらでも大きくなります。 散らばり方が違えば係数は変わりますが、急に伸びる形は共通です。

使用率 ρ\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 の結果が実行ごとにどれだけ揺れるかを見てください。 飽和の近くでは、同じ条件でも測るたびに結果がばらつきます。

答えを見る

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 万件でもまだ足りていません。

シードを変えた ρ=0.95\rho = 0.95 の平均待ち時間は、1、2、3、42 で 19.59、15.58、18.73、16.75 でした。 使用率 0.5 の行はほとんど動かないのに、0.95 では 2 割以上揺れます。 本番の計測でも、飽和に近い系ほど 1 回の測定を信用できなくなります。