34.2 性能建模 (利特尔法则、排队论)
一句话
建模不是把延迟算准到毫秒,而是用 4 条运算律 + 1 个排队公式,在压测之前就回答三个问题:理论上限是多少、瓶颈在哪个资源、加机器/优化哪一段有没有用。
建模回答的三个问题
34.1 给了指标定义与 M/M/1 曲线,这一节把曲线变成可计算的模型。
| 问题 | 需要输入 | 模型输出 | 典型误差 |
|---|---|---|---|
| 容量:这套配置能扛多少 QPS? | 请求的 CPU 时间、并发度、目标利用率 | 需要的核数 / 连接数 / 实例数 | ±20% 到 2 倍 |
| 瓶颈:先撑不住的是谁? | 每个资源的访问次数 × 单次耗时 | 各资源利用率,最高者即瓶颈 | 通常准确到「是哪个资源」 |
| 收益:优化 A 还是 B? | 各段耗时分解、串行比例 | 优化后的端到端延迟 / 加速比 | 方向可靠,数值偏乐观 |
🔑 模型给数量级和方向,压测给真实数字 (34.5 性能测试)。先建模再压测,能避免「压出问题却不知道看哪里」;只压测不建模,容易把某次环境噪声当成容量上限。
四条运算律:不需要泊松假设
这四条只依赖「计数守恒」,不要求指数分布,因此在真实系统里最耐用。
| 定律 | 公式 | 回答什么 | 用在哪 |
|---|---|---|---|
| 利用率定律 (Utilization Law) | U = X × S | 某资源的利用率 = 到达该资源的吞吐 × 单次占用时间 | CPU、磁盘、连接池、DB |
| 利特尔法则 (Little's Law) | L = λ × W | 在途请求数 = 到达率 × 停留时间 | 线程池、连接池、队列长度 |
| 强制流定律 (Forced Flow Law) | X_k = V_k × X_0 | 下游吞吐 = 入口吞吐 × 每次请求访问它的次数 | N+1 查询、缓存、RPC 扇出 |
| 交互式响应时间定律 | R = N / X - Z | 闭环系统里,响应时间 = 用户数 / 吞吐 - 思考时间 | 并发用户、连接池、压测模型 |
算一遍:瓶颈到底是谁
下单接口 300 QPS,API 每请求耗 8ms CPU,跑在 4 核 上;每个请求访问数据库 4 次,每次 4ms,DB 是 8 核。
API:U = 300 × 0.008 / 4 = 60%
DB :X = 300 × 4 = 1200 QPS
U = 1200 × 0.004 / 8 = 60%
两个资源都恰好 60%,看起来「很均衡」。但把 QPS 提到 350:
API:350 × 0.008 / 4 = 70%
DB :350 × 4 × 0.004 / 8 = 70%
仍然同时到 70%。真实的系统不会这么巧:谁的 U 先到 100%,谁就是瓶颈。上例中只要 DB 的访问次数从 4 变成 5 (多一次校验查询),DB 就会先撞线;反过来,把 API 的 8ms 优化到 6ms,瓶颈立刻全部转移到 DB。
🔑 优化的第一步不是改代码,是列出资源清单并各算一个 U。没有这张表,你优化的很可能是那个「看起来最忙」而不是「最先饱和」的资源。
强制流定律:N+1 为什么是灾难
一个列表页返回 100 条记录,每条记录再查一次作者 (N+1),DB 每次查询 0.2ms,DB 有 4 核,入口 100 QPS:
V = 1 + 100 = 101 次查询/请求
X_db = 100 × 101 = 10100 QPS
U = 10100 × 0.0002 / 4 = 50.5%
把 N+1 改成一次批量查询 (V=2):
X_db = 200 QPS,U = 200 × 0.0002 / 4 = 1%
同样的功能,DB 利用率从 50% 掉到 1%,50 倍差距。这就是为什么「加索引」救不了 N+1:索引让单次查询从 2ms 变 0.2ms,只快 10 倍;减少查询次数快 50 倍。两者叠加才是 500 倍。
交互式响应时间定律:并发用户为什么越用越慢
闭环系统里,N 个用户、思考时间 Z、系统吞吐 X,则平均响应时间:
R = N / X - Z
200 个在线用户,思考时间 20s,系统吞吐 8 QPS:
R = 200 / 8 - 20 = 5s
吞吐掉到 4 QPS 时:
R = 200 / 4 - 20 = 30s
吞吐只降了一半,用户感受到的延迟涨了 6 倍。因为用户不会因为系统慢就少点一次,他们只是把更多时间花在等待上。这也解释了闭环压测的陷阱:并发数固定时,系统变慢会自动降低发压速率,吞吐曲线看起来很「稳」,但延迟已经爆了 (34.5 性能测试与负载模型 的 coordinated omission)。
利特尔法则:能用,但有四个前提
8.3 已经用 并发 = QPS × 延迟 估过连接池。这里补的是它什么时候不成立。
L = λ × W
| 前提 | 含义 | 被违反时的现象 |
|---|---|---|
| 稳态 | 统计窗口内 λ、W 基本不变 | 启动、发布、促销峰值时算出的 L 毫无意义 |
| 守恒 | 请求不凭空产生/消失,重试算新请求 | 客户端自动重试让 λ 翻倍,但你没把它算进去 |
| 同一系统边界 | L、λ、W 必须描述同一段 | 用网关 QPS 配服务端处理时间,差值就是排队与网络 |
| 同一时间窗 | 三个量取同一窗口 | 用峰值 QPS 配平均延迟,低估 3–5 倍 |
三种用法,一个式子
| 已知 | 求 | 式子 | 例子 |
|---|---|---|---|
| QPS、延迟 | 并发 / 池大小 | L = λW | 500 QPS × 50ms = 25 个在途请求 |
| 池大小、延迟 | 吞吐上限 | X = L / W | 20 连接 / 50ms = 400 QPS |
| 并发、QPS | 延迟 | W = L / λ | 100 在途 / 800 QPS = 125ms |
⚠️ 「并发」不等于「线程数」。100 个线程里有 90 个在等锁、等 IO,它们都算在途;反过来,异步 IO 用一个线程也能有 1000 个在途请求。用线程数代替 L,会把连接池算大一个数量级。
代码验证:模拟出来的 L 是否等于 λW
下面的程序模拟 M/M/c 队列 (泊松到达、指数服务),用对 N(t) 积分得到时间平均在系统数,再和 λeff × W 对比。
package main
import (
"container/heap"
"fmt"
"math"
"math/rand"
"sort"
)
type freeTimes []float64
func (f freeTimes) Len() int { return len(f) }
func (f freeTimes) Less(i, j int) bool { return f[i] < f[j] }
func (f freeTimes) Swap(i, j int) { f[i], f[j] = f[j], f[i] }
func (f *freeTimes) Push(x any) { *f = append(*f, x.(float64)) }
func (f *freeTimes) Pop() any { old := *f; v := old[len(old)-1]; *f = old[:len(old)-1]; return v }
type event struct {
t float64
d int
}
// simulate 模拟 M/M/c:lam 到达率(个/秒),mu 服务率(个/秒),c 个服务台。
// 返回时间平均在系统数 L、平均停留时间 W、平均排队时间 Wq、模拟总时长。
func simulate(lam, mu float64, c, n int, seed int64) (L, W, Wq, horizon float64) {
rng := rand.New(rand.NewSource(seed))
free := make(freeTimes, c) // 每个服务台的下一次空闲时刻
arr := make([]float64, n)
start := make([]float64, n)
fin := make([]float64, n)
t := 0.0
for i := 0; i < n; i++ {
t += rng.ExpFloat64() / lam // 下一次到达
svc := rng.ExpFloat64() / mu
earliest := heap.Pop(&free).(float64) // 最早空闲的服务台
arr[i] = t
start[i] = math.Max(t, earliest) // 排队:等到有服务台空出来
fin[i] = start[i] + svc
heap.Push(&free, fin[i])
}
horizon = 0
for _, f := range fin { // 最后一个到达的请求不一定最后结束
if f > horizon {
horizon = f
}
}
// 对 N(t) 做积分,得到时间平均在系统数
evs := make([]event, 0, 2*n)
for i := 0; i < n; i++ {
evs = append(evs, event{arr[i], +1}, event{fin[i], -1})
}
sort.Slice(evs, func(i, j int) bool { return evs[i].t < evs[j].t })
area, cur, last := 0.0, 0, 0.0
for _, e := range evs {
area += float64(cur) * (e.t - last)
cur += e.d
last = e.t
}
for i := 0; i < n; i++ {
W += fin[i] - arr[i]
Wq += start[i] - arr[i]
}
return area / horizon, W / float64(n), Wq / float64(n), horizon
}
// erlangC 返回 M/M/c 的排队概率 (到达后需要等待的概率),a = lam/mu。
func erlangC(c int, a float64) float64 {
sum, fact := 0.0, 1.0
for k := 0; k < c; k++ {
if k > 0 {
fact *= float64(k)
}
sum += math.Pow(a, float64(k)) / fact
}
fact *= float64(c) // 补上 c!
top := math.Pow(a, float64(c)) / fact * float64(c) / (float64(c) - a)
return top / (sum + top)
}
func main() {
// 服务时间 S=10ms (mu=100),每台利用率 80%,各模拟 100 万个请求
fmt.Printf("%-2s %-5s %-9s %-9s %-9s %-8s %-8s\n", "c", "λ", "Wq实测", "Wq理论", "W实测", "L", "λeff×W")
for i, c := range []int{1, 2, 4} {
lam := 80.0 * float64(c)
L, W, Wq, horizon := simulate(lam, 100, c, 1000000, int64(42+i*1000))
rho := lam / (100 * float64(c))
wqTheory := erlangC(c, lam/100) * 0.01 / (float64(c) * (1 - rho))
lambdaEff := 1000000 / horizon
fmt.Printf("%-2d %-5.0f %-9.2f %-9.2f %-9.2f %-8.3f %-8.3f\n",
c, lam, Wq*1000, wqTheory*1000, W*1000, L, lambdaEff*W)
}
}
输出:
c λ Wq实测 Wq理论 W实测 L λeff×W
1 80 40.83 40.00 50.84 4.070 4.070
2 160 17.64 17.78 27.63 4.419 4.419
4 320 7.40 7.46 17.40 5.568 5.568
三点结论:
L与λeff×W完全一致:这是同一时间窗下的恒等关系,不能用来证明模拟器正确;它真正说明的是必须用有效到达率λeff = n/horizon,不能直接套参数 λ。Wq与 Erlang C 的偏差在 2.5% 以内:这才是对模拟器的检验;偏差来自有限样本与随机波动,不影响趋势判断。c从 1 增到 4,Wq 从 40.8ms 降到 7.4ms:模拟与 Erlang C 都给出同样趋势,这就是下一节「合并队列」的量化收益。
排队模型怎么选:先看 Kendall 记号
A/S/c/K/N/D 依次是:到达过程 / 服务时间分布 / 服务台数 / 队列容量 / 顾客数 / 调度规则。工程上常用的只有下面几种。
| 模型 | 假设 | 公式 | 什么时候用 | 注意 |
|---|---|---|---|---|
| M/M/1 | 泊松到达、指数服务、单服务台、无限队列 | Wq = ρS/(1-ρ),W = S/(1-ρ) | 快速估算单队列 | 指数服务 Cs=1 是常见基准;Cs<1 时偏悲观,Cs>1 时低估长尾 |
| M/M/c | c 个服务台共享一个队列 | Erlang C:Wq = C(c,a)·S/(c(1-ρ)) | 连接池、线程池、多副本 | 需要查/算 Erlang C,a = λS |
| M/D/1 | 服务时间恒定 | Wq = ρS/(2(1-ρ)) | 定长任务、固定大小包 | 只有 M/M/1 的一半等待 |
| M/G/1 | 服务时间一般分布 | P-K 公式:Wq = ρS(1+Cs²)/(2(1-ρ)) | 已知服务时间变异系数 | Cs 是服务时间标准差/均值 |
| M/M/1/K | 队列长度上限 K | 用生灭过程求阻塞率 | 有界队列、限流 | 队列满后请求被拒绝 |
| G/G/1 | 一般到达、一般服务 | Kingman 近似 | 真实流量 (非泊松) | 见下节的变异性公式 |
💡 为什么 M/M/1 常被当成「悲观估计」:指数分布的
Cs=1,而真实批处理、缓存命中、固定 SQL 的Cs往往小于 1。但 GC 停顿、慢查询、锁等待会让Cs远大于 1,这时 M/M/1 反而低估长尾。
多服务台:合并队列是免费的午餐
4 台服务器、总吞吐 320 QPS、每台 80% 利用率、S=10ms。两种排队方式:
| 方式 | 结构 | 平均排队 Wq | 平均停留 W |
|---|---|---|---|
| 各自排队 | 4 个独立的 M/M/1,每个 80 QPS | 40.0ms | 50.0ms |
| 共享队列 | 1 个 M/M/4,总 320 QPS | 7.5ms | 17.5ms |
同样的机器、同样的利用率,共享队列把排队时间砍到 1/5.4。Erlang C 的完整曲线:
| 服务台 c | 总利用率 | 每台利用率 | 排队概率 C(c,a) | Wq (S=10ms) | W |
|---|---|---|---|---|---|
| 1 | 80% | 80% | 0.800 | 40.0ms | 50.0ms |
| 2 | 80% | 80% | 0.711 | 17.8ms | 27.8ms |
| 4 | 80% | 80% | 0.596 | 7.5ms | 17.5ms |
| 8 | 80% | 80% | 0.458 | 2.9ms | 12.9ms |
这不是数学游戏,它对应四个工程决策:
| 场景 | 各自排队 (差) | 共享队列 (好) |
|---|---|---|
| 连接池 | 每个 worker 一个池,空闲连接浪费 | 全局池,按需分配 |
| 任务队列 | 每个消费者一个队列,忙闲不均 | 一个队列 + 多消费者 |
| 负载均衡 | 按连接数分发,长连接导致倾斜 | 按请求分发 / 最少连接 / P2C |
| 数据库 | 每个服务一个连接池,总量失控 | 统一代理层 / 全局配额 |
🔑 共享队列的收益来自「让空闲的服务台去处理别人的积压」。代价是共享队列本身要有锁或原子操作,规模极大时会变成新瓶颈;同时会破坏局部性 (缓存亲和、会话粘滞)。所以不是「越共享越好」,而是「在共享成本 < 排队收益时共享」。
变异性:被忽略的第二个参数
排队时间不只取决于平均服务时间,还取决于服务时间的波动。M/G/1 的 P-K 公式:
Wq = ρS/(1-ρ) × (1 + Cs²)/2
ρ=0.8、S=10ms 时,不同变异系数下的排队时间:
| 服务时间分布 | Cs | 倍数 | Wq | 典型来源 |
|---|---|---|---|---|
| 定长 | 0 | 0.5× | 20ms | 固定大小包、定时任务 |
| 低变异 | 0.5 | 0.625× | 25ms | 缓存命中、简单查询 |
| 指数 | 1.0 | 1× | 40ms | M/M/1 基准 |
| 高变异 | 2.0 | 2.5× | 100ms | 混合快慢查询、锁竞争 |
| 极高变异 | 5.0 | 13× | 520ms | GC 停顿、慢 SQL、跨机房重试 |
到达侧同样有变异。G/G/1 的 Kingman 近似把两边合起来:
Wq ≈ ρS/(1-ρ) × (Ca² + Cs²)/2
| 到达 Ca | 服务 Cs | Wq (ρ=0.8, S=10ms) |
|---|---|---|
| 1.0 | 1.0 | 40ms |
| 0.5 | 0.5 | 10ms |
| 2.0 | 2.0 | 160ms |
| 2.0 | 0.5 | 85ms |
把 Ca 从 2 降到 1,和把 Cs 从 2 降到 1,收益一样大。这解释了为什么下面这些手段有效:
| 手段 | 作用在哪个参数 | 效果 |
|---|---|---|
| 限流 / 流量整形 | Ca ↓ | 削掉突发,排队时间下降 |
| 连接池预热、JIT 预热 | Cs ↓ | 消除冷启动的长服务时间 |
| 超时截断 | 尾部截断 | 防止单个慢请求拖住队列,但会产生重试 (新的变异) |
| 请求合并 / 批处理 | 单次 S ↑、次数 ↓ | 平均吞吐升,但单请求等待增加,需权衡 |
| 隔离舱 (bulkhead) | 限制故障传播 | 把高变异限制在小池子里 |
⚠️ 超时不是免费的:超时把「慢请求」变成「失败 + 重试」,如果重试没有预算控制,Ca 反而更高,形成重试风暴 (35.5 / 35.7)。
闭环系统与扩展性上限
闭环:并发用户模型
压测工具用固定并发 (闭环) 时,测的是「N 个用户能跑出多少吞吐」,不是「入口有 1000 QPS 时会怎样」。两者用不同的模型:
| 维度 | 开环 (Open) | 闭环 (Closed) |
|---|---|---|
| 谁控制到达 | 外部固定速率 | N 个用户/协程,完成后再发下一个 |
| 适合描述 | 真实入口流量、网关 | 会话型业务、连接池、数据库连接 |
| 系统变慢时 | 到达率不变,队列堆积 | 发压自动变慢,吞吐下降 |
| 风险 | 把系统压垮 | 掩盖容量问题 (34.5 性能测试) |
| 典型工具 | wrk2、vegeta、k6 constant-arrival-rate | ab、JMeter 固定线程、Locust 固定用户 |
扩展性:USL 曲线
Amdahl 定律只考虑「串行比例」,预测的是「无限加核也超不过 1/s」。真实系统还有跨核协调成本 (缓存一致性、锁、跨节点通信)。USL (Universal Scalability Law):
C(N) = N / (1 + σ(N-1) + κN(N-1))
σ:串行争用比例 (排队拿同一把锁、走同一段单线程逻辑)κ:交叉通信/一致性开销 (缓存行同步、共识、跨节点 RPC)
取 σ=0.05、κ=0.001:
| 核数 N | 1 | 2 | 4 | 8 | 16 | 32 | 48 | 64 |
|---|---|---|---|---|---|---|---|---|
| 相对吞吐 C(N) | 1.00 | 1.90 | 3.44 | 5.69 | 8.04 | 9.03 | 8.56 | 7.82 |
| 每核效率 | 100% | 95% | 86% | 71% | 50% | 28% | 18% | 12% |
32 核是峰值,之后加核反而更慢。三种典型形状:
| 现象 | 主导参数 | 对策 |
|---|---|---|
| 加机器吞吐线性涨,但效率越来越低 | σ 大 | 减少共享资源、缩短临界区、无状态化 |
| 加机器到某点后吞吐下降 | κ 大 | 分片、异步、减少跨节点同步、批量合并 |
| 16 核只有 8 倍 | σ+κ 共同作用 | 先测单核效率,再决定是否值得加 |
🔑 USL 的实用价值:它告诉你「这套架构的容量上限是几核」,而不是「还能加几台机器」。如果 32 核后曲线掉头,继续扩容是给云厂商送钱。
把模型落到容量决策:四步
| 步骤 | 动作 | 产出 |
|---|---|---|
| ① 画资源链 | 列出每个可能饱和的资源:CPU、内存带宽、磁盘 IOPS、网络、连接池、DB、下游 | 资源清单 |
| ② 取参数 | 每请求的 V (访问次数)、S (单次耗时)、c (并发能力)、目标 ρ | 参数表 |
| ③ 算 U 与 W | U = X·V·S/c,找最大 U;用 Little 算池大小 | 瓶颈与容量 |
| ④ 敏感性分析 | 哪个参数 ±20% 会翻转结论 | 风险清单 |
算一遍:500 QPS 的 API 需要几核
CPU:500 QPS × 8ms = 4.0 核 → 4 核机器 U=100%,已经饱和
按 70% 红线:500 × 0.008 / 0.7 = 5.71 → 至少 6 核
若把单请求 CPU 优化到 5ms:500 × 0.005 / 0.7 = 3.57 → 4 核够用
| 资源 | 计算 | U | 结论 |
|---|---|---|---|
| API CPU | 500 × 8ms / 6 核 | 66.7% | 达标 (≤70%) |
| DB | 500 × 3 × 2ms / 8 核 | 37.5% | 有余量 |
| 连接池 | 500 × 50ms = 25 在途 | — | 池 35–40 |
| 网络 | 500 × 20KB = 10MB/s | 80Mbps | 千兆网 8% |
优化 8ms → 5ms 省下 2 核 (33%);如果只加核不优化,6 核只是把 U 压到 70%,下一次流量涨 20% 又要扩。模型在这里的作用是:先判断「该优化还是该扩容」。
💡 估算误差 2 倍完全可以接受。模型的价值不是精确,而是排除「差 10 倍」的数量级错误:比如算出需要 600 核,那一定是架构问题,不是买机器能解决的。
模型失效的五个原因
| 原因 | 表现 | 修正 |
|---|---|---|
| 非稳态 | 发布、冷启动、促销峰值时模型全错 | 用瞬时模型 / ramp 压测,不用稳态公式 |
| 重尾分布 | P99 由 0.1% 的慢请求决定,指数假设完全失效 | 用实测分布或对数正态/帕累托拟合 |
| 缓存改变 S | 命中率随负载变化,S 不是常数 | 分层建模:命中/未命中两条路径 |
| 资源耦合 | GC、超线程、邻居噪声、CPU 降频 | 留 30% 冗余;用压测校准 |
| 观测偏差 | 只统计「已发出」的请求 (CO) | 用开环定速 + 按计划时间计算延迟 |
反模式
| 反模式 | 后果 |
|---|---|
用 并发 = 线程数 套 Little 法则 | 把阻塞线程也算成处理能力,池子开大 10 倍 |
| 用平均延迟算容量 | 忽略长尾,按平均值配的池在 P99 时排队 |
| 只看 CPU 利用率,不看队列长度 | CPU 70% 但 runqueue 已经单调增长,拐点早到了 |
| 认为「加机器 = 线性扩容」 | USL 峰值后加机器更慢,钱花了容量还降 |
| 对每个资源都按 100% 利用率规划 | 没有冗余,一次尖峰全站超时 |
| 用单机 M/M/1 模型算分布式系统 | 忽略网络、协调、共享存储的额外 V 和 S |
| 把闭环压测的 QPS 当入口容量 | 系统变慢时压测自动减速,容量被高估 2–5 倍 |
| 优化非瓶颈资源 | 按 Amdahl,收益接近 0,还增加复杂度 |
| 忽略服务时间方差 | 平均 10ms 但 Cs=5 时,排队时间是 M/M/1 的 13 倍 |
自检
Q1 一个服务 4 核,500 QPS,每请求 8ms CPU。CPU 利用率是多少?按 70% 红线需要几核?如果 QPS 翻倍到 1000,需要几核?
▸ 答案:U = 500×0.008/4 = 100%,已经饱和 (不是「刚好用满」,是排队已经发散)。按 70%:500×0.008/0.7 = 5.71,需要 6 核。1000 QPS 需要 1000×0.008/0.7 = 11.4,即 12 核,不是 8 核——因为 70% 红线是乘在分母上的,容量与核数是线性关系,但 70% 的余量不能省。另外必须先确认 DB、连接池、网络没有先饱和,否则加 CPU 只是把瓶颈推给下游。
Q2 4 台服务器,每台 80% 利用率,S=10ms。方案 A:每台一个队列;方案 B:一个共享队列。排队时间差多少?共享队列的代价是什么?
▸ 答案:A 是 4 个 M/M/1,每个 Wq=40ms;B 是 M/M/4,Wq≈7.5ms,差 5.4 倍,机器数量和总吞吐完全相同。代价:① 共享队列需要锁/原子操作,高并发下队列本身可能成为瓶颈;② 破坏缓存亲和性和会话粘滞;③ 一个慢消费者可能拖长整个队列的尾延迟。所以只在「共享成本 < 排队收益」时合并,而不是无条件合并。
Q3 监控显示某接口平均延迟 50ms、P99 500ms,QPS 200,连接池 20。按 Little 法则池子够不够?为什么线上仍可能报「连接池等待超时」?
▸ 答案:按平均算 L = 200×0.05 = 10,20 个连接够。但池子够不够取决于「慢请求的比例 × 持续时间」:若只有 1% 请求各占 500ms,平均在途约 2×0.5 = 1 个,单靠长尾压不满 20 个连接;真正危险的是慢请求比例突然放大——比如缓存集中失效或下游抖动让 20% 请求都变慢 (约 40 QPS × 0.5s = 20 个在途),20 个连接瞬间排满,后续请求的排队时间又被计入延迟,P99 进一步恶化。正确做法是按尾部场景 (慢请求比例上升) 估池,并监控连接池等待时间;或者用 bulkhead 隔离长请求。
Q4 某服务 8 核 QPS 8000,16 核 9000,32 核 8500。用 USL 解释,并给出两条可执行的改进方向。
▸ 答案:曲线在 16 核附近见顶、32 核下降,说明 κ (跨核一致性/通信开销) 已经大于并行收益,不是 CPU 不够。两条方向:① 减少共享——把全局锁/全局计数器/共享缓存分片,降低缓存行争用;② 减少跨节点协调——把同步 RPC 改成批量/异步,或按用户分片让请求落在单核/单节点内完成。继续加核只会让 κN(N-1) 项更大,容量继续降。
Q5 为什么说 M/M/1 通常是「悲观估计」,但在 GC 场景下又可能「低估」长尾?
▸ 答案:M/M/1 假设服务时间服从指数分布,Cs=1;定长或低变异服务的 Cs<1,实际排队更短,所以 M/M/1 偏悲观。但 GC 停顿、慢查询会让服务时间出现极端长尾 (Cs 远大于 1),P-K 公式里 (1+Cs²)/2 会放大等待,这时用 Cs=1 的 M/M/1 会低估 P99。结论:平均值模型只能用于容量量级判断,长尾必须用实测分布。