0.4 配套代码:Little’s Law 容量计算器
对应小节:0.4 用 Little’s Law 估算容量 要验证的结论:用平均值配置资源是危险的;峰值 QPS × 尾部延迟才是资源需求的上界。
一、公式回顾
L = λ × W
L:在途请求数(个)
λ:到达率(req/s)
W:驻留时间(秒)
二、代码:三个用法 + 一个危险演示
// src/main/kotlin/lesson00/LittlesLaw.kt
package lesson00
import kotlin.math.ceil
/**
* 计算在途请求数(并发需求)。
* @param qps 到达率,单位 req/s
* @param latencyMs 驻留时间,单位毫秒
*/
fun inFlight(qps: Double, latencyMs: Double): Double = qps * (latencyMs / 1000.0)
/**
* 反推:给定并发上限与单次处理时间,理论最大吞吐。
* 注意这是"理论值"——它假设资源 100% 忙碌且没有排队。
*/
fun maxThroughput(concurrency: Int, latencyMs: Double): Double =
concurrency / (latencyMs / 1000.0)
fun main() {
// ═══════════════════════════════════════════════════════════
// 用法一:算出需要多少连接/线程/协程
// ═══════════════════════════════════════════════════════════
println("=== 用法一:订单查询接口需要多少连接?===")
val qps = 200.0
val avgLatency = 30.0
println("峰值 QPS = %.0f, 平均延迟 = %.1f ms".format(qps, avgLatency))
println("→ 平均需要 %.1f 个连接".format(inFlight(qps, avgLatency)))
println("→ 如果连接池设成 100,有 %.0f 个连接平时是闲置的(白占数据库内存)"
.format(100 - inFlight(qps, avgLatency)))
println()
// ═══════════════════════════════════════════════════════════
// 用法二:从资源上限反推吞吐天花板
// ═══════════════════════════════════════════════════════════
println("=== 用法二:数据库最多 50 个连接,单查询 20 ms,理论最大吞吐?===")
val theoretical = maxThroughput(50, 20.0)
println("→ 理论上限 %.0f req/s".format(theoretical))
println("→ 但这是「50 个连接永远 100% 忙碌且零排队」的理想情况")
println("→ 考虑排队效应(利用率不该超过 70%%),实际可稳定支撑约 %.0f req/s"
.format(theoretical * 0.7))
println()
// ═══════════════════════════════════════════════════════════
// 用法三:从"活跃请求数"反推实际延迟
// ═══════════════════════════════════════════════════════════
println("=== 用法三:观测到 100 个活跃请求,QPS 是 500,平均延迟是多少?===")
val observedConcurrency = 100.0
val observedQps = 500.0
println("→ W = L / λ = %.0f / %.0f = %.3f s = %.0f ms"
.format(observedConcurrency, observedQps, observedConcurrency / observedQps,
observedConcurrency / observedQps * 1000))
println("→ 如果这个数字远大于「服务端处理时间」,多出来的部分就是排队")
println()
// ═══════════════════════════════════════════════════════════
// 危险演示:按平均值配置 vs 按尾部配置
// ═══════════════════════════════════════════════════════════
demonstrateTheDanger()
// ═══════════════════════════════════════════════════════════
// 敏感性分析:延迟变化对资源需求的影响
// ═══════════════════════════════════════════════════════════
sensitivityTable()
}
/**
* 核心危险:同一份流量,按 P50 配置资源和按 P99 配置资源,需求差 7 倍以上。
* 而系统恰恰在 P99 那个时刻最需要资源。
*/
fun demonstrateTheDanger() {
val peakQps = 1000.0
val p50 = 8.0
val p99 = 60.0
println("=== 危险演示:同一个接口,两种算法差多少?===")
println("峰值 QPS = %.0f".format(peakQps))
println("按 P50 计算:L = %.0f × %.3f = %5.1f 个".format(peakQps, p50 / 1000, inFlight(peakQps, p50)))
println("按 P99 计算:L = %.0f × %.3f = %5.1f 个".format(peakQps, p99 / 1000, inFlight(peakQps, p99)))
println()
println("两个数字差 %.1f 倍。".format(inFlight(peakQps, p99) / inFlight(peakQps, p50)))
println()
println("如果把连接池按 P50 配置(8 个):")
println(" → 平时刚好够用(CPU 看着很健康)")
println(" → 一旦进入那 1% 的慢时刻,60 个请求抢 8 个连接")
println(" → 排队让延迟进一步上升 → 更多请求堆积 → 雪崩")
println()
println("如果把连接池按 P99 配置(%d 个):".format(ceil(inFlight(peakQps, p99)).toInt()))
println(" → 最坏时刻够用,但平时有 %.0f 个连接闲置(浪费数据库内存)"
.format(inFlight(peakQps, p99) - inFlight(peakQps, p50)))
println(" → 实践做法:取 P95 左右的值 + 排队上限(有界池)+ 快速失败")
println()
}
/** 敏感性分析:延迟涨一点,资源需求涨多少 */
fun sensitivityTable() {
val qps = 1000.0
println("=== 敏感性分析(QPS = %.0f):延迟 → 需要的并发数 ===".format(qps))
println("平均延迟(ms) | 需要并发数 | 相对 10ms 的倍数")
val baseline = inFlight(qps, 10.0)
for (ms in listOf(10, 15, 20, 30, 50, 100, 200)) {
val l = inFlight(qps, ms.toDouble())
println("%11d | %10.1f | %14.1fx".format(ms, l, l / baseline))
}
println()
println("注意:延迟从 10ms 涨到 100ms(10 倍),所需并发也涨 10 倍。")
println(" 但你的连接池是按 10ms 配的 → 延迟一涨,连接池立刻成为瓶颈。")
println(" 这就是「延迟恶化 → 资源耗尽 → 延迟更恶化」的正反馈。")
}
/**
* 一个真实的容量规划表格生成器:
* 给定 QPS 与目标延迟,输出每种资源该配多少。
*/
fun capacityPlan(peakQps: Double, p50Ms: Double, p95Ms: Double, p99Ms: Double) {
println("=== 容量规划建议(峰值 QPS = %.0f)===".format(peakQps))
println("场景 | 延迟 | 并发需求 | 建议配置(含 30% 余量)")
println("乐观 (P50) | %5.1fms | %7.1f | %d".format(p50Ms, inFlight(peakQps, p50Ms),
ceil(inFlight(peakQps, p50Ms) * 1.3).toInt()))
println("典型 (P95) | %5.1fms | %7.1f | %d".format(p95Ms, inFlight(peakQps, p95Ms),
ceil(inFlight(peakQps, p95Ms) * 1.3).toInt()))
println("悲观 (P99) | %5.1fms | %7.1f | %d".format(p99Ms, inFlight(peakQps, p99Ms),
ceil(inFlight(peakQps, p99Ms) * 1.3).toInt()))
println()
println("建议:以 P95 那一行为基准配置,用有界队列 + 限流保护住 P99 的极端情况。")
}
三、预期输出(关键部分)
=== 危险演示:同一个接口,两种算法差多少?===
峰值 QPS = 1000
按 P50 计算:L = 1000 × 0.008 = 8.0 个
按 P99 计算:L = 1000 × 0.060 = 60.0 个
两个数字差 7.5 倍。
四、四个必须记住的点
- λ 和 W 必须来自同一段业务。 用「全站平均 QPS」乘「全站平均延迟」得到的数字没有意义——要么按接口算,要么按资源维度(都走数据库的算一组)算。
- 用峰值 QPS,不用平均 QPS。 系统是在峰值时刻垮掉的,不是平均时刻。
- 同时算 P50 版和 P99 版。 P50 版告诉你「平时需要多少」,P99 版告诉你「最坏时刻需要多少」。两者的差距就是你的风险敞口。
- Little’s Law 只描述稳态平均,不含排队。 所以算出来的数字是下界,实际配置要考虑排队余量(第 5 节解释了为什么)。
五、动手改造
| 改动 | 观察什么 |
|---|---|
把 peakQps 从 1000 改成 10000 |
资源需求线性增长——但你的数据库连接上限是固定的,什么时候会撞墙? |
| 把 P99 从 60ms 改成 300ms | 需要的并发变成多少?这解释了为什么「一次 GC 停顿」能让连接池瞬间打满 |
在 capacityPlan 里把余量从 30% 改成 50% |
成本增加多少?(把它换算成机器台数,就明白 headroom 是有代价的) |
| 自己加一个「重试」参数:假设 5% 的请求会重试一次 | λ 实际变成多少?资源需求涨多少?(这就是重试风暴的数学模型) |
六、这段代码的局限
- 它假设到达率是恒定的,真实流量有波峰波谷,瞬时并发会远高于平均值。
- 它不包含排队模型,所以「需要 8 个连接」不代表「8 个连接就够了」。
- 它假设请求之间相互独立,而真实系统中慢请求会占用资源影响后续请求。