0.5 为什么 90% 利用率已经很危险
上一节:0.4 用 Little’s Law 估算容量 | 下一节:0.6 协调遗漏:压测报告的最大谎言 配套代码:00-introduction/05-queueing 动手:这一节强烈建议跑一遍配套模拟,因为结论违反直觉。
一句话结论
延迟对「利用率」的反应不是线性的。 CPU 用到 50% 时,排队几乎不存在;用到 90% 时,平均排队时间已经是服务时间的 9 倍。这就是容量规划必须留 30%–50% 余量的数学依据,而不是「保险起见」。
一、先用超市收银台建立直觉
想象一家超市只有一个收银台(一个 CPU 核 / 一个连接):
| 收银员的忙碌程度 | 你去结账时的体验 |
|---|---|
| 50% 忙(一半时间在发呆) | 你走过去基本不用等,直接结账 |
| 80% 忙 | 有时要等一下,因为前面可能正好有人 |
| 90% 忙 | 大概率要排队,而且队伍不短 |
| 95% 忙 | 几乎每次都要排长队 |
| 99% 忙 | 队伍无限增长,系统实际上已经瘫了 |
关键洞察:收银员只从 50% 忙到 90% 忙,「空闲时间」只从 50% 降到 10%(少了 5 倍),但你的等待时间却涨了远不止 5 倍。
为什么?因为空闲时间是「吸收波动」的缓冲。当收银员有 50% 空闲时,偶尔来一波客人马上就能消化掉;当只剩 10% 空闲时,一波客人来了就得排队,而排队期间又来了新客人——排队会自我强化。
二、把它量化:排队公式
在「单队列、单服务、到达时间随机、服务时间随机」这个简化模型下(学术上叫 M/M/1),平均排队等待时间的近似公式是:
平均等待时间 ≈ (ρ / (1 - ρ)) × 服务时间
其中 ρ(rho)就是利用率,取值 0 到 1。
代入不同的 ρ:
| 利用率 ρ | 系数 ρ/(1-ρ) | 平均排队时间 | 换句话说 |
|---|---|---|---|
| 0.5(50%) | 1.0 | = 1 × 服务时间 | 排队时间约等于服务本身 |
| 0.8(80%) | 4.0 | = 4 × 服务时间 | 服务 10 ms → 平均等 40 ms |
| 0.9(90%) | 9.0 | = 9 × 服务时间 | 服务 10 ms → 平均等 90 ms |
| 0.95(95%) | 19.0 | = 19 × 服务时间 | 服务 10 ms → 平均等 190 ms |
| 0.99(99%) | 99.0 | = 99 × 服务时间 | 服务 10 ms → 平均等 990 ms |
看清楚这张表:利用率从 90% 涨到 95%,只多了 5 个百分点,但排队时间从 9 倍涨到 19 倍——翻了一倍多。
这就是为什么「我们 CPU 只用了 90%,还有余量」是一句危险的话。那 10% 不是「余量」,是「马上要溢出的缓冲区」。
三、这对工程实践意味着什么
3.1 容量规划必须留余量(headroom)
假设你的服务在 4 核 8G 的机器上,压测发现 CPU 打到 100% 时能处理 1000 QPS。那么这台机器的安全容量是多少?
| 目标利用率 | 安全容量 | 理由 |
|---|---|---|
| 90% | 900 QPS | 排队已经是服务时间的 9 倍,P99 很难看 |
| 80% | 800 QPS | 排队 4 倍,大多数场景可接受 |
| 60%–70% | 600–700 QPS | 推荐区间:能吸收突发流量与 GC 停顿 |
| 50% | 500 QPS | 很保守,适合延迟极敏感的服务 |
注意这里的反直觉之处:不是「1000 QPS 打满才算用尽」,而是「跑在 600 QPS 时,你才真正有余力应对突发」。
突发流量怎么来的:重试风暴、定时任务、缓存集中失效、上游重试、爬虫、业务活动。这些都不是你能控制的。
3.2 不要把所有资源都跑满
这条原则适用于所有「排队型」资源:
| 资源 | 建议水位 | 超过后会怎样 |
|---|---|---|
| CPU | < 60%–70% | 排队延迟非线性上升、GC/JIT 线程抢不到 CPU |
| 数据库连接池 | pending 长期为 0,active < 70% | 请求开始排队,延迟雪崩 |
| 线程池 / 协程调度器队列 | 队列深度接近 0 | 任务堆积,延迟不可控 |
| 磁盘 IO 队列 | 队列深度低 | await 时间飙升 |
| 网络带宽 | < 70% | 丢包、重传、TCP 拥塞 |
3.3 「平均利用率」是骗人的
一个更隐蔽的问题:平均 60% 不代表安全。
假设 CPU 在「30%」和「95%」之间来回跳,平均是 62%。看起来达标,但实际上在那些 95% 的时刻,系统已经在剧烈排队了。
所以要看的是「利用率的分布」,而不是平均值——至少要看 P95/P99 利用率,或者直接看「饱和度指标」(连接池 pending、队列深度)。
顺带一提:这和第 3 节是同一个道理的不同侧面。平均值的谎言,在延迟上表现为「掩盖长尾」,在利用率上表现为「掩盖高峰」。
四、这个模型的边界(不要生搬硬套)
M/M/1 是一个极度简化的模型,现实中有很多偏离:
| 模型假设 | 现实 | 影响 |
|---|---|---|
| 到达时间是随机的(泊松) | 真实流量有规律的波峰(早高峰、活动) | 实际比模型更差 |
| 服务时间随机 | 有缓存时会呈现「快慢两极」 | 分布更复杂 |
| 单队列单服务 | 多核、多阶段、有优先级 | 需要更复杂的模型 |
| 队列无限长 | 真实队列有上限,满了会拒绝 | 到达上限后是「快速失败」而非「无限等待」 |
所以这个公式的用途是:
- ✅ 建立方向性直觉:「利用率高 → 延迟非线性爆炸」这个结论永远成立。
- ✅ 作为留余量的依据:30%–50% 的 headroom 不是拍脑袋。
- ❌ 不要用它精确预测你的系统延迟——那要靠实测。
五、本节小结
- 延迟对利用率的反应是非线性的:ρ=0.5 → 1×,ρ=0.8 → 4×,ρ=0.9 → 9×,ρ=0.95 → 19×。
- 「90% 利用率」不是「还剩 10% 余量」,而是「延迟已经涨了 9 倍,缓冲区即将耗尽」。
- 容量规划要留 headroom(常用 30%–50%),这是数学结论,不是保守主义。
- 所有排队型资源(CPU、连接池、线程池、IO 队列)都适用这条规律。
- 要看利用率的高峰(P95/P99)或饱和度指标,而不是平均值。
- M/M/1 是简化模型,用来建立直觉和定余量,不用来精确预测。
六、自测
- 同事说:「我们服务器 CPU 才用 65%,很健康。」你会追问哪两个问题?
- 一个服务在压测中表现为:QPS 从 500 涨到 700 时,P50 只从 10 ms 涨到 12 ms;但从 700 涨到 800 时,P50 从 12 ms 直接涨到 90 ms。请用本节的模型解释这个现象,并说明这台机器的「实用容量」应该是多少。
- 为什么「加缓存」能缓解利用率问题?它改变的是公式里的哪个量?
- ① CPU 利用率是平均值还是峰值? 如果是平均值,要看 P95/P99;② 瓶颈是不是 CPU? 如果真正的瓶颈是连接池或下游,CPU 65% 说明不了任何问题——要看饱和度指标(连接池 pending、队列深度)。还可以追问:这是单机还是集群平均、测量时间窗口多长、有没有容器 CPU limit。
- 这是典型的拐点现象。500–700 QPS 区间利用率还低(排队系数小),延迟基本等于服务时间;700 QPS 之后利用率逼近高位,排队系数急剧放大,所以 P50 骤增。实用容量应取拐点之前——大约 600–650 QPS(留出余量),而不是 700 或 800。
- 缓存降低的是服务时间(原本 20 ms 的数据库查询变成 0.5 ms 的内存读取),从而在同样 QPS 下降低了利用率 ρ。它没有改变到达率 λ,也没有增加服务能力,只是让每个请求「更快做完」,于是同样的硬件能扛更多请求。(副作用:内存成本、一致性风险、以及未命中时的抖动——见 0.2 节的代价清单。)