文档目录

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 倍。

四、四个必须记住的点

  1. λ 和 W 必须来自同一段业务。 用「全站平均 QPS」乘「全站平均延迟」得到的数字没有意义——要么按接口算,要么按资源维度(都走数据库的算一组)算。
  2. 用峰值 QPS,不用平均 QPS。 系统是在峰值时刻垮掉的,不是平均时刻。
  3. 同时算 P50 版和 P99 版。 P50 版告诉你「平时需要多少」,P99 版告诉你「最坏时刻需要多少」。两者的差距就是你的风险敞口。
  4. Little’s Law 只描述稳态平均,不含排队。 所以算出来的数字是下界,实际配置要考虑排队余量(第 5 节解释了为什么)。

五、动手改造

改动 观察什么
把 peakQps 从 1000 改成 10000 资源需求线性增长——但你的数据库连接上限是固定的,什么时候会撞墙?
把 P99 从 60ms 改成 300ms 需要的并发变成多少?这解释了为什么「一次 GC 停顿」能让连接池瞬间打满
在 capacityPlan 里把余量从 30% 改成 50% 成本增加多少?(把它换算成机器台数,就明白 headroom 是有代价的)
自己加一个「重试」参数:假设 5% 的请求会重试一次 λ 实际变成多少?资源需求涨多少?(这就是重试风暴的数学模型)

六、这段代码的局限

  • 它假设到达率是恒定的,真实流量有波峰波谷,瞬时并发会远高于平均值。
  • 它不包含排队模型,所以「需要 8 个连接」不代表「8 个连接就够了」。
  • 它假设请求之间相互独立,而真实系统中慢请求会占用资源影响后续请求。