本章把前两章的硬件事实变成可计算的工具:延迟数量级、排队与扩展定律、观测方法与基准测试纪律。

学习目标

读完本章,你应当能够:

  1. 只用一张延迟表做心算换算:把"一次跨机房调用"折成内存访问次数、CPU 周期数与 L1 命中次数。
  2. 推导 Little 定律 L = λ × W,并由 QPS 与延迟估算并发数、由并发数与延迟估算吞吐上限、由三者估算线程池规模。
  3. 写出 Amdahl 定律与 Gustafson 定律,区分强扩展与弱扩展,用算例证明"串行占比 5% 时加速比上限为 20"。
  4. 按对象正确选用 USE 方法与 RED 方法,并说出两者的指标口径为什么不能互换。
  5. 计算扇出 N 时"至少一个下游变慢"的概率 1 − (1 − p)^N,并解释平均值为什么掩盖尾延迟。
  6. 识别基准测试的五个陷阱,说出每个陷阱伪造出的是哪一类错误结论。
  7. 复述"先测量再优化"的闭环,说明为什么优化后必须做回归与代价核算。

前置知识检查

你需要知道在哪一节补齐
存储层次的延迟量级、AMAT = T_c + (1 − h) × T_m、缓存行与伪共享见第 1 章 1.1 与 1.5 节;本章 3.1 节只把结论整理成换算表
系统调用与上下文切换的开销量级、CPU 时间片与调度延迟见第 9 章 9.3 节、第 5 章 5.3 与 5.5 节
平均值、中位数、分位数的统计含义本科概率论与数理统计

3.1 延迟数字表:把量级变成心算能力

第 1 章给出了各级存储的量级,本节把它压缩成一张必须背到能心算的表。背这张表的用途不是复述数字,而是在面试现场快速判断一个方案是否根本不可能达标:如果需求是"单请求 P99 小于 20 ms",那么任何在同步路径上做两次跨机房调用的设计都已被排除,不需要再看代码。

表 3-1 可心算的延迟数字表

事件典型耗时相对 L1 命中折算 CPU 周期(1 GHz)心算锚点
L1 缓存命中约 1 ns1×1计量单位
分支预测失败 / L2 缓存命中约 4 ~ 5 ns4~5×4~5一次误判 ≈ 5 次 L1
L3 缓存命中 / 互斥锁加解锁约 15 ~ 20 ns15~20×15~20跨核共享与同步的代价
主存访问约 100 ns100×100一次缺失的标价
系统调用约 100 ns ~ 1 μs10²~10³×10²~10³与内存访问同档到 10 倍
上下文切换约 1 ~ 5 μs10³~5×10³×10³~5×10³10³ 次 L1
同机房网络往返(RTT)约 0.1 ~ 0.5 ms10⁵×10⁵~5×10⁵10⁵ 次 L1
SSD 随机读(4 KB)约 100 μs10⁵×10⁵与同机房 RTT 同档
机械盘随机读(4 KB)约 5 ~ 10 ms10⁷×10⁷比 RTT 贵两个数量级
跨大洲网络往返约 150 ms10⁸×10⁸物理上限,优化不掉

心算的第一条规则:折成"内存访问次数"。 除法在表内完成,乘法只在最后做一次。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
换算练习(全部心算,除完再乘)

  ① 一次跨机房调用(0.5 ms)= 500 μs,而 1 次内存访问 = 0.1 μs
     ⟹ 0.5 ms ≈ 5 × 10³ 次内存访问(5000 次)
  ② 一次 SSD 随机读(100 μs)≈ 1000 次内存访问
     ⟹ "每次请求查一次盘"等价于"每次请求多访问 1000 次内存"
  ③ 一次跨大洲 RTT(150 ms)≈ 1.5 × 10⁶ 次内存访问(150 万次)
  ④ 反过来问:2000 次内存访问是多少?
     每次 0.1 μs ⟹ 2000 × 0.1 μs = 200 μs ⟹ 约等于一次 SSD 随机读
  ⑤ 一次跨机房调用 vs 一次 L1 命中:0.5 ms / 1 ns = 5 × 10⁵ 倍

★ 记住这条"除数轴":ns → μs → ms 每步 1000 倍,跨越两步就是 10⁶ 倍。
  硬件世界里 10⁶ 倍的差距不可能靠调优参数抹平,只能靠换层次(缓存、本地化、异步)。

第二条规则:折成 CPU 周期。 在 1 GHz 下 1 个周期 = 1 ns,于是"ns 数就等于周期数"。3 GHz 下除以 3,量级不变。这条折算能立刻暴露两类错误设计:一是把 μs 级操作放进热循环(1000 周期一次的操作放在每请求循环里);二是把 ms 级操作放进同步路径(150 万周期的一次跨洲往返,同步等它等于让 CPU 空转 150 万周期)。真正的收益来自把等待变成并行:等待可以重叠,计算不能。

重要

面试里最容易丢分的不是记不住数字,而是说不出这些数字的量级差距意味着什么。一句话模板:“内存与网络差 10⁵ 倍量级,所以性能优化的第一杠杆永远是消除跨层访问(少一次 RPC、少一次盘、少一次系统调用),而不是把同一层里的常数因子调小。” 这个判断决定你把时间花在哪里,是性能工程的第一性问题。

提示

记忆口诀:"缓存的单位是纳秒,内存和系统调用的单位是百纳秒,网络和磁盘的单位是百微秒到毫秒。" 三档,每档差三个数量级。面试时先给这句总纲,再按需展开具体数字,比逐条背表更稳。

3.2 吞吐与延迟:Little 定律的推导与用法

**吞吐(throughput)**是单位时间内完成的请求数(QPS/TPS),延迟(latency)是单个请求从进入到完成的时间。两者量纲不同、瓶颈不同、优化手段不同,却被平均值混为一谈,这是性能讨论里最常见的第一误区。

  • 为什么可以不同步:处理是流水线式的。给一条 10 级流水线喂入 10 倍请求,单请求延迟几乎不变(仍是 10 级),吞吐却涨到 10 倍。延迟由最慢的一环加上排队时间决定,吞吐由瓶颈资源的容量除以单位工作量决定。
  • 什么时候会同步恶化:当资源利用率接近饱和,排队时间按 1/(1 − ρ) 发散,延迟急剧上升(这就是 3.5 节尾延迟的排队论来源),而吞吐因为被瓶颈钉住而不再增长。所以"吞吐到顶 + 延迟起飞"是同一次资源饱和的两个侧面。

两者的数量关系就是 Little 定律(Little’s law):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
Little 定律:L = λ × W

  定义:L = 系统内的平均请求数(并发度,包含"正在被处理"与"还在排队")
        λ = 请求到达率(每秒到达数,稳态下等于完成率 = 吞吐)
        W = 单个请求在系统内的平均停留时间(排队时间 + 服务时间)

  推导一(计数法,最直观):
    设观测窗口 T 内共完成 N 个请求,每个请求在系统内停留 Wᵢ,则
      平均并发  L = (1/T) × Σ Wᵢ
      平均到达率 λ = N / T
    ⟹ λ × W = (N/T) × (1/N) × Σ Wᵢ = (1/T) × Σ Wᵢ = L   恒等成立
    ★ 关键点:这个恒等式对"到达过程"和"服务时间分布"没有任何假设,
      不要求泊松到达,不要求指数服务时间,只要求在观测窗口内系统守恒
      (进入数 = 离开数 + 仍在系统内的数)。

  推导二(面积法,等价但更几何):
    把每个请求画成一条从进入到离开的横条(横轴时间、纵轴请求序号),
      Σ Wᵢ = 所有横条的面积 = 系统内请求数随时间的积分 ≈ L × T
    而 N ≈ λ × T,两边同除 T 即得 L = λ × W

用法一:由 QPS 与延迟反推并发数(最常考)。 设 QPS = 1000、平均延迟 W = 20 ms = 0.02 s,则 L = 1000 × 0.02 = 20,即系统里平均有 20 个请求在途。

用法二:由并发数反推吞吐上限。 设线程池固定 8 个工作线程、每个请求服务时间 50 ms,则 λ_max = L / W = 8 / 0.05 = 160 QPS。此时再调大上游超时或加大重试都没有意义——吞吐上限由并发度与单请求时间锁死,只能降 W(优化单请求耗时)或升 L(加并发,前提是不撞到下一个瓶颈,见第 31 章 31.2 节的线程数公式)。

用法三:判定"加线程是否有用"。 用一个可测的量来判断:利用率 ρ = λ × S(S 为纯服务时间,不含排队)。ρ < 1 是稳态条件;若实测 ρ 已接近 1 且队列在增长,此时加线程只会增加上下文切换与缓存抖动(见第 5 章 5.5 节的切换开销构成),吞吐不升、延迟更差。

表 3-2 三个量的相互换算(含排队时间的口径陷阱)

已知求公式算例
QPS、平均延迟并发数L = λ × W1000 QPS × 20 ms = 20 并发
并发数、服务时间吞吐上限λ_max = L / S8 线程 / 50 ms = 160 QPS
并发数、吞吐平均停留时间W = L / λ20 并发 / 1000 QPS = 20 ms
QPS、服务时间利用率ρ = λ × S1000 × 0.05 s = 50(超载:λ 必须小于 1/S)
到达率、服务率平均排队时间(M/M/1)W_q = ρ / (λ × (1 − ρ))ρ = 0.9 时排队时间是服务时间的 9 倍

警告

最常见的错误是把 W 只当成服务时间 S。W = 排队时间 + 服务时间,用 L = λ × S 算并发会系统性偏小,而偏小的线程池又会让排队变长、W 变大——这是一个自我强化的欠配循环。正确顺序是:先由目标 P99 反推可接受的排队预算,再定并发数。 第二个错误方向相反:认为"线程数 = 并发数"。若请求在等待下游响应(阻塞 I/O),线程数确实约等于并发数;若用异步/协程,并发数可以远大于线程数,此时 Little 定律约束的是在途请求数(受连接池、内存与下游容量限制),不是线程数。

3.3 Amdahl 定律与 Gustafson 定律:强扩展与弱扩展

Amdahl 定律(Amdahl’s law)回答:工作量固定时,增加处理器能获得多少加速比?Gustafson 定律(Gustafson’s law)回答:执行时间固定(问题规模随处理器一起放大)时,能获得多少加速比?前者是强扩展(strong scaling),后者是弱扩展(weak scaling)。两者的差异不在数学,而在被固定的量不同。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
Amdahl 定律(固定问题规模)
  设:并行部分占比例 p(按串行执行时间计),串行部分占 s = 1 − p,处理器数 n
  串行执行时间 T₁ = 1(归一化)
  并行执行时间 Tₙ = s + p/n
             1
  加速比 S(n) = ───────────
              s + p/n

  渐近上限:S(∞) = 1/s      ← 令 n → ∞,p/n → 0

  串行占比 s = 5% ⟹ S(∞) = 1/0.05 = 20
    n = 8 :S = 1 / (0.05 + 0.95/8)  = 5.9259  ≈ 5.93  ← 8 核只拿到 5.93×
    n = 64:S = 1 / (0.05 + 0.95/64) = 14.95
    n = ∞ :S = 20                          ← 无论多少核,永远越不过 20
  ★ 8 核的并行效率 = 5.93 / 8 = 74.1%;核数越多效率越低。n = 1.90 / 3.48 分别对应 2 核与 4 核:
    核数少时"看起来还行",上限的伤害要到中高核数才显形。

Gustafson 定律(固定执行时间,规模随 n 放大)
  设:串行部分占比例 s(按"放大后的总工作量"计),处理器数 n
  放大后的工作量 = s + n × (1 − s)      ← n 个处理器各自处理同样大小的一份
  加速比 S(n) = s + n × (1 − s) = n − s × (n − 1)

  s = 5%:
    n = 8:S = 0.05 + 8 × 0.95 = 7.65       ← 接近 8,效率 95.6%
    n = 64:S = 0.05 + 64 × 0.95 = 60.85
  ★ 对比同参数下的 Amdahl:8 核 5.93 对 7.65,64 核 14.95 对 60.85。
    随着 n 增大,Amdahl 趋于常数 20 而 Gustafson 线性增长 —— 这就是两条定律
    对"多核有没有前途"给出相反结论的原因:它们问的根本不是同一个问题。

为什么 5% 的串行部分把上限压到 20? 关键在于 S(∞) = 1/s 这个极限是用"串行比例"定义的,不是用"串行耗时"定义的。串行部分本身没有变慢,它只是在总时间里的占比被无限放大的并行加速顶到了 100%:n → ∞ 时 p/n → 0,总时间 Tₙ → s,于是只剩串行部分在跑。这也解释了敏感性:s = 1% 时上限 100,s = 10% 时上限 10,s = 20% 时上限 5——串行占比每翻一倍,上限就砍一半。

必须能枚举"串行部分"长什么样,否则这条定律只是公式:单线程完成的初始化与配置加载、全局锁保护的临界区(见第 6 章 6.2 节)、内存分配器与内核路径中的全局数据结构、日志落盘与序列化、启动/终止阶段、以及分布式场景下的协调成本(选主、屏障、全局有序 ID、跨分片聚合)。还有一类容易被忽略的"伪并行":任务切分过细时,调度与合并本身成为串行开销。

注意

面试官会追问的下一层:Amdahl 定律为什么经常低估实际加速比? 因为它假设并行部分的比例 p 保持不变,而真实系统里并行化会改变工作集与访问路径:8 个核共享更大容量的 L3,某些算法在并行后总访存量下降(分块后每块的工作集装进私有缓存),于是出现超线性加速(superlinear speedup),S(n) > n。Amdahl 定律也不刻画同步与通信开销随 n 增长的一面,因此实测曲线通常先接近理论值、再因通信开销掉头向下——那条"掉头"的拐点才是真正的可扩展性极限。

重要

区分两者的正确话术:“同一个问题被固定"用 Amdahl(强扩展,问的是"这份活多久能干完”);“每核的工作量被固定"用 Gustafson(弱扩展,问的是"同样的时间能处理多大问题”)。 工程含义:延迟敏感型任务(固定响应时间要求)看 Amdahl,它在警告你"串行路径必须清零";吞吐/批处理型任务(数据持续增长)看 Gustafson,它在说服你"加机器确实线性有效"。把这两句话用在场景设计题的容量估算里,是明确的加分项(见第 36 章 36.2 节)。

3.4 USE 方法与 RED 方法:观测对象决定指标口径

性能排查最大的浪费不是工具不够,而是不知道该看哪些指标,于是把 CPU 使用率从头看到尾。两个方法解决"看什么"的问题,但它们的适用对象根本不同:USE 面向资源,RED 面向服务。

表 3-3 USE 方法与 RED 方法的适用对象差异

维度USE 方法RED 方法
提出者与适用对象Brendan Gregg;每一种资源(CPU、内存、磁盘、网络、连接池、锁)Tom Wilkie;每一个对外服务/端点(HTTP 接口、RPC 方法、队列消费者)
三个字母Utilization(利用率)、Saturation(饱和度)、Errors(错误)Rate(速率)、Errors(错误)、Duration(耗时)
回答的问题“这个资源是不是瓶颈?”“这个服务是不是在变差?用户感受到什么?”
使用时机定位瓶颈阶段(已知慢,找在哪)建立监控与告警阶段(未知何时慢,要提前发现)
天然优势系统性覆盖,避免"只看 CPU"的盲点(如磁盘饱和、连接池耗尽)以用户体验为口径,天然适合 SLO 与告警(见 3.5 节分位数)
典型误用对"服务"问利用率(服务没有利用率,机器才有)对"资源"问速率(磁盘的"速率"是 IOPS 还是 MB/s?)

表 3-4 四类核心资源的 USE 指标映射

资源利用率(Utilization)饱和度(Saturation)错误(Errors)
CPUmpstat 的 %usr + %sys;注意 %iowait 不是 CPU 忙(见第 11 章 11.2 节)运行队列长度 vmstat 的 r、/proc/loadavg;排队时间用 perf sched 看机器检查异常、节流(cgroup cpu.stat 的 nr_throttled)
内存已用物理内存比例;更该看 MemAvailable 而非 MemFree(见第 12 章 12.1 节)换页 vmstat 的 si/so、直接回收导致的分配延迟尖刺、OOM 计数分配失败、OOM killer 触发次数、cgroup memory.events
磁盘iostat 的 %util(多并发 I/O 下不可靠,见第 11 章 11.3 节)平均队列长度 aqu-sz、await 相对 svctm 的放大倍数设备错误、文件系统只读重挂载、dmesg 中的 I/O error
网络带宽占用相对链路容量ss -lnt 的 Send-Q/Recv-Q、TCP 重传率、连接池等待队列长度重传、RST、校验和错误、SYN 丢弃(见第 16 章 16.3 节)

RED 的指标口径:Rate 是每秒请求数(要区分成功与失败的口径),Errors 是失败请求数与失败率(要区分"客户端错误"与"服务端错误",两者的告警含义不同),Duration 是响应时间分布——不是平均值,而是 P50/P90/P99/P999 分位数(原因见 3.5 节)。

重要

两个方法不是二选一,而是从外到内的两段:先用 RED 从服务侧发现"哪个端点变差了、是否达到 SLO",再用 USE 从资源侧定位"是哪个资源饱和了"。 顺序不能反。若一上来就看资源,你会得到"CPU 不高、内存不高、磁盘不高"这种结论,因为它回答不了"用户到底有没有受影响";而只做 RED 则永远停在"知道慢但不知道慢在哪"。

警告

常见错误回答:“USE 是看 CPU/内存/磁盘,RED 是看 QPS/错误率/响应时间。” 这只是把指标名念了一遍,没有回答"为什么这样分"。正确答案要落到对象上:利用率是资源的属性(一台机器 8 核,只有一个 CPU 利用率),速率是服务的属性(一台机器上有 5 个服务,各有自己的 QPS)。把服务的 QPS 说成"服务的利用率"、或把 CPU 的 %usr 说成"CPU 的速率",都是口径错位。

3.5 P99/P999 与尾延迟:平均值如何掩盖问题

为什么平均值会掩盖问题,用一个具体分布算一遍就清楚了。设 100 个请求中有 99 个耗时 10 ms、1 个耗时 1 s:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
九个数字看清"平均值骗局"(单位 ms)

  算术平均 = (99 × 10 + 1 × 1000) / 100 = 1990 / 100 = 19.9 ms
  中位数 P50 = 10 ms
  P90 = 10 ms       ← 90% 的请求都比它快
  P99 = 1000 ms     ← 恰在分界点上,取第 99 个排序值
  P999(需要样本量 ≥ 1000 才有意义)在 100 个样本里无法估计

  ★ 平均值 19.9 ms 看起来"还好",但最慢的 1% 用户经历了 100 倍于中位数的延迟。
  ★ 分位数才是用户体验的坐标:
      P50 决定"大部分人的体感",P99/P999 决定"有多少人抱怨、有多少请求超时重试"。
  ★ 另一个陷阱:平均值本身会被少数极值拉动(19.9 是中位数 10 的两倍),
    因此它既不代表典型用户,也不代表最差用户 —— 它不代表任何人。

**尾延迟(tail latency)**指分布最右侧的分位数(通常 P99 及以上)。它之所以必须单独关注,有三个互相独立的理由:

  1. 它决定超时与重试的地板线。若超时阈值设成平均值,则必然有一大批请求超时;超时触发重试又把流量放大,反过来推高延迟(见第 35 章 35.3 节熔断与退避)。
  2. 它是用户体验的实际下限。用户的一次页面加载往往由多个请求组成,只要有一个慢,整体就慢——这就是下面的扇出放大。
  3. 它是 SLO 的直接标的。可用性目标本质上是"在 X ms 内完成的比例达到 Y%"。

尾延迟放大(tail amplification)的数学。 设一个请求需要同时(并发)调用 N 个下游,每个下游"变慢"(超过阈值)的独立概率为 p。整体不变慢要求 N 个下游全都不慢,概率 (1 − p)^N,于是:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
至少一个下游变慢的概率 = 1 − (1 − p)^N

  推导:N 个下游各自独立,"全部正常"的概率 = (1−p) × (1−p) × … × (1−p) = (1−p)^N
        由补事件得"至少一个异常"的概率,这是德摩根律的直接应用

  算例 A:p = 1%(单个下游的 P99 就是慢阈值)
    N = 10 时 1 − 0.99¹⁰ = 9.56%  ← 单依赖的"P99"被放大成整体的"P90 附近"
    N = 100 时 1 − 0.99¹⁰⁰ = 63.40%;N = 200 时 1 − 0.99²⁰⁰ = 86.60%
  算例 B:p = 1‰(更严,单依赖的 P999 才是慢阈值)
    N = 10 时 1 − 0.999¹⁰ ≈ 1.00%;N = 100 时 1 − 0.999¹⁰⁰ ≈ 9.52%
    ← 要求整体 P90 达标,单依赖就必须做到 P999 以上

  ★ 关键推论:若希望整体在 p_target 以内,单个下游的慢概率必须低于
      p ≤ 1 − (1 − p_target)^(1/N) ≈ p_target / N     (p_target 很小时)
    即"扇出 N 倍,对每个依赖的尾延迟要求就严格约 N 倍"。

表 3-5 扇出倍数 N 与"至少一次慢"的概率(p 为单个依赖的慢概率)

扇出 Np = 0.1%p = 1%p = 5%p = 10%
10.10%1.00%5.00%10.00%
20.20%1.99%9.75%19.00%
50.50%4.90%22.62%40.95%
101.00%9.56%40.13%65.13%
504.88%39.50%92.31%99.48%
1009.52%63.40%99.41%99.997%
20018.14%86.60%99.996%≈ 100%

四条工程对策(每条都对应公式里的一项):压 N——减少同步扇出,把可选的依赖改成异步或预取/批量;压 p——对最慢的下游做专项优化,并把慢请求快速失败而不是无限等待;去独立性假设——1 − (1 − p)^N 假设各下游独立,而真实系统里的慢往往由共享资源(同一个机架、同一个连接池、同一次 GC 停顿)引发,此时概率是正相关的,实际尾延迟会比公式预测的更差,这是面试官最爱追问的一层;兜底——用对冲请求(hedged request,向第二个副本再发一份、取先返回者)与超时预算(timeout budget,把整条链路的剩余预算逐跳向下传)把尾延迟的上界钳住。

小心

把"P99 很高"答成"平均延迟还好,可能是偶发抖动"会直接把题目答崩。尾延迟不是抖动,而是分布的固有形状:它来自排队、GC 停顿(见第 32 章 32.5 节)、缓存未命中、磁盘排队、下游依赖的长尾、以及共享资源争用。抖动是"不可解释"的说法,长尾是"可归因"的现象。 面试官一定会追问:“你的 P99 是从哪里来的?“准备一个能落到具体机制的归因链,而不是"偶发”。

提示

分位数的采样纪律:P999 需要至少上万个样本才稳定,P9999 需要十万级以上;用 1 分钟窗口统计 QPS 只有 100 的系统,P99 的样本数只有 6000,估计误差已经很大。另一个陷阱是跨实例聚合分位数:把 10 个实例的 P99 再取平均,得到的既不是全局 P99 也不是任何实例的 P99,必须用直方图/摘要(如指数桶)合并(见第 11 章 11.5 节)。

3.6 基准测试的五个陷阱

基准测试(benchmark)的目标是得到一个能支持决策的结论(这个改动快了 30%),而五个陷阱各自伪造出一类错误结论。表 3-6 是必背清单。

表 3-6 基准测试的五个陷阱与伪造出的错误结论

陷阱机制伪造出的错误结论对策
未预热(warm-up)JIT 尚未编译热点方法、页缓存与文件缓存为空、连接池与线程池为空、CPU 频率未升到稳态(turbo/调频)、分支预测器未训练“优化后反而更慢"或"这个框架启动这么慢不能用”丢弃预热阶段的样本;JIT 用 -XX:+PrintCompilation 确认稳定;跑够稳态时长并报告预热时长
坐标系选择掩盖差异线性轴下 1% 的差异看不出,对数轴下 0.1% 的差异被放大成"巨大改进”;只报单点值不报分布“优化没有效果”(实际有效)或"优化效果显著"(实际是噪声)同时给绝对差值与相对差值;纵轴从 0 起画;报告分位数而非单点;给出置信区间/重复次数
只报均值均值被少数极值拉动或被多数正常值稀释,无法反映分布形状(见 3.5 节)“延迟没变”(实际 P99 恶化一倍,均值因正常请求变快而持平)报 P50/P90/P99/P999 加最大值;报吞吐与延迟两条曲线
压测端先成为瓶颈压测机的 CPU/网卡/端口耗尽(TIME_WAIT 堆积)、单机文件描述符上限、客户端 GC、序列化开销先于服务端饱和“服务端 5000 QPS 就到顶了”(实际瓶颈在压测机)监控压测机自身的 CPU/网卡/软中断;用多台压测机或分布式压测;对照 ss -s 看 TIME_WAIT 增长
测试环境与生产不一致数据量、数据分布(热 key 比例)、机型与核数、JVM 堆参数与 GC 器、内核参数、网络拓扑与 RTT、是否有其他租户争用“容量够用”(生产因数据量放大 10 倍而崩)或"这个优化没用"(生产瓶颈在别处)记录并冻结配置清单(版本、参数、数据规模);至少做一轮"生产同构"验证;用生产流量回放(traffic replay)而不是合成流量

两个补充的隐藏陷阱值得主动提:一是缺少对照组——一次只改一个变量,并保留改动前的基线(同一次运行窗口内 A/B 交叉执行,避免"上午测基线、下午测优化"被环境漂移污染);二是把微基准当结论——单函数微基准(microbenchmark)受死代码消除、常量折叠、循环展开影响极大,JMH 这类框架用黑洞(blackhole)与多轮 fork 对抗,但微基准收益绝不能外推为端到端收益(某函数优化 50%、它只占总耗时 2%,端到端上限即 1%,见 3.3 节 Amdahl)。

警告

最常见的一句错误回答是"我本地压了一下,提升了 3 倍"。面试官会依次追问:预热的样本丢了吗?压测机自己饱和了吗?你报的是均值还是 P99?测试数据和生产是同一个量级吗? 四个问题里只要有一个答不上来,前面所有数字都会被判为无效。 正确姿势是先声明纪律,再给数字:“固定配置、预热 60 秒后取 5 分钟稳态样本,压测机 CPU 与网卡均在 40% 以下(用 mpstat -P ALL、ss -s 自查),数据规模与生产同量级,报 P50/P99 与吞吐。”

3.7 性能优化的正确流程:先测量,再优化,最后回归

不要猜——这句话在面试里的分量比任何优化技巧都重。原因很直白:一个陌生系统的瓶颈分布无法靠直觉判断,而优化错了地方,收益为零且引入风险。流程如下。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
性能优化闭环(每一步都有产物,缺一环结论就不可信)

  ① 定义指标与目标            产物:SLI/SLO(口径、分位数、窗口、流量口径)
        │                     反例:"让服务更快" —— 不可验证
        ▼
  ② 建立可重复的基线           产物:固定配置的基线报告(含预热、样本量、分布)
        │                     ★ 没有基线,后面的"提升"都是故事
        ▼
  ③ 定位瓶颈在哪一层           产物:证据(火焰图/采样/队列长度),不是猜测
        │                     手段:RED 找端点 → USE 找资源 → 剖析找代码行
        ▼
  ④ 提出假设并预测收益         产物:一句话假设 + 预期幅度
        │                     例:"序列化占 30% 耗时,换零拷贝应降 20%"
        ▼
  ⑤ 单变量修改并复测           产物:A/B 交叉执行的对照结果
        │                     ★ 一次只改一个变量,否则无法归因
        ▼
  ⑥ 回归验证 + 代价核算        产物:全局无退化的证据 + 代价清单
        │                     查:吞吐、P99、错误率、资源占用、成本
        └──────────▶ 回到 ①(新基线),继续下一轮

  关键判据:③ 的输出必须是"机制 + 证据",而不是"我猜是 GC"。

表 3-7 一次请求的分层归因:证据落在哪一层,修法就完全不同

层要看的量手段典型根因
客户端排队时间 vs 传输时间加时间戳分段本地线程池排满、DNS、代理
网络握手与首字节时间tcpdump、curl -w丢包重传、拥塞窗口小、RTT 大
应用(本机)CPU 时间 vs 墙钟时间perf、火焰图锁竞争、序列化、正则、多余拷贝
应用(等待)off-CPU 时间(究竟在等谁)off-CPU 火焰图下游 RPC、DB、磁盘、锁
下游依赖下游自身 P99链路追踪(TraceID 传播)下游尾延迟被本层扇出放大(见 3.5 节)
系统资源USE 三要素vmstat、iostat、ss磁盘饱和、内存回收、fd 耗尽
内核/运行环境软中断、上下文切换、cgroup 节流mpstat、pidstatsy 高、CPU 被节流、NUMA 远程访问

顺序原则:先判断时间是花在 CPU 上还是等待上。CPU 高就看应用剖析;CPU 不高却慢就必然在等某样东西,用 off-CPU 火焰图或链路追踪找出等待对象,再回到上表定位层。

优化手段只该有四类(想不出归类说明还没定位到瓶颈):减少工作量(消除冗余计算与重复 RPC、降数据量)、摊销(批量化、合并小 I/O、连接复用)、并行/异步(把等待重叠,见 3.1 节)、换层次(把数据搬到更快的层次)。

回归验证的三个必查项:一是全局指标不能退化——针对 P99 的优化经常抬高 P50(例如为压低长尾而增加批处理,普通请求多等一个批次窗口),或者压低延迟而抬高 CPU(缓存换 CPU);二是极端场景不能崩——缓存失效、下游全挂、冷启动、数据量放大 10 倍;三是代价与成本核算——多用的内存、连接数、机器成本是否值得这点延迟收益(性能优化从来是取舍)。

重要

面试里最有分量的回答结构是:“我先不猜,先测。” 具体展开为:定义 SLI(例如接口 P99)→ 取基线 → 用 RED 确认哪个端点变差、用 USE 确认哪个资源饱和 → 用剖析(profiling)拿到"哪一行代码、什么机制" → 提出可证伪的假设与预期收益 → 单变量验证 → 回归。能完整说出这条链,比多报几个优化技巧更能证明工程成熟度。

注意

面试官会追问的下一层:“如果指标已经达标,还要不要继续优化?” 要区分"够用"与"脆弱":达标但余量不足(利用率长期高于 70%、P99 贴着 SLO 线)意味着没有故障吸收能力——一次机房抖动或流量尖峰就会击穿。此时正确的动作不是压榨延迟,而是提高余量与降级能力(限流、熔断、排队上限、隔离),也就是把 3.5 节的尾延迟风险与 3.6 节的容量验证补齐。性能工程的终点不是"更快",而是"在预期负载与故障下仍然可控"。

本章小结

关键结论一句话表述常见错误
延迟分三档缓存约纳秒、内存与系统调用约百纳秒、网络与磁盘约十微秒到毫秒;每档差三个数量级只背数字,说不出"跨档访问必须消除"这个优化方向
换算轴是 ×1000ns → μs → ms 每步 1000 倍;0.5 ms 的同机房调用 ≈ 5000 次内存访问把 RPC 与内存访问当同量级问题来优化常数因子
Little 定律无分布假设L = λ × W 由计数法恒等推出,不要求泊松到达或指数服务时间以为它只在 M/M/1 队列下成立
W 含排队时间用 L = λ × S(只算服务时间)会系统性低估并发,形成欠配循环把服务时间当停留时间
Amdahl 管强扩展S(n) = 1 / (s + p/n),上限 S(∞) = 1/s;s = 5% 时上限 20,8 核只得 5.93答"加核就能线性加速",或说不出 20 是占比决定的而非耗决定的
Gustafson 管弱扩展S(n) = n − s × (n − 1),规模随核数放大,64 核可达 60.85把两条定律当互相矛盾的结论,而非不同前提的答案
USE 面向资源利用率是资源的属性(利用率/饱和度/错误)对"服务"问利用率
RED 面向服务速率/错误/耗时是服务的属性,耗时必须用分位数对"资源"问速率,或用平均耗时代表服务表现
均值掩盖长尾99 × 10 ms + 1 × 1000 ms 的均值 19.9 ms,掩盖了 P99 = 1 s用平均值验收延迟目标
尾延迟按扇出放大整体慢概率 1 − (1 − p)^N;p = 1%、N = 100 时已达 63.4%认为"每个依赖都达标"就等于"整体达标"
独立假设是乐观的共享资源导致的慢是正相关的,真实尾延迟比公式更差直接套公式给出比实际更好的预测
基准测试五陷阱未预热、坐标系、只报均值、压测端先饱和、环境不一致用本地压测的均值提升宣布优化成功
优化先测量先定位在哪一层(CPU 还是等待),再单变量改,最后回归凭直觉改代码,跳过基线与回归

自测题

Q1(计算)需求:接口 P99 必须小于 10 ms。某方案在请求同步路径上包含一次同机房 RPC(0.3 ms)和一次 SSD 随机读(100 μs),另有 2 ms 的本地计算。仅按量级判断这个方案是否可行,并给出至少两条可行的改造方向。

答案与解析

粗算:0.3 ms(RPC)+ 0.1 ms(SSD)+ 2 ms(计算)≈ 2.4 ms 的服务时间,但 P99 不是服务时间——它等于服务时间加上 99 分位的排队时间。换算成内存访问更直观:0.3 ms ≈ 3000 次内存访问,100 μs ≈ 1000 次,2 ms ≈ 20000 次——本地计算已经是最大项,先优化它才是第一优先级(这正是 Amdahl 的用法:占比最大的部分收益最高)。

所以在"资源有余量"的前提下,2.4 ms 的服务时间留给 10 ms 的 P99 有约 7.6 ms 的排队预算,可行;但若该系统并发高、线程池欠配(用 L = λ × S 低估并发),排队时间会迅速吃掉预算,P99 先破 10 ms。核算顺序应是:先用 λ × W 估出在途请求数,再检查线程池/连接池是否够,再看队列是否在增长。

改造方向:① 消掉同步 RPC——改成异步并发(把 0.3 ms 的等待与其他工作重叠,见 3.1 节"等待可重叠、计算不可重叠");② 消掉 SSD 随机读——加缓存或改成本地内存索引(等价于每次请求省掉 1000 次内存访问);③ 削本地计算——预计算、批量化或换算法。

错在哪:把"服务时间 = P99"是最典型的错误,它让所有容量估算都偏乐观;另一个错误是看到 SSD 就要去掉它——2 ms 的计算才是大头,先动它收益最高。面试官会怎么追问:①"10 ms 的 P99 预算里,你会给下游 RPC 分多少?"——答"按超时预算逐跳分配,且单跳超时必须小于整条链路的残余预算,而不是每跳都设 10 ms"。②"如果 P99 是 9.5 ms 达标但 P999 是 200 ms,你怎么办?"——查是否存在共享资源导致的偶发停顿(GC、日志落盘、锁),并看扇出倍数是否放大了它。

Q2(计算)服务实测 2000 QPS、平均响应时间 25 ms,线程池上限 64。请用 Little 定律判断线程池是否够用;若平均响应时间升到 40 ms 而 QPS 不变,结论如何变化?

答案与解析

L = λ × W = 2000 × 0.025 s = 50。50 < 64,有余量,理论上够用。响应时间升到 40 ms 后 L = 2000 × 0.04 = 80 > 64——并发需求超过线程池上限,池子被打满,新请求只能排队,排队又把 W 继续推高,L = λ × W 中的 W 进一步变大,形成正反馈直到请求超时或被拒绝。

这个连锁反应值得画出来:W ↑ ⟹ L ↑ ⟹ 线程不够 ⟹ 排队 ⟹ W ↑。它解释了为什么"服务变慢"常常不是线性恶化而是突然崩塌——越过并发上限后系统进入超载区,吞吐不再随负载增长(Little 定律的稳态前提被破坏)。

错在哪:用 L = λ × S(只算服务时间,比如只填 20 ms)算得 40,得出"64 绰绰有余"的结论——W 必须包含排队时间,而排队时间恰恰是池子不够时增长最快的部分。另一个错误是认为"线程数超过并发数就是浪费"——还需要留出突发与故障时的余量(否则没有吸收能力,见 3.7 节 [!NOTE])。

面试官会怎么追问:①"那线程池设成 128 行不行?"——若请求是 CPU 密集型,加线程只会增加上下文切换与缓存抖动(见第 5 章 5.5 节);若瓶颈在数据库连接池,加线程只是把排队从线程池挪到连接池。②"怎么定这个 64?"——由目标 P99 反推可接受排队时间,再除以单请求服务时间,并留出故障余量,而不是拍一个数字。

Q3(推导)某服务串行部分占 8%。分别用 Amdahl 定律与 Gustafson 定律求 16 核时的加速比,并解释为什么两个答案差异这么大,以及在面试中应该用哪一个来论证"扩容是否有效"。

答案与解析

Amdahl(固定问题规模):S(16) = 1 / (0.08 + 0.92/16) = 1 / 0.1375 = 7.27,上限 S(∞) = 1/0.08 = 12.5。 Gustafson(固定时间、规模随核数放大):S(16) = 16 − 0.08 × 15 = 16 − 1.2 = 14.8。

差异的根源不在数学而在前提:Amdahl 假设问题大小不变,于是 16 核里有 15 份算力在抢那 8% 的串行段,串行段成为瓶颈;Gustafson 假设每核的工作量不变、问题随之放大 16 倍,于是"串行部分"在总量中的占比被稀释为 8%,加速比接近 16。Amdahl 回答"这份活干多快",Gustafson 回答"同样时间能干多大的活"。

用法:论证扩容有效性要看负载类型。在线延迟敏感服务(请求量由外部流量决定、单请求工作量固定)应当用 Amdahl——它在提醒你"先清掉串行段,否则 16 核只能拿 7.27 倍,浪费一半算力";批处理/离线/数据持续增长型任务应当用 Gustafson——它说明"加机器确实近似线性有效"。

错在哪:把两条定律当成互相矛盾的结论(“一个说多核没用,一个说多核有用”),而不去检查前提;或算 Amdahl 时把 0.92/16 写成 0.92 × 16。面试官会怎么追问:①"8% 的串行部分具体可能是哪些代码?"——必须能枚举:全局锁、初始化、日志落盘、内存分配器/内核路径的全局结构、跨分片聚合、任务切分与合并。②"实测加速比超过 16 怎么办?"——可能来自超线性效应(并行后工作集装进私有缓存反而减少总访存量)或测量偏差(未预热、基线受压测端限制),要能给出两种解释而不是直接宣称定律错了。

Q4(辨析)“我们给这个 HTTP 接口做了 USE 分析:利用率 30%、饱和度低、错误率 0,所以接口没问题。“这句话错在哪?修正后应该怎么观测?

答案与解析

错在对象错位。USE 方法面向资源,三个指标都是资源的属性:利用率是"某资源忙的时间比例”,而一个 HTTP 接口没有利用率——它可能跨越多个进程、多个实例、多个下游与多台机器。对着一台 8 核机器说"利用率 30%“可以,对着一台机器上的 5 个接口说"接口利用率 30%“没有定义。而"错误率 0"确实是指标的属性,但它属于 RED 的 Errors 列。

正确做法是先用 RED 看服务,再用 USE 看资源:RED 侧取该接口的 Rate(每秒请求数,区分成功/失败口径)、Errors(区分 4xx 与 5xx:4xx 增长常是客户端或契约问题,不是服务端容量问题)、Duration(必须给 P50/P90/P99/P999,不是平均值,见 3.5 节)。确认是哪个端点、在哪段时间变差之后,再用 USE 去查它依赖的资源:CPU(%usr + %sys 与运行队列 r)、内存(MemAvailable 与 si/so)、磁盘(aqu-sz/await,%util 在多并发下不可靠,见第 11 章 11.3 节)、网络(重传率、连接池与 Send-Q 队列)。

错在哪:把"资源指标正常"当成"服务没问题”——很多性能问题源自排队与协调(锁等待、连接池等待、下游长尾),这些不体现在任何单一资源的利用率上;反过来,资源利用率正常也可能因为该资源不是瓶颈而是另一个资源在排队。面试官会怎么追问:①"如果 CPU、内存、磁盘都正常,接口还是慢,你下一步查什么?"——查等待:off-CPU 火焰图、链路追踪看下游 P99、锁竞争、连接池等待队列、以及扇出放大。②”%util 为什么不可靠?"——它是"设备有 I/O 在飞的时间比例”,RAID 与高并发下多个 I/O 可同时在飞,%util 接近 100% 并不等价于设备饱和,要看队列长度与 await(见第 11 章 11.3 节)。

Q5(计算)一个聚合接口需要并发调用 20 个下游服务。每个下游单次调用"超过 200 ms"的概率是 0.5%,且各下游相互独立。求该接口"至少一次超 200 ms"的概率。若要把这个概率压到 1% 以内,每个下游的慢概率需要降到多少?

答案与解析

P = 1 − (1 − 0.005)^20 = 1 − 0.995^20。0.995^20 ≈ 0.9046,故 P ≈ 9.54%。也就是说,每个下游都有 99.5% 的把握(相当于单依赖的 P995 达标),整体却只有约 90.5% 的请求能全部正常。

要把整体压到 1%:解 1 − (1 − p)^20 ≤ 0.01,即 (1 − p)^20 ≥ 0.99,1 − p ≥ 0.99^(1/20)。ln 0.99 = −0.01005,除以 20 得 −0.0005025,取指数得 0.99950,所以 p ≤ 0.0005,即每个下游的慢概率必须低于 0.05%,比原要求严格约 10 倍。

这正是"扇出放大"的工程含义:扇出 N 倍,对每个依赖的尾延迟要求就严格约 N 倍(小概率下的近似式 p ≤ p_target / N)。20 个下游要整体 P99 达标,等于要求每个下游做到 P9995 以上的水平。

错在哪:① 直接把 0.5% × 20 = 10% 当答案——这个近似在小概率时接近正确(9.54% 对 10%),但当成精确值就会在 p 变大时错得离谱:p = 10%、N = 20 时近似式给 200%(显然荒谬),精确值是 87.8%;② 忽略独立性假设不成立:真实系统里 20 个下游常共享同一批机器、同一张网卡、同一个连接池与同一段 GC 时间,慢事件是正相关的,实际概率会高于公式给出的 9.54%。所以公式给的是乐观下界,工程上必须按更悲观的情况留余量。

面试官会怎么追问:①"怎么降低这个概率,给出可落地的四类手段?"——压 N(减少同步扇出,改成异步/批量/预取)、压 p(专项优化最慢下游 + 快速失败)、去相关(把下游打散到不同机架/可用区,降低共享资源导致的相关性)、兜底(对冲请求、逐跳超时预算、整体降级返回部分结果)。②"为什么相关性会让尾延迟更差,能不能量化?"——可以:若 N 个下游共享一个以概率 q 出故障的公共资源,则整体慢概率至少是 q(相关系数为 1 时退化为 q + (1−q) × [1 − (1−p)^N]),当 q 远大于 1 − (1−p)^N 时,共享资源成为尾延迟的主导项,此时优化单个下游完全无效,必须做隔离。

面试高频问题与陷阱

面试问法考查角度陷阱 / 加分点对应小节
一次跨机房调用大概相当于多少次内存访问?这些量级对性能优化意味着什么?数量级是否形成直觉陷阱:只背"网络慢"说不出倍数;或把 0.5 ms 与 100 ns 说成"差不多一个数量级"。加分:给出 0.5 ms ≈ 5000 次内存访问(10⁵ 倍),并由此推出"第一杠杆是消除跨层访问"3.1
你怎么估算一个服务需要多少并发?请现场算一下。Little 定律的实际使用陷阱:答"看机器配置"或拍一个数字;用 L = λ × S 只算服务时间而漏掉排队时间。加分:写出 L = λ × W,说清 W 含排队,并检查利用率 ρ = λ × S 是否留出故障余量3.2
吞吐和延迟是一回事吗?什么情况下加机器也提不高吞吐?两个维度与瓶颈的区分陷阱:答"吞吐高延迟就低",或以为加机器必然线性提升。加分:说明延迟由最慢一环加排队决定、吞吐由瓶颈容量决定,并指出串行段(Amdahl)与下游容量才是上限3.2、3.3
如果程序里有 5% 的代码不能并行,8 核能加速多少倍?为什么不能再高了?Amdahl 定律的推导与极限陷阱:答"快 8 倍左右"或"能到 20 倍"(把上限当实际值);把 20 说成来自串行耗时而非串行占比。加分:算出 8 核 5.93 倍、8 核效率 74%,并解释上限 1/s 的来源3.3
Amdahl 定律和 Gustafson 定律矛盾吗?你在什么场景下用哪一个?强扩展 vs 弱扩展的前提差异陷阱:认为两者冲突、或只记得公式说不出前提差异。加分:点明"固定问题规模"与"固定执行时间"的区别,并对应到延迟敏感服务与批处理/离线任务3.3
一台机器上 CPU 不高、内存不高、磁盘也不高,但接口就是慢,你怎么查?观测方法论与盲点陷阱:继续在资源利用率里找答案(排队与协调问题不体现在利用率上);只会说"看日志"。加分:先用 RED 确认端点与分位数,再用 USE 查资源,再用 off-CPU/链路追踪查"在等谁",并提到扇出放大与锁/连接池排队3.4、3.7
平均响应时间 20 ms,这个服务算快吗?平均值与分位数陷阱:答"很快"就结束,不追问分布。加分:举 99 × 10 ms + 1 × 1000 ms 的例子说明均值 19.9 ms 而 P99 = 1 s,并给出应该报哪些分位数、样本量要多大才稳定3.5
一个接口调了 100 个下游,每个下游 P99 是 10 ms,这个接口的 P99 大概是多少?尾延迟放大的计算与假设陷阱:答"还是 10 ms"或"想当然地取最大值"。加分:算出 1 − 0.99¹⁰⁰ ≈ 63.4%,并主动指出独立性假设不成立时实际更差,给出压 N/压 p/去相关/兜底四类对策3.5
你做了个优化说快了 3 倍,怎么证明这个结论可信?基准测试纪律陷阱:答"我压测过了"却答不出预热、压测端饱和度、分位数、环境一致性。加分:主动声明固定配置、预热时长、样本量与窗口、压测机余量、A/B 交叉执行,并用 Amdahl 检查收益幅度是否与热点占比自洽3.6
线上接口 P99 突然变高,你的排查步骤是什么?为什么不能直接改代码试?先测量再优化的流程陷阱:一上来就说"加缓存"或"调大线程池"(凭猜测优化,改了也不知道是否有用)。加分:给出"定义指标 → 取基线 → RED 定端点 → USE 定资源 → 剖析定代码 → 假设可证伪 → 单变量验证 → 回归与代价核算"的闭环3.7
优化之后除了看目标指标,还要回归什么?权衡与副作用意识陷阱:只看被优化的那个指标,忽略副作用。加分:查全局无退化(P99 优化常抬高 P50)、极端场景(缓存失效、下游全挂、冷启动、数据量放大 10 倍)、代价核算(内存/连接/成本)3.7

延伸阅读

  • John L. Hennessy & David A. Patterson, Computer Architecture: A Quantitative Approach(第 6 版第 1 章):Amdahl 定律与"性能的度量与报告"一节,是 3.3 节的规范表述来源;同书关于强扩展与弱扩展的讨论。
  • Gene M. Amdahl, “Validity of the single processor approach to achieving large scale computing capabilities”(1967,AFIPS);John L. Gustafson, “Reevaluating Amdahl’s Law”(1988,CACM)——两条定律的原始论文,对照读能看清"被固定的量不同"这一核心差异。
  • Raj Jain, The Art of Computer Systems Performance Analysis:Little 定律的严格推导(守恒律视角)、排队论基础与基准测试设计章节(预热、重复次数、置信区间)。
  • Brendan Gregg, Systems Performance: Enterprise and the Cloud(第 2 版):第 2 章"方法"(USE 方法的完整定义)、第 3 章"操作系统"、第 12 章"基准测试"(五个陷阱的工程化清单与反例)。
  • Tom Wilkie 关于 RED 方法的公开文章(Weaveworks 工程博客),以及 Google SRE Book 第 4 章"Service Level Objectives"(SLI/SLO 口径、分位数与错误预算的官方表述)。
  • Dean & Barroso, “The Tail at Scale”(Communications of the ACM, 2013):尾延迟放大的完整论述、相关性来源,以及对冲请求(hedged request)、微分区(micro-partition)、选择性复制等对策的原始出处。
  • Linux man-pages:mpstat(1)、vmstat(8)、iostat(1)、ss(8)、perf-stat(1)——3.4 与 3.6 节各指标的口径定义。