# 第 3 章 性能工程：从纳秒到毫秒


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

## 学习目标

读完本章，你应当能够：

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 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⁸ | 物理上限，优化不掉 |

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

```text
换算练习（全部心算，除完再乘）

  ① 一次跨机房调用（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 万周期）。真正的收益来自**把等待变成并行**：等待可以重叠，计算不能。

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

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

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

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

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

两者的数量关系就是 Little 定律（Little's law）：

```text
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 = λ × 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 倍 |

> [!WARNING]
> 最常见的错误是**把 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）**。两者的差异不在数学，而在**被固定的量不同**。

```text
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、跨分片聚合）。还有一类容易被忽略的"伪并行"：任务切分过细时，调度与合并本身成为串行开销。

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

> [!IMPORTANT]
> 区分两者的正确话术：**"同一个问题被固定"用 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 节）。

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

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

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

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

```text
九个数字看清"平均值骗局"（单位 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`，于是：

```text
至少一个下游变慢的概率 = 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 为单个依赖的慢概率）**

| 扇出 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，把整条链路的剩余预算逐跳向下传）把尾延迟的上界钳住。

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

> [!TIP]
> 分位数的采样纪律：**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）。

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

## 3.7 性能优化的正确流程：先测量，再优化，最后回归

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

```text
性能优化闭环（每一步都有产物，缺一环结论就不可信）

  ① 定义指标与目标            产物：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、pidstat | `sy` 高、CPU 被节流、NUMA 远程访问 |

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

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

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

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

> [!NOTE]
> 面试官会追问的下一层：**"如果指标已经达标，还要不要继续优化？"** 要区分"够用"与"脆弱"：达标但余量不足（利用率长期高于 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 的本地计算。仅按量级判断这个方案是否可行，并给出至少两条可行的改造方向。

<details><summary>答案与解析</summary>

粗算：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、日志落盘、锁），并看扇出倍数是否放大了它。

</details>

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

<details><summary>答案与解析</summary>

`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 反推可接受排队时间，再除以单请求服务时间，并留出故障余量，而不是拍一个数字。

</details>

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

<details><summary>答案与解析</summary>

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 怎么办？"——可能来自超线性效应（并行后工作集装进私有缓存反而减少总访存量）或测量偏差（未预热、基线受压测端限制），要能给出两种解释而不是直接宣称定律错了。

</details>

**Q4**（辨析）"我们给这个 HTTP 接口做了 USE 分析：利用率 30%、饱和度低、错误率 0，所以接口没问题。"这句话错在哪？修正后应该怎么观测？

<details><summary>答案与解析</summary>

**错在对象错位**。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 节）。

</details>

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

<details><summary>答案与解析</summary>

`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` 时，**共享资源成为尾延迟的主导项**，此时优化单个下游完全无效，必须做隔离。

</details>

## 面试高频问题与陷阱

| 面试问法 | 考查角度 | 陷阱 / 加分点 | 对应小节 |
|---|---|---|---|
| 一次跨机房调用大概相当于多少次内存访问？这些量级对性能优化意味着什么？ | 数量级是否形成直觉 | 陷阱：只背"网络慢"说不出倍数；或把 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 节各指标的口径定义。


---

> 作者: [破戒僧](https://onlyblacktea.cn)  
> URL: https://onlyblacktea.cn/book/backend/ch03/  

