第 3 章 性能工程:从纳秒到毫秒
本章把前两章的硬件事实变成可计算的工具:延迟数量级、排队与扩展定律、观测方法与基准测试纪律。
学习目标
读完本章,你应当能够:
- 只用一张延迟表做心算换算:把"一次跨机房调用"折成内存访问次数、CPU 周期数与 L1 命中次数。
- 推导 Little 定律
L = λ × W,并由 QPS 与延迟估算并发数、由并发数与延迟估算吞吐上限、由三者估算线程池规模。 - 写出 Amdahl 定律与 Gustafson 定律,区分强扩展与弱扩展,用算例证明"串行占比 5% 时加速比上限为 20"。
- 按对象正确选用 USE 方法与 RED 方法,并说出两者的指标口径为什么不能互换。
- 计算扇出 N 时"至少一个下游变慢"的概率
1 − (1 − p)^N,并解释平均值为什么掩盖尾延迟。 - 识别基准测试的五个陷阱,说出每个陷阱伪造出的是哪一类错误结论。
- 复述"先测量再优化"的闭环,说明为什么优化后必须做回归与代价核算。
前置知识检查
| 你需要知道 | 在哪一节补齐 |
|---|---|
存储层次的延迟量级、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 ns | 1× | 1 | 计量单位 |
| 分支预测失败 / L2 缓存命中 | 约 4 ~ 5 ns | 4~5× | 4~5 | 一次误判 ≈ 5 次 L1 |
| L3 缓存命中 / 互斥锁加解锁 | 约 15 ~ 20 ns | 15~20× | 15~20 | 跨核共享与同步的代价 |
| 主存访问 | 约 100 ns | 100× | 100 | 一次缺失的标价 |
| 系统调用 | 约 100 ns ~ 1 μs | 10²~10³× | 10²~10³ | 与内存访问同档到 10 倍 |
| 上下文切换 | 约 1 ~ 5 μs | 10³~5×10³× | 10³~5×10³ | 10³ 次 L1 |
| 同机房网络往返(RTT) | 约 0.1 ~ 0.5 ms | 10⁵× | 10⁵~5×10⁵ | 10⁵ 次 L1 |
| SSD 随机读(4 KB) | 约 100 μs | 10⁵× | 10⁵ | 与同机房 RTT 同档 |
| 机械盘随机读(4 KB) | 约 5 ~ 10 ms | 10⁷× | 10⁷ | 比 RTT 贵两个数量级 |
| 跨大洲网络往返 | 约 150 ms | 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):
| |
用法一:由 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 = λ × W | 1000 QPS × 20 ms = 20 并发 |
| 并发数、服务时间 | 吞吐上限 | λ_max = L / S | 8 线程 / 50 ms = 160 QPS |
| 并发数、吞吐 | 平均停留时间 | W = L / λ | 20 并发 / 1000 QPS = 20 ms |
| QPS、服务时间 | 利用率 | ρ = λ × S | 1000 × 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)。两者的差异不在数学,而在被固定的量不同。
| |
为什么 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) |
|---|---|---|---|
| CPU | mpstat 的 %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:
| |
**尾延迟(tail latency)**指分布最右侧的分位数(通常 P99 及以上)。它之所以必须单独关注,有三个互相独立的理由:
- 它决定超时与重试的地板线。若超时阈值设成平均值,则必然有一大批请求超时;超时触发重试又把流量放大,反过来推高延迟(见第 35 章 35.3 节熔断与退避)。
- 它是用户体验的实际下限。用户的一次页面加载往往由多个请求组成,只要有一个慢,整体就慢——这就是下面的扇出放大。
- 它是 SLO 的直接标的。可用性目标本质上是"在 X ms 内完成的比例达到 Y%"。
尾延迟放大(tail amplification)的数学。 设一个请求需要同时(并发)调用 N 个下游,每个下游"变慢"(超过阈值)的独立概率为 p。整体不变慢要求 N 个下游全都不慢,概率 (1 − p)^N,于是:
| |
表 3-5 扇出倍数 N 与"至少一次慢"的概率(p 为单个依赖的慢概率)
| 扇出 N | p = 0.1% | p = 1% | p = 5% | p = 10% |
|---|---|---|---|---|
| 1 | 0.10% | 1.00% | 5.00% | 10.00% |
| 2 | 0.20% | 1.99% | 9.75% | 19.00% |
| 5 | 0.50% | 4.90% | 22.62% | 40.95% |
| 10 | 1.00% | 9.56% | 40.13% | 65.13% |
| 50 | 4.88% | 39.50% | 92.31% | 99.48% |
| 100 | 9.52% | 63.40% | 99.41% | 99.997% |
| 200 | 18.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 性能优化的正确流程:先测量,再优化,最后回归
不要猜——这句话在面试里的分量比任何优化技巧都重。原因很直白:一个陌生系统的瓶颈分布无法靠直觉判断,而优化错了地方,收益为零且引入风险。流程如下。
| |
表 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、pidstat | sy 高、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 节的容量验证补齐。性能工程的终点不是"更快",而是"在预期负载与故障下仍然可控"。
本章小结
| 关键结论 | 一句话表述 | 常见错误 |
|---|---|---|
| 延迟分三档 | 缓存约纳秒、内存与系统调用约百纳秒、网络与磁盘约十微秒到毫秒;每档差三个数量级 | 只背数字,说不出"跨档访问必须消除"这个优化方向 |
| 换算轴是 ×1000 | ns → μ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 节各指标的口径定义。