# 第 11 章 Linux 性能四板斧


> 性能问题不是猜出来的，是从系统整体逐层收敛到一行代码的证据链。本章把 CPU、内存、磁盘、网络四类指标读到能定案。
## 学习目标

读完本章，你应当能够：

1. 按「系统整体 → 某类资源 → 具体进程 → 具体代码」组织排查，并说明为什么先看代码必然跑偏。
2. 说清负载 = 可运行（R）+ 不可中断（D）的**任务数**与指数衰减平均口径，推出高负载低 CPU 的三种成因。
3. 把 `vmstat` 各列与 `top` 的八项 CPU 时间对上，判断 `wa` 高的两种相反解释、`sy` 高与切换的关系、`st` 的含义。
4. 用 `iostat -x` 的 `await` / `aqu-sz` 判断磁盘瓶颈并论证 `%util` 为什么不可靠；用 `sar`/`ss`/`netstat -s` 定位网络侧问题。
5. 在 `perf`、off-CPU 火焰图、`bpftrace`、`ftrace`、`/proc/[pid]/stack` 之间选出能回答「进程在等什么」的那一个，并走完「P99 从 50 ms 涨到 800 ms」的排查链。
### 前置知识检查

| 你需要知道 | 在哪一节补齐 |
|---|---|
| 进程状态机、`D` 状态、cgroup 资源限制 | 见第 10 章 10.1 节、10.6 节 |
| CFS 调度与上下文切换开销的量级 | 见第 5 章 5.5 节 |
| 系统调用开销与中断路径 | 见第 9 章 9.5 节、9.7 节 |
| page cache 与虚拟内存的观测面 | 见第 7 章 7.6 节、第 8 章 8.3 节 |
## 11.1 排查方法论：收敛路径与 USE 法

排查难在**搜索空间太大**：一次慢请求可能源于应用锁、GC、磁盘队列、宿主机超卖，各层现象相同。新手最常做的动作是打开代码找可疑循环，然后花两小时读一段本来没问题的逻辑——这条路必然失败，因为代码层是搜索空间最大、证据密度最低的一层：一个服务几十万行，而系统指标只有几十个数字。正确做法是利用「**上层指标异常必在该层留下可观测痕迹**」逐层排除，每层把候选原因砍掉一个数量级：

```text
  收敛路径（每层只回答一个问题，答完才能下移）
  ① 系统整体  真的慢吗？慢在哪个维度？   uptime / vmstat 1 / pidstat 1
       判据：负载高? CPU 满? 内存换页? IO 排队? 网络重传?
  ② 某类资源  是 CPU / 内存 / 磁盘 / 网络 哪一类？  vmstat / free / iostat -x / sar -n DEV
       判据：该类资源的利用率或饱和度是否触及上限
  ③ 具体进程  哪个进程贡献了这个指标？     pidstat -u -w -r -d -t 1（须定位到线程）
  ④ 具体代码  它在干什么？在等谁？         perf top / off-CPU 火焰图 / /proc/PID/stack
```

支撑这条路径的是 USE 方法（utilization / saturation / errors）：**利用率回答「忙不忙」，饱和度回答「排队多长」，错误回答「有没有丢东西」**。饱和度对延迟最敏感——利用率 100% 只是「没空闲」，饱和度高意味着新请求要排队，延迟立刻恶化。

| 资源 | 利用率（忙不忙） | 饱和度（排队多长） | 错误（丢了吗） |
|---|---|---|---|
| CPU | `100 − id − wa − st` | `vmstat` 的 `r`、`/proc/loadavg`、`/proc/pressure/cpu` | `dmesg` 的降频与 mce |
| 内存 | `available / total`（不是 `free`） | `vmstat` 的 `si`/`so`、`/proc/pressure/memory` | OOM kill、`memory.events` 的 `oom_kill` |
| 磁盘 | `%util`（须配并发度读） | `aqu-sz`、`await`、`/proc/pressure/io` | `dmesg` 的 I/O error |
| 网络 | `sar -n DEV` 的带宽占比 | `listen overflow`、`ss -s` 的发送队列 | `retransseg`、`SyncookiesSent`、`softnet_stat` 丢包列 |

> [!IMPORTANT]
> 面试的分水岭在这一节：能背 `vmstat` 各列的人很多，能说清「先看饱和度还是先看利用率、以及为什么」的人很少。标准答法：利用率说明资源有没有闲着，饱和度说明请求有没有在排队，决定延迟的是后者。所以看到 `%util = 100%` 不要下结论，看到 `aqu-sz` 从 1 涨到 8 才是动手信号。
## 11.2 平均负载：统计的是任务数，不是 CPU 使用率
### 11.2.1 三个数字的真实口径

`/proc/loadavg` 与 `uptime` 的三个数字是 **1、5、15 分钟的指数衰减移动平均**（exponentially decaying moving average），不是窗口内的算术平均。内核用定点数（`FSHIFT = 11`，即 `FIXED_1 = 2¹¹ = 2048`）周期性做指数平滑：

```text
  指数衰减平均的递推（kernel/sched/loadavg.c，每个 tick 执行一次）
  active  = nr_running + nr_uninterruptible      ← 注意：D 态被算进来
  load[i] = (load[i] * exp[i] + active * FIXED_1) / (exp[i] + FIXED_1)
  三个窗口的 exp[] 常量（决定衰减快慢）
    exp[0] = 1884 ≈ 2048 / e^(5/60)    → 1 分钟窗口
    exp[1] = 2014 ≈ 2048 / e^(5/300)   → 5 分钟窗口
    exp[2] = 2037 ≈ 2048 / e^(5/900)   → 15 分钟窗口
  为什么不用算术平均：算术平均要保存窗口内全部样本，指数平均只要一个累加器，
  O(1) 空间与时间；代价是旧数据权重永不为零。与 TCP 的 SRTT 平滑（第 17 章 17.3 节）同一取舍。
```

> [!WARNING]
> 高频错误：把 `/proc/loadavg` 说成「过去 1/5/15 分钟的平均进程数」。它既不是算术平均（是指数衰减平均），也不是「进程数」而是**任务数**——同一进程的多个线程各算一个。一个开 200 线程的服务与一个单线程服务，同一份代码的负载口径完全不同。
### 11.2.2 负载的两个组成部分：R 与 D

| 状态 | 内核含义 | 为什么算进负载 |
|---|---|---|
| `R`（TASK_RUNNING） | 正在 CPU 上跑，或在运行队列等 CPU | 想要 CPU 而没拿到，是真正的 CPU 需求 |
| `D`（TASK_UNINTERRUPTIBLE） | 不可中断睡眠：等磁盘 I/O、等驱动事件 | 它**想要资源**而没拿到。不可中断是因为该路径持有内核不变量，被信号打断会让结构半更新（见第 10 章 10.2 节） |
| `S` 等可中断睡眠 | 等网络、等锁、`epoll_wait`、`sleep` | **不算**：不消耗资源，不代表系统承压 |

`D` 态计入负载是负载指标最重要也最容易答错的一点。设计意图是：**负载衡量「系统有多少活等着被干」，这个量不该只算 CPU**。一块慢盘会把一批任务按在 `D` 态，此时 CPU 可能很闲，但系统确实忙不过来——这就是高负载低 CPU 的来源，也是 8 核机器上负载同为 8 却含义完全相反的原因。

> [!CAUTION]
> 把 `load average` 说成「CPU 使用率」最容易把面试答崩。两者**量纲不同**：负载是任务个数（无上限，可到几千），CPU 使用率是时间百分比（0 ~ 100%）。64 核机器负载 64 而使用率 100%，与负载 64 而使用率 10%，是两个完全不同的问题。
### 11.2.3 数值口径与三种「高负载低 CPU」的原因

`load ≤ 核数` 说明队列基本不排队，`≈ 核数` 说明刚好跑满无余量，`> 核数` 说明开始排队，`load / 核数` 大致反映每个任务要等的倍率。但该判据**只在负载由 CPU 贡献时成立**：若负载主要来自 `D` 态，除以核数毫无意义，因为慢盘造成的 `D` 态任务数与核数无关。

三种成因的机制：**其一，`D` 态 I/O 阻塞**——任务在等磁盘、等网络文件系统、等块设备。**其二，大量线程频繁唤醒**——负载按 tick 采样**瞬时队列长度**而非累积 CPU 时间，若一批线程处于「睡极短 → 醒 → 跑极短 → 再睡」的循环（忙等、细粒度定时器、锁竞争重试、惊群），采样点容易撞上它们同时在队列里的瞬间，负载虚高而 `us + sy` 不高。**其三，cgroup CPU 配额节流**——任务确实处于 `R` 态，但 `cpu.max`（v2）/ `cpu.cfs_quota_us`（v1）在周期内已用完，被节流到下个周期：整机看 CPU 空闲，容器内看永远排队。

| 原因 | 负载来源 | CPU 使用率 | 定位命令 |
|---|---|---|---|
| `D` 态 I/O 阻塞 | `b` 列 | 低，`wa` 可能高 | `vmstat` 的 `b`、`iostat -x`、`/proc/PID/wchan` |
| 线程频繁唤醒 | `r` 列瞬时抖动 | 低但 `sy` 偏高 | `vmstat` 的 `cs`/`in`、`pidstat -w 1` |
| cgroup CPU 配额 | `r` 列持续非零（容器内视角） | 被硬顶在配额值 | `/sys/fs/cgroup/cpu.stat` 的 `nr_throttled`/`throttled_usec` 是否持续增长 |

```bash
cat /proc/loadavg   # 形态: 0.42 0.65 0.71 1/523 12043；第 4 字段 = 当前运行队列 1 个 / 总任务 523 个
vmstat 1 3          # 区分 R 与 D：r 列 = 运行队列，b 列 = D 态；负载的两种来源都在这
# procs -----------memory---------- ---swap-- -----io---- --system-- -------cpu-------
#  r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
#  9  0      0 812345  12345 456789    0    0     3    12  210  340  3  2 95  0  0
#  0  8      0 ...        ← r=9,b=0 是 CPU 排队；r=0,b=8 是卡在 D 态等磁盘
```

> [!TIP]
> 记忆锚点：**「高负载低 CPU」= 有人想要资源但拿不到，而这个资源不是 CPU**。三个候选就是等盘（D）、等调度机会（唤醒风暴）、等配额（cgroup 限流）。按这个顺序答，比罗列现象有效得多。
## 11.3 CPU 统计：把 `vmstat` 的每一列对上内核计数器
### 11.3.1 列定义与第一次输出的陷阱

`vmstat` 的数据来自 `/proc/stat`、`/proc/vmstat`、`/proc/meminfo`，所有比率列都是「**自上次采样的区间速率**」，因此第一次输出是**自开机以来的累计平均**，必须丢弃，从第二行看。这是现场最容易踩的坑：`vmstat 1` 第一行显示 `us=30`，有人据此判断「CPU 30%」，实际那是开机至今的平均值。

| 列 | 含义 | 判据与常见误读 |
|---|---|---|
| `r` | 运行队列中可运行的任务数（含正在运行的） | 持续 `> 核数` 说明 CPU 饱和；对应 `load` 的 `R` 部分 |
| `b` | 处于不可中断睡眠（`D`）的任务数 | 非零且持续就是 I/O 阻塞；这就是负载里 `D` 的部分 |
| `si` / `so` | 从 swap 换入 / 换出到 swap 的速率（KB/s） | **判断「是否真的在用 swap」的唯一可靠信号**，见 11.5.2 |
| `bi` / `bo` | 块设备读入 / 写出的块数（blocks/s，1 block = 1024 B） | 统计**块设备层** I/O；单位是块不是 KB，见下 |
| `us` / `sy` | 用户态消耗 / 内核态消耗（含系统调用与部分中断处理） | `sy` 高通常指向系统调用密集或切换密集，见 11.3.4 |
| `id` | 空闲 | `id` 高**不代表没压力**：`D` 态任务不消耗 CPU |
| `wa` / `st` | 等待 I/O 的时间 / 被宿主机偷走的时间 | 前者见 11.3.3；后者仅虚拟化下有，见 11.3.5 |
### 11.3.2 `top` 的八项 CPU 分解

`top` 顶部的 `%Cpu(s)` 与 `vmstat` 是同一批计数器，只是切分更细：`us`（用户态）、`sy`（内核态）、`ni`（nice，被调低优先级的进程所用）、`id`（真空闲）、`wa`（等 I/O）、`hi`（硬中断）、`si`（软中断）、`st`（被偷走）。八项在任何时刻相加恒为 100%，所以现场心算用 `有效计算时间 = us + sy + ni + hi + si = 100 − id − wa − st`。一个易混淆点：`top` 默认 Irix 模式下 `us` 已包含 guest 时间，按 `Shift+i` 切到 Solaris 模式才会分开。

| 字段 | 高的时候该怀疑什么 | 下一步命令 |
|---|---|---|
| `us` | 应用计算密集：序列化、加密、正则回溯、GC | `perf top -p PID` 看热点函数 |
| `sy` | 系统调用频繁、切换频繁、内核锁竞争、网络协议栈 | `vmstat 1` 看 `cs`；`perf top -g` 看内核函数 |
| `ni` | 有进程被调过 nice 且占用可观 CPU | `top` 的 `NI` 列 |
| `id` | 空闲。若同时负载高，必须转查 `b` 列与 cgroup 配额 | `vmstat` 的 `b`、`cpu.stat` |
| `wa` | 见 11.3.3，两种相反解释 | `iostat -x`、`pidstat -d` |
| `hi` / `si` | 硬中断过密（高频小包网卡）/ 软中断过密（`NET_RX`、`BLOCK`） | `/proc/interrupts`、`/proc/softirqs` |
| `st` | 宿主机 CPU 超卖 | `mpstat -P ALL 1`，见 11.3.5 |

```bash
vmstat 1 5                      # 每秒一行共 5 行：看趋势形态，而不是某一瞬间的绝对值
mpstat -P ALL 1                 # 按核展开，才能发现"只有 1 个核 100%、其余空闲"这类问题
top -H -p $(pgrep -f 'svc')     # -H 是关键：否则只有进程级汇总，看不到哪个线程在烧 CPU
```

> [!WARNING]
> `vmstat` 的 `bi`/`bo` 单位是 **blocks/s（1 block = 1024 B）**，而 `iostat` 的 `rkB/s`/`wkB/s` 是 **KB/s**。把 `bo = 102400` 当成 100 MB/s 会算错 1000 倍。这是报告事故时最常见的量纲错误之一。
### 11.3.3 `wa` 高不代表磁盘慢：两种相反解释

`wa`（iowait）对应 `/proc/stat` 的 `iowait`，内核只在很窄的条件下累加：**该 CPU 空闲（没有可运行任务），同时至少有一个任务正在等本地块设备的 I/O 完成**。每个限定词都重要：CPU 有活干时等待 I/O 的任务不产生 `iowait`，所以**忙碌的机器上 `wa` 天然偏低**，此时磁盘可能已经是瓶颈；等网络文件系统（NFS）或套接字的 I/O **不计入**，所以存储全走 NFS 的服务磁盘很慢也可以 `wa = 0`；它是「CPU 视角的空闲归类」，`iowait` 期间发起 I/O 的 CPU 实际空闲，并没有被 I/O 占住。

| 现象 | 可能的真相 | 区分办法 |
|---|---|---|
| `wa` 高 | 磁盘确实慢，任务在排队 | `iostat -x` 的 `await` 显著高于设备固有延迟、`aqu-sz` 上升 |
| `wa` 高 | 磁盘其实空闲，只是该 CPU 恰好没事干而另一任务在等一次 I/O | `%util` 低、`aqu-sz ≈ 0`，说明设备没排队 |
| `wa` 低但应用慢 | 磁盘是瓶颈，但 CPU 一直在忙（没机会累加 `iowait`） | `pidstat -d` 看 `iodelay`，它**按进程归属**且不受 CPU 是否空闲限制 |

> [!IMPORTANT]
> 标准答法的形成逻辑：`wa` 是「CPU 空闲且有人在等本地块设备 I/O」的时长占比，**归因给 CPU，不归因给磁盘**。所以它既不是磁盘利用率的代理，也不能作为磁盘瓶颈的判据——判磁盘瓶颈请看 11.6 节的 `await` 与 `aqu-sz`。
### 11.3.4 `sy` 高与系统调用、上下文切换

`sy` 包含三部分：**系统调用执行**、**中断与软中断处理**（扣掉单列的 `hi`/`si`）、**内核线程与内核内部工作**。一次系统调用的成本分四块（见第 9 章 9.5、9.7 节）：特权级切换（寄存器与栈保存、`syscall`/`sysret` 本身）、地址空间相关处理（`SWAPGS`、返回用户态时的 TLB 处理）、参数校验与权限检查、以及**执行体本身可能触发上下文切换**（如 `read` 遇数据未就绪就睡）。因此 `sy` 高的成因按排查顺序是：系统调用**次数**太多（不是单次太贵，典型是 `read`/`write` 小包循环、`futex` 风暴、`epoll_wait` 空转，用 `strace -c -f -p PID` 或 `perf trace` 看分布）；上下文切换太多（每次切换都要执行调度与地址空间切换，还破坏缓存局部性，用 `vmstat` 的 `cs` 与 `pidstat -w` 定位）；内核锁竞争（`sy` 高且 CPU 大量「忙等在内核」，用 `perf top -g` 看内核函数）。
### 11.3.5 `st`：被宿主机偷走的时间

`st`（steal time）是**当前 vCPU 处于可运行状态，但宿主机物理 CPU 没有调度给它**的时间。它是协作式指标，只有宿主机的虚拟化层（KVM 的 `kvm_steal_time` 等）主动报告时才有意义，裸机恒为 0；判据是持续超过 5% ~ 10% 就值得警惕。它的关键性质是**在 Guest 内部任何应用优化都无效**，典型症状是「同一份代码、同样 QPS，在容器或云主机上 P99 抖动明显更大」。

> [!NOTE]
> `st` 与其他字段的边界：这段时间 vCPU「想跑没跑成」，所以不计入 `id`（不是空闲），也不计入 `us`/`sy`（没真的执行）。这正是八项之和恒为 100% 的原因，也是排查虚拟化性能时**必须把 `st` 单列出来看**的原因：它会把真实 CPU 需求掩盖成一个偏小的 `us`。它由宿主机写入 Guest 内核的 per-CPU 统计，容器共享同一内核，所以只能从宿主机或节点层监控读到。
## 11.4 上下文切换：`cs` 高但不忙的典型场景
### 11.4.1 `cs` 与 `in`：两个常被混为一谈的计数器

```text
  两类事件的来源与关系（/proc/stat 的 intr 与 ctxt 两个累加计数器）
  时钟/设备中断、IPI 唤醒 ──► intr ──► vmstat 的 in
  以下事件都可能引发一次切换 ──► ctxt ──► vmstat 的 cs
    ├─ 任务主动睡眠（等 I/O、等锁、等条件变量）  → 自愿切换
    ├─ 时间片耗尽被抢占 / 更高优先级任务被唤醒   → 非自愿切换
    └─ 中断返回时发现更高优先级任务可运行        → 非自愿（in 与 cs 的耦合点）
  两者不等价：一次中断不一定引发切换（处理完直接返回原任务）；一次切换也不一定
  源于中断（syscall 里主动 sleep 也会切）
```

自愿切换由任务自己发起（等 I/O、等锁、`nanosleep`、`futex`、读空管道），高本身正常，但与低 CPU 使用率组合说明大量时间花在等待上；非自愿切换由内核抢占发起（时间片耗尽仍想跑、被更高优先级抢占），说明 CPU 争抢激烈或优先级配置不当，该任务的 CPU 需求没被满足。
### 11.4.2 `cs` 高但不忙的三类场景

「`cs` 几十万/秒而 `us + sy` 只有 20%」信息量很大：它几乎排除了「算得太多」，指向「切换得太多」。**其一，锁竞争。** 无竞争的 `futex` 快路径在用户态一次 CAS 完成（约 20 ns）；一旦有竞争，等待方要进内核 `futex` 等待队列睡眠，持锁方释放时再唤醒——**一次竞争至少带来两次切换**，且唤醒后未必马上抢到锁，可能反复「睡 → 醒 → 抢不到 → 再睡」。**其二，频繁唤醒（唤醒风暴 / 惊群）。** 事件源一次到达唤醒大量等待者，或线程池里每个任务都极短；唤醒目标若在不同 CPU 还会触发核间中断（IPI），`in` 跟着涨。**其三，细粒度定时器与轮询退避。** 大量任务以毫秒甚至更小间隔 `nanosleep`，或 `select`/`poll` 超时值极小导致空转。

```bash
pidstat -w 1 3                          # 列: UID PID cswch/s nvcswch/s Command；cswch/s 高 → 在等资源
pidstat -w -t -p $(pgrep -f 'svc') 1    # nvcswch/s 高 → 抢不到 CPU；-t 暴露"哪个线程在频繁睡醒"
perf record -e sched:sched_switch -a -- sleep 5 && perf script | head -50   # 追踪 prev/next 配对
```

> [!WARNING]
> 常见错误回答：「上下文切换的开销就是保存寄存器，很快」。真实成本是四块：寄存器与栈的保存恢复（约 1~5 μs）、**地址空间切换导致的 TLB 与缓存局部性损失**（大头，可能让之后几千条指令全部缓存未命中，见第 1 章 1.4 节）、调度器决策、以及新任务预热。所以减少切换次数的收益远大于让切换更快。
## 11.5 内存指标：`free` 的每一列与 `available`
### 11.5.1 `free` 输出的逐列解释

```text
  free -m 的列布局（Linux 3.14 之后）
                 total    used    free   shared  buff/cache   available
  Mem:           15883    6210    1893      412        7779        8842
  Swap:           4095     128    3967
  total      物理内存总量（MemTotal）
  used       = total − free − buff/cache
  free       完全未被引用的内存；它是"最没用"的那个数字
  shared     tmpfs/shmem 占用（/dev/shm、共享内存段），已被计入 cache
  buff       块设备元数据缓存（buffer_head 层面），典型只有几十~几百 MB
  cache      page cache（文件内容 + tmpfs + 可回收 slab），这才是大头
  available  free + 大部分可回收 cache/slab − 内核保留量（内核估算"不用 swap 还能给新应用多少内存"）
```

| 列的常见误读 | 为什么错 | 正确读法 |
|---|---|---|
| 「`free` 只剩 200 MB，赶紧扩容」 | page cache 会被优先用作缓存，可随时回收 | 看 `available / total` 与 `/proc/pressure/memory` |
| 「`used` 高就是内存泄漏」 | `used` 不含 buff/cache，但含不可回收 slab 与共享内存 | 按进程看 RSS/PSS：`/proc/PID/smaps_rollup` |
| 「`buff/cache` 是应用缓存，删掉能省内存」 | 它就是 page cache，删掉只会在下次读盘时重建 | `drop_caches` 只做实验，不做优化 |
| 「`buff` 与 `cache` 是两个独立可加的量」 | 2.4 起两者已统一到同一套页管理；`shared` 也已被计入 `cache` | 分开看，不做加法 |

`available` 由内核 `si_mem_available()` 估算（`/proc/meminfo` 的 `MemAvailable`）：从 `free` 出发，加上可回收 page cache 的绝大部分、可回收 slab 的一部分，再减去 `min_free_kbytes` 这类保留量。它是**估算值**，所以判据要看比例与趋势，不要卡在几百 KB。真正内存不足的签名是三条同时出现：`available` 逼近 0、`buff/cache` 被压得很低（说明回收一直在工作）、`si`/`so` 开始动。
### 11.5.2 `si` / `so`：是否在用 swap 的唯一可靠信号

`vmstat` 的 `si`（从 swap 换入）与 `so`（换出到 swap）是**唯一可靠的「是否真的在用 swap」信号**。原因：`free` 的 `Swap: used` 只是「swap 里目前有多少页」，它**只反映历史压力**——一台机器可能在几分钟突发中换出 2 GB，之后内存宽裕了，这些页仍躺在 swap 里（换入是惰性的，被访问时才换回）。若 `si`/`so` **持续非零**，确实不够，损失机制也明确：换页按页（4 KB）同步进行，一次换入可能是一次磁盘读；若换入的页之后又要换出别的页，就形成 **swap 抖动（thrashing）**，CPU 大量时间耗在页替换上，吞吐断崖式下跌。此时 `wa` 未必高，因为换页等待发生在缺页处理路径上，属于进程自己的执行时间。反之 `si`/`so` **全为 0 而 `Swap used` 非零**只说明历史上换过，现在不在动，**不构成当前性能问题**。

```bash
vmstat 1 5                        # 看 si/so 两列是否有持续非零，这才是"正在换页"
cat /proc/pressure/memory         # avg10 = 最近 10 秒内至少一个任务因内存而停顿的时间占比
cat /sys/fs/cgroup/memory.events  # 容器内看 cgroup v2 事件：关注 max / oom / oom_kill
```

> [!IMPORTANT]
> 标准答法：「判断是否在用 swap，看 `vmstat` 的 `si`/`so` 是否持续非零，不要看 `free` 的 `Swap used`。」能补上「`Swap used` 是存量、只反映历史压力」与「`vm.swappiness` 的默认 60 决定的是**换出倾向**而不是『是否允许换出』」，就领先绝大多数候选人。注意 `swappiness = 0` 也不等于禁止换页，内存严重不足时内核仍会换出（见第 12 章）。
## 11.6 磁盘 I/O：`await` 与 `aqu-sz`，以及 `%util` 为什么不可靠
### 11.6.1 `iostat -x` 的关键列

`iostat -x` 的数据源是 `/proc/diskstats`，**它默认打印所有设备（含分区）**，看分区行与整盘行容易重复计数，务必用 `iostat -x 1 nvme0n1` 限定设备。

| 列（`iostat -x` 常见名） | 旧名 | 含义 | 判据 |
|---|---|---|---|
| `r/s`、`w/s` | `tps` | 每秒读/写请求次数（IOPS 的两个分量） | 与 `await` 一起决定吞吐上限 |
| `rkB/s`、`wkB/s` | `kB_read/s` 等 | 每秒读/写的千字节数 | 除以 `r/s` 得平均请求大小；小请求 + 高 IOPS 即随机负载 |
| `r_await`、`w_await` | — | 读/写请求平均完成延迟（ms），含排队 + 设备服务时间 | 与设备固有延迟比较，见 11.6.3 |
| `await` | — | 读写延迟的加权平均 | 混合负载下会掩盖读或写的异常，读写要分开看 |
| `aqu-sz` | `avgqu-sz` | 平均**在途请求数**（队列长度） | `> 1` 说明有排队；判断饱和度的首选 |
| `%util` | — | 「设备至少有一个 I/O 在处理」的时间占比 | 见 11.6.2，**不可靠** |
| `rareq-sz`、`wareq-sz` | — | 读/写平均请求大小（KB） | `≥ 128 KB` 偏顺序，`4~16 KB` 偏随机 |
| `%rrqm`、`%wrqm` | — | 被合并的请求占比 | 合并率高说明上层 I/O 模式接近顺序 |
### 11.6.2 `%util` 为什么不可靠

`%util` 的定义是「采样区间内该设备**有至少一个 I/O 在途**的时间占比」。对单队列设备它近似等于「忙」的比例，但对现代设备这个类比彻底失效，因为**一台设备可以同时处理多个 I/O**：

```text
  并发 I/O 下 %util 与真实余量的关系（同一块 NVMe SSD）
  情形 A：单条流同步逐笔下发        情形 B：32 条流并发
    t=0 ├── req1 ──┤                  t=0 ├── req1 ─────────────┤
    t=L    ├── req2 ──┤                   ├── req2 ─────────────┤
    t=2L      ├── req3 ──┤                ├── ... 共 32 个 ─────┤
                                          ├── req32 ────────────┤
    %util = 100%（每刻都有 1 个在途）  %util = 100%（同样）；实际 IOPS = 1/L vs 32/L（高 32 倍）
  ⟹ %util 只能回答"设备有没有闲着"，不能回答"还有多少余量"
  RAID / 多队列更极端：逻辑卷背后 8 块盘并行，%util = 100% 而每块盘 aqu-sz 只有 0.5，余量充裕。
```

正确结论：**`%util` 高 ≠ 磁盘是瓶颈**，它只说明设备一直在处理 I/O；当在途并发远大于设备最佳并发度时 `%util` 必然 100%。反过来 **`%util` 低也不代表磁盘没问题**：等待可能发生在网络文件系统上，而它根本不计入块设备统计。
### 11.6.3 判断磁盘瓶颈的正确口径

一是看 **`await` 是否显著高于设备固有延迟**。数量级参照（4 KB 随机读，单次设备延迟）：NVMe SSD 约 100 μs、SATA SSD 约 100~200 μs、机械盘约 5~10 ms。若 NVMe 报出 `w_await = 5 ms`，那是排队而非设备慢。难点在于**必须知道这台设备的基线**，所以生产上看相对变化（与同集群同型号节点对比）而非绝对值。二是看 **`aqu-sz` 与并发度的关系**：在途请求数直接反映饱和度，由 Little 定律 `aqu-sz ≈ 到达率 × await` 可用它**交叉验证报告数字是否自洽**，若不自洽（虚拟化逻辑卷常见）本身就是重要线索；`aqu-sz` 接近或超过设备最佳队列深度时，再加负载只增延迟不增吞吐（`IOPS = Q / L` 的关系见第 9 章 9.9.1 节）。三是看 **`r_await` 与 `w_await` 是否分离**：混合负载下 `await` 会被平均掉，一次冷读的长延迟可能被大量写请求稀释，读写分开后常能发现「写在排长队，读很轻松」。

```bash
iostat -x 1 nvme0n1        # 限定目标设备；5.x 列含 r_await/w_await/aqu-sz/%util
pidstat -d 1               # 按进程归因：谁在产生这些 I/O，iodelay 暴露"在等 I/O"的时间
blktrace -d /dev/nvme0n1 -o - | blkparse -i - | head -40   # 用延迟分布代替平均值：长尾才是 P99 的根因
```

> [!CAUTION]
> 面试最容易答崩的一句是「`%util` 到 100% 说明磁盘到瓶颈了」。正确的反问式回答是：**「`%util` 只说明设备一直在处理 I/O，它不告诉你还有多少余量；判断饱和要看 `aqu-sz` 与 `await`。在并发 I/O 或 RAID 场景下，`%util = 100%` 完全可以健康。」** 反过来也要会答：`%util` 低同样不能证明磁盘没问题，因为等待可能发生在网络文件系统上。
## 11.7 网络指标：`sar`、`ss`、`netstat -s` 的排障含义
### 11.7.1 `sar -n` 子开关

| 命令 | 输出什么 | 关键列与判据 |
|---|---|---|
| `sar -n DEV 1` | 每网卡收发速率 | `rxkB/s`/`txkB/s` 与线速比较；`rxpck/s` 与 `txpck/s` 相除得平均包长，**包长小说明小包密集**（每包协议栈固定开销大，pps 才是瓶颈） |
| `sar -n TCP 1` | 连接层状态 | `active/s`、`passive/s`（新建连接速率）、`iseg/s`/`oseg/s`。`active/s` 远大于 `passive/s` 说明本机是客户端角色 |
| `sar -n SOCK 1` | Socket 总量 | `totsck`、`tcp-tw`（`TIME_WAIT` 数量）、`tcpsck`。`tcp-tw` 暴涨会耗尽本地端口（见第 16 章） |
| `sar -n EDEV 1` | 网卡错误与丢弃 | `rxdrop/s`、`txdrop/s`、`rxerr/s`。**非零说明链路侧已在丢**，这类丢弃常被应用层重传掩盖 |
### 11.7.2 `ss -s`、`netstat -s` 与它们的坑

`ss -s` 给的是**存量快照**：各类 socket 的当前数量、内核态内存占用、按状态的 TCP 连接计数，回答「现在有多少连接、卡在哪个状态」。`netstat -s`（或 `nstat`）给的是**累计计数器**，判据是看**增量**而不是总量——累计值只增不减，总量里的历史重传毫无意义。

| 计数器（`netstat -s` 中的行） | 含义 | 排障含义 |
|---|---|---|
| `segments retransmitted` | 重传段数 | 除以 `out segments` 得**重传率**：局域网应 `< 0.1%`，广域网 `< 1%`。偏高指向丢包、链路错误或对端接收窗口不足 |
| `SYNs to LISTEN sockets dropped` | SYN 队列溢出 | `tcp_max_syn_backlog` 太小或遭 SYN Flood，与 `SyncookiesSent` 对照 |
| `times the listen queue of a socket overflowed` | **accept 队列溢出** | 这是应用的锅：`accept()` 太慢，已完成三次握手的连接被丢弃，客户端超时重试。这是「服务端有空闲 CPU 却拒绝连接」的经典根因（见第 16 章） |
| `packets pruned from receive queue` | 接收缓冲区不足 | 应用读得慢，内核丢包，触发重传与窗口收缩 |
| `SyncookiesSent` / `SyncookiesRecv` | 启用了 SYN cookie | 说明半连接队列**确实溢出过**，是被动防护生效的证据，不是正常状态 |
| `TCPAbortOnMemory` / `TCPBacklogDrop` | 内存压力下丢连接 | 需与连接数上限、`tcp_mem` 一起看 |

```bash
nstat -az > /tmp/s1; sleep 10; nstat -az > /tmp/s2        # 取两次快照求差才是判据
awk 'NR==FNR{a[$1]=$2; next} $1 in a && $2!=a[$1] {print $1, "+"($2-a[$1])}' /tmp/s1 /tmp/s2 | sort -k2 -nr | head -20
# 输出形态: TcpRetransSegs +1842      ← 10 秒内 1842 次重传，该查链路了
ss -s      # 存量视角: Total: 1234  TCP: 890 (estab 512, closed 300, timewait 280)
ss -tan state time-wait | awk '{print $4}' | cut -d: -f2 | sort | uniq -c | sort -nr | head   # ss 走 netlink，比 netstat 快
```

## 11.8 高级工具的选择：谁在算 vs 谁在等

前七节回答「哪类资源饱和了」。最后一步要回答「**具体是哪行代码、或者它在等什么**」，工具选择的唯一依据是问题的形状：

```text
  按问题形状选工具（选错的代价是两小时白干）
  Q1 "CPU 在算什么?"   → perf top / perf record + on-CPU 火焰图（只看正在 CPU 上执行的栈）
  Q2 "进程为什么不在跑?" → off-CPU 火焰图 / /proc/PID/stack + wchan
     · 关键区分：等锁（off-CPU，可被唤醒）vs D 态（不可中断）；落地靠 eBPF 追踪
       sched_switch，统计"从切出到切入"的时长与栈
  Q3 "调了哪些系统调用、哪个最慢?" → strace -T -c / perf trace
     · strace 每次 syscall 两次 ptrace 停，开销巨大，只适合短时或低频进程
  Q4 "内核某函数被调了多少次?" → bpftrace / ftrace（内核内建、零额外依赖）
  Q5 "延迟的长尾分布长什么样?" → bpftrace 直方图 / perf latency（平均值骗人，必须看 P99/P999）
```

| 工具 | 最好回答的问题 | 主要代价 / 边界 |
|---|---|---|
| `perf top` / `perf record -g` + 火焰图 | **on-CPU** 热点函数与调用链归属（谁调用了那个热点） | 只有 on-CPU 视角；受 `perf_event_paranoid` 限制；采样频率过低会漏长尾 |
| off-CPU 火焰图 | 进程**不在 CPU 上**的时间花在哪（等锁、等 I/O、被抢占） | 需 eBPF（`bcc` 的 `offcputime`）或 `perf sched`；用户态栈符号缺失则无法归因到业务代码 |
| `bpftrace` / `ftrace` | 一行脚本回答特定问题（谁在 `open`、`read` 大小分布、谁在等 `futex`）；ftrace 可看内核调用链耗时 | 需内核 4.x+ 与 `CONFIG_BPF`；脚本本身可能成为新的开销源；`function_graph` 在热路径开销大 |
| `strace -T -c` | 系统调用次数与耗时分布 | ptrace 开销可达数量级，对高 QPS 服务不可用，会先把它拖死 |
| `/proc/PID/stack`、`wchan` | 某个进程此刻**卡在哪**（内核栈 + 等待函数名） | 需 root；`wchan` 在 4.1+ 默认只给地址 |

```bash
perf top -p $(pgrep -f 'svc') -g      # on-CPU 热点：加 -p 只看该进程，不加是全局视角
perf record -F 999 -g -p PID -- sleep 30 && perf script | stackcollapse-perf.pl | flamegraph.pl > oncpu.svg   # 火焰图脚本来自 Brendan Gregg 开源仓库
offcputime -df -p PID 30 > offcpu.folded        # off-CPU：等锁/等 I/O 的时间去哪了（bcc，需 eBPF）
flamegraph.pl --color=io --title="Off-CPU" offcpu.folded > offcpu.svg
bpftrace -e 'kprobe:futex_wait { @[ustack] = count(); }'          # 一行脚本：谁在等 futex（锁竞争的第一现场）
echo function_graph > /sys/kernel/tracing/current_tracer          # ftrace：追踪内核函数的调用链
for p in $(ps -eo pid,state | awk '$2=="D"{print $1}'); do
    echo "PID $p wchan=$(cat /proc/$p/wchan)"; cat /proc/$p/stack | head -8   # D 态进程卡在哪个函数，直接指向根因
done
```

> [!TIP]
> 面试话术锚点：**「`perf top` 看到的是 CPU 在算什么，看不到进程在等什么。等锁、等 I/O、等网络的时间不在任何 CPU 热点里，只能靠 off-CPU 火焰图或调度事件追踪。」** 这能立刻把「只会用 `perf top`」和「真的排查过线上问题」区分开。追问「off-CPU 的栈怎么来」——答 eBPF 挂 `sched_switch`，记录切出时刻与栈、切回时求差并按栈聚合（`offcputime` 就是这个思路）。
## 11.9 完整案例：接口 P99 从 50 ms 涨到 800 ms

设定：无代码变更、无发布，流量与前一天同一时段持平，但 P99 从 50 ms 涨到 800 ms，P50 只从 20 ms 涨到 35 ms。**P50 几乎没动而 P99 涨了 16 倍**这个形状本身排除了「整体资源不足」（那会同时抬高 P50），指向「部分请求被某个偶发事件阻塞」。

第一步看系统整体：`vmstat` 若给出 `r=2, b=11, si=so=0, id=88, st=0`，则**负载高（`r + b` 超过 8 核）但 CPU 空闲（`id = 88`），且 `b = 11`**——这正是 11.2.3 节「原因一：`D` 态 I/O 阻塞」的签名；`si`/`so = 0` 排除内存换页，`st = 0` 排除虚拟化超卖。第二步验证块设备侧是否真的排队：

```bash
uptime; nproc; cat /proc/loadavg; vmstat 1 5    # 一次拿到负载、核数与 r/b/si/so/cs 的轮廓
iostat -x 1 nvme0n1
# 稳态行: r/s=12 r_await=1.20  w/s=340 w_await=48.60  aqu-sz=17.20  %util=99.80；判据：NVMe 写延迟基线约 0.1~1 ms，48.6 ms 是排队而非设备慢，aqu-sz=17.2 说明在途已堆满
df -hT | awk '{print $1, $2, $7}'               # 确认落盘的是哪一类文件系统
```

这里的关键是**交叉验证**：`w_await` 与 `aqu-sz` 同时恶化，两个独立指标指向同一结论，才是可靠证据——单看 `%util = 99.8%` 不足以下结论（11.6.2 节）。同时要排除一个常见误判：若 `df` 显示数据落在 NFS 上，这笔 I/O `iostat` 根本看不到（`iowait` 也不计），此时要换 `nfsiostat`、`mountstats`。第三步把写入归因到进程与线程，第四步才读代码——而第一件事不是读代码，是读内核栈：

```bash
pidstat -d 1 3                                     # kB_wr/s 找出写入大户；iodelay 高说明真在等 I/O
ps -eo pid,stat,wchan:24,comm | awk '$2 ~ /^D/'    # 形态: 8421 D jbd2_log_wait_commit java ← 卡在文件系统日志提交
cat /proc/8421/stack     # 自顶向下是调用链的反序
# [<0>] __flush_work+0x1a2/0x2c0 / journal_commit_transaction+0x8f1 / kjournald2+0x9d
# ⟹ ext4 的日志提交线程 jbd2 被块设备写延迟拖住
```

**定位结论。** `b = 11` 与 `w_await = 48.6 ms` 说明块设备写队列已饱和；`pidstat -d` 把写入归因到某个进程；`/proc/PID/stack` 显示它卡在文件系统日志提交上。根因是**写 I/O 饱和导致的 `D` 态阻塞**：某进程开始大量写盘，同盘上的其他进程一起被拖慢，每次写请求排队约 50 ms，应用线程被按在 `D` 态，于是 P99 跳到数百毫秒。而 P50 几乎不动，是因为只有命中这条写路径的请求才被阻塞：读缓存命中的请求不受影响（占多数，P50 稳定），恰好需要写盘或读冷数据的请求才排队（少数，构成 P99）——这就是尾延迟放大的一般规律。

**处置与验证（按收益/风险排序）。** 一是**先止损、不动代码**：确认是不是临时批量任务（备份、日志轮转、数据搬迁）在占盘，错峰即可。二是**减写入量**：调低日志级别、合并小写、批量提交（把 N 次 `fsync` 合成一次，即组提交思路，见第 21 章 21.3 节）。三是**隔离 I/O**：日志/备份/数据库拆到不同块设备，用 cgroup v2 的 `io.max` 或 `io.latency` 给关键进程设保护（见第 10 章 10.6 节），注意 `io.max` 限带宽/IOPS 而 `io.latency` 限延迟，后者对尾延迟更对症。四是**确认后再扩容**：只有在减少无效写入并隔离之后仍饱和，才考虑加盘或换更高 IOPS 的设备。五是**验证**：`vmstat` 的 `b` 回到 0 附近、`aqu-sz` 落到 1 以下、`w_await` 回到设备基线、P99 回落，四项都要变好才算解决。

> [!IMPORTANT]
> 这个案例的价值不在结论，而在于**每层都有排除法的动作**：第一步用 `si`/`so` 与 `st` 排除内存和虚拟化，第二步用 `await` + `aqu-sz` 双指标确认磁盘饱和（而不是只信 `%util`），第三步用 `pidstat -d` 归因到进程，第四步用 `/proc/PID/stack` 归因到等待对象。面试时把这条链讲完整，比背任何结论都有说服力。
## 本章小结

| 关键结论 | 一句话表述 | 常见错误 |
|---|---|---|
| 收敛路径与 USE | 每层只回答一个问题；利用率看忙不忙，饱和度看排队多长，延迟由饱和度决定 | 上来就读代码；只看利用率，看到 `id` 高就认为健康 |
| 平均负载 | 可运行（R）+ 不可中断（D）的**任务数**，1/5/15 分钟指数衰减平均，须除以核数 | 说成「CPU 使用率」或「算术平均的进程数」 |
| `vmstat` / `wa` | 第一次输出是累计平均必须丢；`bi/bo` 单位是 blocks/s；`wa` 是「CPU 空闲且有人在等本地块设备 I/O」，归因给 CPU | 用第一行判断当前 CPU；认定 `wa` 高就是磁盘慢 |
| `sy` / `st` | `sy` 指向系统调用次数、切换次数、内核锁竞争；`st` 是被宿主偷走的时间，容器内不可见 | 说「切换开销只是保存寄存器」；把云上抖动全归因到应用 |
| 内存与 swap | 看 `available` 而非 `free`；`buff/cache` 是合并列且 `shared` 已计入 `cache`；`si`/`so` 持续非零才是真换页 | `free` 小就扩内存；看到 `Swap used` 非零就断定内存不足 |
| 磁盘判据 | 用 `await` 对比设备基线、用 `aqu-sz` 看并发度；`%util` 只说明设备没闲着 | 说「`%util` 100% 就是磁盘瓶颈」 |
| 网络判据 | 累计计数器要取增量算比率；`listen queue overflow` 是应用 `accept` 慢 | 直接读 `netstat -s` 的绝对值下结论 |
| 工具选择 | `perf top` 看 on-CPU 在算什么；off-CPU 火焰图看它在等什么；`/proc/PID/stack` 看卡在哪 | 用 `perf top` 找阻塞问题，自然什么都找不到 |
## 自测题

**Q1**（判断 + 推导）某 8 核机器 `uptime` 显示 `load average: 32.00, 30.50, 28.00`，而 `top` 显示 `5.0 us, 3.0 sy, 90.0 id, 0.0 wa, 0.0 st`。请判断是否存在 CPU 瓶颈，并给出至少三种可能的解释与对应验证命令。
<details><summary>答案与解析</summary>

不存在 CPU 瓶颈。判据有两条：`us + sy = 8%`，CPU 大量空闲；而 `load / 核数 = 4` 看似「超载 4 倍」，但这个判据只在**负载由 CPU 贡献**时有效。负载 = `R` + `D`，`id = 90%` 说明运行队列不长，那 32 个任务必然主要来自 `D` 态。

1. **`D` 态 I/O 阻塞**：`vmstat 1` 看 `b` 是否约等于 32；`ps -eo stat,pid,comm | awk '$1 ~ /^D/'`；`iostat -x 1` 看 `await`/`aqu-sz`；`/proc/PID/stack` 看栈顶等待函数。
2. **线程频繁唤醒导致队列瞬时虚高**：`vmstat` 的 `cs`/`in` 与负载同向飞涨；`pidstat -w 1` 看谁的自愿切换率极高。此时 `wa ≈ 0`，可与第一种区分。
3. **cgroup CPU 配额节流**：`/sys/fs/cgroup/cpu.stat` 的 `nr_throttled`/`throttled_usec` 持续增长（仅「非零」不算，要看增长）；`cpu.max` 看配额。整机空闲与容器内排队可以同时成立。

错在哪：把 `load / 核数 = 4` 直接读成「CPU 严重不足」。面试官会追问「`wa` 是 0，你怎么还说是 I/O 问题？」——`wa` 只在 **CPU 空闲**时才累加，所以 `wa = 0` 在此恰好排除了「本地块设备等待」，把方向推向 cgroup 节流、唤醒风暴，或**网络文件系统**（NFS 等待不计入 `iowait`）。这一问考的就是 `wa` 的口径。
</details>

**Q2**（计算 + 辨析）某 NVMe SSD 的 `iostat -x` 为 `w/s = 4000`、`w_await = 3.5`（ms）、`aqu-sz = 14.0`、`%util = 100.00`。请计算写吞吐与每个请求的平均停留时间，判断是否已饱和，并说明 `%util = 100%` 能否作为瓶颈证据。
<details><summary>答案与解析</summary>

写 IOPS = `w/s = 4000`。由 Little 定律 `aqu-sz ≈ 到达率 × 停留时间`：`Q = 4000 /s × 3.5 × 10⁻³ s = 14.0`，与报告的 `aqu-sz` 一致——说明报告数字自洽可信，可以放心用它判断。设备服务时间（不含排队）无法从这些列直接得出，必须与**基线**比较：若同型号设备在 `aqu-sz ≈ 1`、`w_await ≈ 0.1 ms` 时也能达到 4000 IOPS，那么现在这 3.5 ms 里约 3.4 ms 是排队，设备**已经饱和**——它跑在吞吐上限附近，再加负载只会增加延迟而不再增加 IOPS。

`%util = 100%` **不能**单独作为瓶颈证据。它的定义是「设备至少有一个 I/O 在处理的时长占比」，`aqu-sz = 14` 时设备当然每刻都有在途请求，所以 `%util` 必然 100%——这是**定义上的必然**，不携带「还有没有余量」的信息。一块能并发 64 个请求的 NVMe 在 `aqu-sz = 14` 时可能仍有余量，`%util` 依然是 100%。

错在哪：用 `%util = 100%` 直接下结论，答不出它的定义；以及忽略 `aqu-sz` 与 `await` 的自洽性检验（虚拟化逻辑卷上这两值常不自洽）。面试官会追问「你怎么知道设备上限」——用 `fio` 压出该型号的 IOPS-延迟曲线做基线（见第 13 章 13.5 节）。
</details>

**Q3**（推导）`free -m` 显示 `total = 16384`、`used = 15800`、`free = 120`、`buff/cache = 464`、`available = 380`。请判断是否内存不足，并说明为什么 `available` 与 `free` 差得这么小。
<details><summary>答案与解析</summary>

这是内存不足的信号。`available = 380 MB` 约占 `total` 的 2.3%；更要紧的是 `buff/cache` 只剩 464 MB——正常运行的 Linux 上 page cache 通常能占内存一半以上，`cache` 被压到这么低说明**回收（reclaim）一直在工作**，反复释放缓存去满足分配。

两者关系是 `available ≈ free + 大部分可回收 page cache 与 slab − 内核保留的低水位`。正常时 `available` 远大于 `free`（因为 cache 很大）；这里只差 260 MB，说明可回收部分几乎被榨干，`available` 退化成「`free` 加一点点」。**这个差值本身就是压力指标**，比绝对值更早预警。还要补两件事：是否**正在**换页（`vmstat 1` 的 `si`/`so` 持续非零才是铁证，`Swap used` 非零不能证明什么）；回收压力与 OOM 风险（`/proc/pressure/memory` 的 `avg10`、`dmesg -T | grep -i oom`、容器内 `memory.events` 的 `oom_kill`，见第 12 章 12.5 节）。错在哪：只答「`free` 小是正常的，要看 `available`」——通则本身对，但本例 `available` 也小，结论应是内存不足。这是把正确的通则套到不满足其前提的数据上。面试官会追问「`available` 怎么算」：内核 `si_mem_available()` 从 `free` 出发，加上可回收 page cache 的绝大部分与可回收 slab 的一部分，再减去 `min_free_kbytes` 等保留量；它是**估算值**，判据看趋势与量级。
</details>

**Q4**（设计）一个 Java 服务在容器中运行，容器内 CPU 稳定 200%（配额 2 核），吞吐比物理机低 40%，而宿主机 `mpstat -P ALL 1` 显示多核 `%steal` 在 8% ~ 15% 波动、`%idle` 仍有余量。请给出排查路径与处置建议，并说明为什么「优化 Java 代码」在这里无效。
<details><summary>答案与解析</summary>

先分清两个独立的限流来源。**来源一：cgroup CPU 配额。** 容器内 CPU 顶在 200%（正好是 `cpu.max` 的 2 核）。验证：`cat /sys/fs/cgroup/cpu.max`（v2 格式 `quota period`，如 `200000 100000` 即 2 核）；`cpu.stat` 的 `nr_throttled` 与 `throttled_usec` 是否**持续增长**——只有持续增长才说明真被节流。此时容器内负载（`R`）很高而整机 `id` 仍有空闲，正是 11.2.3 节第三种成因。

**来源二：宿主机 steal time。** `%steal` 8% ~ 15% 意味着 vCPU 可运行但物理 CPU 没分给它。这部分损失**在容器内任何指标上都看不出来**（表现为「时间变慢了」而非「被等待了」），只能从宿主机或云厂商监控读到；`%idle` 仍有余量说明不是物理 CPU 全满，而是虚拟机与其他租户争抢（超卖）。处置：先在容器内确认节流（`cpu.stat`、`/proc/pressure/cpu`）；再在宿主机确认 steal 分布——集中在少数核指向亲和/超线程争抢，均匀分布指向整体超卖；配额不足就提高 `cpu.max`，超卖导致的偷取只能靠换宿主机、换规格、迁移独享型实例。

**为什么优化代码无效**：两个来源都在应用之外——配额是 cgroup 调度器在周期边界拒绝给 CPU，steal 是宿主机虚拟化层不给物理 CPU。代码优化得再快，`%steal` 那几十毫秒里它也执行不到指令。**能优化的是「在给定算力下做更多事」**（减少系统调用与切换、减少 GC 停顿），但「把被偷走的时间拿回来」超出应用权限边界。错在哪：看到 CPU 200% 就断言「CPU 打满了，加机器」；正确顺序是先分清「被自己算满」还是「被配额或宿主机限住」。面试官会追问「`%steal` 在容器里能看到吗」——不能：它由宿主机虚拟化层写入 Guest 内核的 per-CPU 统计，容器共享同一内核。
</details>

**Q5**（推导 + 设计）`vmstat 1` 持续显示 `cs = 400000`、`in = 120000`，而 `us = 12%`、`sy = 18%`；`pidstat -w -t` 显示某进程 `cswch/s` 高达 30 万/秒、`nvcswch/s` 只有几百。请判断属于哪一类问题，给出把范围收窄到「具体是哪把锁」的命令序列，并说明为什么这里不该优先调内核参数。
<details><summary>答案与解析</summary>

判断：**自愿上下文切换占绝对多数**（30 万 vs 几百），说明任务不是在抢 CPU（否则非自愿切换该高），而是在**反复主动睡眠等待某个资源**；CPU 合计仅 30%，排除了计算密集。为什么更可能是锁竞争而非普通 I/O 等待：`in` 也高而 `wa` 未升高，说明不是块设备等待；`cs`/`cswch` 的量级（几十万/秒）远超正常 I/O 等待频率。锁竞争的特征是**同一把锁上形成「睡眠 → 唤醒 → 抢不到 → 再睡眠」循环**，每次至少两次切换，切换率被放大到远超实际等待次数。

```bash
pidstat -w -u -t -p <PID> 1 5                                    # 线程级切换率与 CPU
bpftrace -e 'kprobe:futex_wait { @[kstack, ustack] = count(); }'  # futex 是用户态锁入口：统计锁地址与栈
perf lock record -p <PID> -- sleep 10 && perf lock report        # 内核视角的锁竞争统计
perf record -e sched:sched_switch -e sched:sched_wakeup -a -- sleep 5   # 配对出"睡眠-唤醒"对
offcputime -df -p <PID> 30 > offcpu.folded                       # off-CPU 火焰图：按栈聚合等锁时间
```

为什么不该先调内核参数：症状是**应用内部在锁上反复睡眠**，属于并发设计问题（临界区过长、锁粒度过粗、用全局锁保护本可分段的状态、CAS 重试风暴），改 `vm.max_map_count`、`vm.nr_hugepages` 之类参数不会减少任何一次 `futex_wait`；正确方向是缩小临界区、按业务键分段加锁、改用分片或无锁结构、或用批处理摊薄唤醒次数。错在哪：只看 `cs` 绝对值就断言「系统忙着切换所以要减线程数」——线程数不是根因，**每个线程为什么反复睡醒**才是；以及混淆自愿与非自愿切换，把「等锁」误判成「抢 CPU」。面试官会追问「你怎么证明是同一把锁」——统计 `futex_wait` 的**锁地址分布**或 `kstack`/`ustack` 聚合结果。
</details>
## 面试高频问题与陷阱

| 面试问法 | 考查角度 | 陷阱 / 加分点 | 对应小节 |
|---|---|---|---|
| 平均负载是什么意思？那三个数字分别代表什么？ | 负载口径 | 答「CPU 使用率」或「过去 1/5/15 分钟的算术平均进程数」直接扣分；必须说清「可运行 R + 不可中断 D 的任务数 + 指数衰减平均」，并主动补一句「要与核数相除才有意义」 | 11.2 |
| 负载很高但 CPU 使用率很低，你会怎么查？ | 高负载低 CPU | 只答「有进程在死循环」——那会推高 CPU；正确答法给三种成因：`D` 态 I/O 阻塞、线程频繁唤醒、cgroup 配额节流，每种配一条验证命令 | 11.2.3 |
| `wa` 很高是不是说明磁盘到瓶颈了？ | `iowait` 口径 | 答「是」是最典型的错；`wa` 是「CPU 空闲且有人在等**本地块设备** I/O」的时长，归因给 CPU 不归因给盘；还要能举出 NFS 等待不计入、CPU 忙时 `wa` 天然偏低这两个反例 | 11.3.3 |
| `%util` 到 100% 了，是不是磁盘瓶颈？ | `%util` 定义 | 答「是」会被立刻追问倒；说明它只是「设备至少有一个 I/O 在处理的时长占比」，不携带余量信息，判据应是 `await` 对比基线加 `aqu-sz` | 11.6.2 |
| `free` 显示内存快用完了，是不是要扩内存？ | 内存指标口径 | 答「`free` 小就危险」；正确答法是看 `available` 占 `total` 的比例，并解释 page cache 可回收、`buff/cache` 是合并列且 `shared` 已计入 `cache` | 11.5.1 |
| 你怎么判断系统是不是在用 swap？ | swap 的唯一可靠信号 | 答「看 `free` 的 `Swap used` 是不是 0」是错的；正确答法是 `vmstat` 的 `si`/`so` 是否持续非零，并说明 `Swap used` 只是存量、反映历史压力 | 11.5.2 |
| 你的服务 P99 突然变高但 P50 正常，怎么排查？ | 尾延迟归因 | 答「整体资源不足」——那会同时抬高 P50；正确思路是「只有部分请求命中某条慢路径」，用四板斧逐层收敛到具体阻塞点 | 11.9 |
| `perf top` 能看到进程在等锁吗？ | on-CPU 与 off-CPU | 答「能」是概念性错误；`perf top` 采样的是正在 CPU 上执行的栈，等锁/等 I/O 的时间不在任何 CPU 上，要用 off-CPU 火焰图或调度事件追踪 | 11.8 |
| `listen queue overflow` 涨了，是网络问题还是应用问题？ | accept 队列语义 | 归因为网络或内核参数；正确答法是「已完成三次握手的连接因 `accept()` 不及时被丢弃」，属于应用并发不足或 `accept` 循环被阻塞 | 11.7.2 |
## 延伸阅读

- `man 1 vmstat`、`man 1 iostat`、`man 1 pidstat`、`man 1 sar`、`man 1 mpstat`（`sysstat` 的官方列定义；`bi`/`bo` 的单位与 `aqu-sz` 的改名说明都在这里）。
- `man 5 proc`（`/proc/loadavg`、`/proc/stat`、`/proc/meminfo`、`/proc/PID/stack`、`wchan`、`schedstat` 的字段定义）、`man 8 ss`、`man 8 netstat`。
- Linux 内核源码：`kernel/sched/loadavg.c`（`calc_load` 与 `exp[]` 常量、`LOAD_FRAC_BITS`）、`mm/page_alloc.c` 的 `si_mem_available()`（`MemAvailable` 的估算口径）、`block/blk-core.c`（`iostat` 各列的计数器来源）。
- Brendan Gregg, *Systems Performance: Enterprise and the Cloud*（第 2 版第 2 章 USE 方法；第 4~10 章分别对应本章四类资源）、*BPF Performance Tools*（off-CPU 分析与 `bpftrace` 单行脚本目录）。
- Linux 内核文档：`Documentation/accounting/psi.rst`（`/proc/pressure/*` 的语义与阈值接口）、`Documentation/admin-guide/cgroup-v2.rst`（`cpu.stat` 的 `nr_throttled`/`throttled_usec`，`io.max` 与 `io.latency` 的差别）、`Documentation/trace/ftrace.rst`。
- `fio` 官方 HOWTO（`iodepth`、`bs`、`rw` 参数与 IOPS-延迟曲线的测法，与第 13 章 13.5 节配合阅读）。


---

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

