﻿# 用 perf 初步分析多线程 + IO 混合程序

> 一个多线程程序同时做计算和 IO，性能有问题时该从哪查起？本文给出一套**三步法**：先用 `perf stat` 做宏观体检，再按线程拆分找出 CPU 型和 IO 型线程各自的问题，最后针对异常模式深入。不要求事先知道程序瓶颈在哪——用这套方法可以**逐层缩小范围**，判断程序有没有明显问题。

> 关联：[perf.md](/tools/code/perf.md)（perf 命令参考）、[perf-advanced.md](/concepts/tools/perf-advanced.md)（`perf lock`/`perf sched`/`perf c2c`）、[perf-sched.md](/concepts/tools/perf-sched.md)（`perf sched` 完全指南）、[pidstat.md](/tools/cpu/pidstat.md)（`-t` 按线程拆分）、[context-switch.md](/concepts/process/context-switch.md)（上下文切换机制）、[pmu.md](/concepts/cpu/pmu.md)（PMU 采样原理）、[compare.md](/tools/compare.md)（perf vs 传统工具定位对比）

---

## 零、一句话结论

**三步法**：`perf stat` 四字段定类型（CPU 型 vs IO 型 vs 混合型）→ `pidstat -t` + `perf record` 拆线程定位责任人 → 按异常模式选择深入工具（`perf record` 抓热点 / `perf lock` 看锁 / `perf sched` 看调度 / `strace` 看 IO syscall），15 分钟内判断多线程+IO 程序有没有明显性能问题。

---

## 一、多线程 + IO 程序长什么样

在实际场景中，大多数"有 IO 的多线程程序"都符合以下模式：

```plantuml
@startuml
skinparam shadowing false
skinparam rectangle {
  RoundCorner 10
}
rectangle "多线程 + IO 程序" as APP {
  rectangle "IO 线程组\n(读 socket / 读文件 / epoll_wait)\n→ 大部分时间阻塞在 syscall" as IO_THREADS #E3F2FD
  rectangle "计算线程组\n(解析协议 / 序列化 / hash / 压缩)\n→ 大部分时间在 user 态计算" as CPU_THREADS #FFECB3
  rectangle "协调线程\n(调度 / 定时器 / 日志)\n→ 少量 CPU + wait/sleep" as MISC #E8F5E9
  CPU_THREADS -[#FF8F00]-> IO_THREADS : 处理完交给 IO 线程发送
  IO_THREADS -[#1565C0]-> CPU_THREADS : 收到数据交给计算线程
}
note right of APP
  典型例子：
  - Web 服务器 (epoll + worker pool)
  - 数据库 (IO 线程读盘 + 计算线程执行查询)
  - 量化交易 (行情接收线程 + 策略计算线程)
  - 流处理 (Kafka consumer + 业务处理)
end note
@enduml
```

**核心矛盾**：IO 线程"在等"（阻塞在内核态），CPU 线程"在算"（烧用户态），两类线程的行为完全不同，不能用同一个指标一刀切。这也是为什么"多线程+IO"的分析比纯计算或纯 IO 程序复杂——你得分清**谁在忙**和**谁在等**。

---

## 二、三步分析法总览

```bash
perf stat -p <PID> -- sleep 10          ──→  第一步：宏观体检（10 秒）
      │
      ├─── 问题出在哪类线程？
      │
pidstat -t -u -w -p <PID> 1            ──→  第二步：按线程拆分
      │
      ├─── CPU 型线程 → perf record 抓热点
      ├─── IO 型线程  → strace 看阻塞在哪个 syscall
      ├─── 锁竞争     → perf lock
      ├─── 调度不均衡  → perf sched
      └─── 伪共享     → perf c2c
                                       ──→  第三步：按异常模式深入
```

**快速启动命令（复制粘贴即用）**：

```bash
# 第一步：宏观体检（10 秒）
perf stat -p <PID> -- sleep 10 2>&1 | tee perf_stat.log
# 第二步：按线程拆分（5 秒采样，观察足够）
pidstat -t -u -w -p <PID> 1
```

下文逐步展开每一条命令输出怎么读、怎么判断。

---

## 三、第一步：`perf stat` 宏观体检

### 3.1 最简四字段：定位程序类型

先跑最简约的 `perf stat`，只看四个指标：

```bash
perf stat -p <PID> \
  -e task-clock,context-switches,cpu-migrations,page-faults \
  -e cycles:u,cycles:k,instructions:u,instructions:k \
  -- sleep 10
```

| 指标 | 作用 | 正常 / 异常信号 |
|------|------|----------------|
| `task-clock` | 该进程累计消耗了多少 CPU 时间（ms） | `task-clock / (elapsed × 核数)` → 实际 CPU 利用率 |
| `context-switches` | 上下文切换总次数 | < 100/s 正常；> 1000/s → 调度过热 |
| `cycles:u` / `cycles:k` | **用户态** vs **内核态** cycle 占比 | `cycles:k / (cycles:u + cycles:k)` = 内核开销比例 |
| `instructions:u` / `instructions:k` | 用户态 vs 内核态指令数 | 结合 cycles 算 IPC，分态对比 |

### 3.2 四字段快速分诊

拿到四字段数据后，对照下表判断程序类型：

```plantuml
@startuml
title perf stat 四字段 → 程序类型分诊
start
:perf stat -p <PID>
cycles:u / cycles:k / instructions:u / instructions:k;
if (cycles:k 占比 > 40%?) then (是)
  if (context-switches > 1000/s?) then (是)
    :**系统调用风暴 + 调度过热**
→ syscall-heavy，用 strace -c 看哪个最频繁
→ 可能是每次 read/write 字节太小;
  else (否)
    :**内核态计算型**
→ 可能大量 page fault / 中断处理
→ 加 -e page-faults 再查;
  endif
else (否 → cycles:k < 40%)
  if (IPC = instructions:u / cycles:u < 0.5?) then (是)
    :**低效计算型**
→ IPC < 0.5 说明 CPU 在空转等内存
→ 用 perf record 抓热点 + 看 cache-misses;
  else (否)
    if (context-switches > 500/s?) then (是)
      :**高并发竞争型**
→ 线程太多或锁竞争激烈
→ 用 pidstat -t -w 拆线程 + perf lock;
    else (否)
      :**无明显问题**
→ IPC 合理、内核占比小、切换少
→ 瓶颈可能在 IO 等待（线程在等，不费 CPU）
→ 进入第二步查 IO 型线程;
    endif
  endif
endif
stop
@enduml
```

### 3.3 扩展事件：进一步确认方向

如果四字段给出了大致方向，再加一组事件确认：

| 追加目的 | 追加事件 | 正常值 | 异常含义 |
|----------|---------|:---:|------|
| 确认是否内存瓶颈 | `cache-misses, cache-references` | miss 率 < 5% | > 10% → 数据布局差，内存子系瓶颈 |
| 确认是否流水线空转 | `stalled-cycles-frontend, stalled-cycles-backend` | — | frontend 高 → 取指瓶颈；backend 高 → 数据瓶颈 |
| 确认是否分支预测失败 | `branch-misses, branches` | miss 率 < 1% | > 5% → 代码有大量不可预测分支（数据依赖型 if/switch） |
| 区分自愿/非自愿切换 | `pidstat -w 1` 的 `cswch/s` / `nvcswch/s` | — | voluntary 高 → 主动睡眠（IO/锁等待）；involuntary 高 → CPU 竞争 |

```bash
# 完整微观体检命令（一行到位，10 秒）
perf stat -p <PID> \
  -e task-clock,context-switches,cpu-migrations,page-faults \
  -e cycles:u,cycles:k,instructions:u,instructions:k \
  -e cache-misses,cache-references \
  -e branch-misses,branches \
  -e stalled-cycles-frontend,stalled-cycles-backend \
  -- sleep 10
```

> **这一步的定位价值**：10 秒之内，你能判断程序的瓶颈在大方向上是"CPU 算不动"、"内存拖后腿"、"IO 在等"还是"线程在打架"。这决定了第二步的排查方向。

---

## 四、第二步：按线程拆分——谁是 CPU 型，谁是 IO 型

多线程程序的根本特征是**线程异构**。`perf stat` 给的是聚合数据，需要拆开看。

### 4.1 `pidstat -t`：逐线程体检

```bash
# -t: 显示线程  -u: CPU 使用率  -w: 上下文切换  -d: IO 延迟
pidstat -t -u -w -d -p <PID> 1
```

看每行的三列：

| 列 | 正常 | 异常 | 含义 |
|----|------|------|------|
| `%CPU` | < 100 | 100 | 此线程吃满了一个核 |
| `%usr` vs `%system` | 看比例 | `%system` > 30% | 此线程频繁进内核 |
| `cswch/s` | < 100 | > 1000 | 此线程频繁主动放弃 CPU（IO 等待 / sleep / lock） |
| `nvcswch/s` | < 10 | > 500 | 此线程频繁被抢占（CPU 竞争） |
| `iodelay`（需 `-d`） | ≈ 0 | > 几十 ms | 此线程在等磁盘 |

**快速分诊**：

```bash
pidstat -t 每一行就是一个线程，快速扫一遍：
  某线程 %CPU ≈ 100, %usr 很高   →  CPU 型线程 → 用 perf record --tid <TID> 抓热点
  某线程 %CPU < 10, cswch/s > 1000 →  IO 型线程 → 用 strace -p <TID> 看阻塞在哪个 syscall
  某线程 cswch/s > 5000           →  锁竞争/条件变量 → 用 perf lock
  多个线程 %CPU ≈ 50, cswch/s 高   →  激烈 CPU 竞争 → 线程数 >> 核数
  某线程 %system > 50%            →  内核态占主导 → strace -c 看 syscall 频次
```

### 4.2 `perf record --tid`：按线程抓热点

找到 CPU 型线程的 TID 后，单独采样：

```bash
# 抓某个 CPU 型线程的调用栈（10 秒）
perf record -g -F 99 --tid <TID> -- sleep 10
# 生成报告
perf report --stdio | head -50
```

> 也可以用 `perf top -p <PID>` 看实时热点分布——如果某个函数占了 30%+ 的采样点，它就是瓶颈。

### 4.3 IO 型线程：`strace` 看等在哪

```bash
# -T: 打印每个 syscall 的耗时（微秒）
# -c: 结束后出统计表
strace -T -c -p <TID> -o strace.log &
sleep 10
kill %1
cat strace.log
```

关注 `% time` 列和 `seconds` 列：

| 情况 | 含义 |
|------|------|
| `epoll_wait` / `poll` 耗时 99% | 线程在等事件，正常（IO 线程就该等） |
| `read` / `write` 耗时高 | IO 慢——可能 disk IO 或网络带宽不足 |
| `futex` 耗时高 | 锁竞争——线程在等锁 |
| `nanosleep` / `clock_nanosleep` | 代码里有显式 sleep，可能没必要 |

---

## 五、第三步：按异常模式深入

### 5.1 模式 A："CPU 满载 + IPC 极低"——算不动

**症状**：多个线程 `%CPU ≈ 100%`，但 `perf stat` 显示 `IPC < 0.5`。

**根因**：CPU 在跑，但大量时间花在等内存（cache miss）或等流水线清空（分支预测失败）。

**深入命令**：

```bash
# 确认 cache miss 率
perf stat -e cache-misses,cache-references -p <PID> -- sleep 10
# 抓热点并做汇编级分析
perf record -g -F 99 -p <PID> -- sleep 10
perf annotate -d <热点函数名>
# 检查是否伪共享（多线程写相邻变量）
perf c2c record -p <PID> -- sleep 10
perf c2c report
```

**判断决策**：

| IPC | 含义 | 行动 |
|:---:|------|------|
| > 2.0 | 计算非常高效 | 要加速只能换算法/加核 |
| 1.0~2.0 | 正常 | 热点函数可能是合理计算 |
| 0.5~1.0 | 有优化空间 | 看热点函数是否可算法优化 |
| < 0.5 | CPU 严重空转 | 必查 cache miss / 数据布局 / 伪共享 |

> 详见 [案例一](/concepts/tools/perf-case-studies/01-mode1-o0-baseline.md) §1.4.2，IPC 0.61 的深层分析。

### 5.2 模式 B："所有线程 CPU 都在 30%~50%"——线程过多 / 锁竞争

**症状**：CPU 整体不低但没一个线程跑满，各线程 `%CPU` 都在 30%~60%。

**根因**：
- 线程数远大于 CPU 核数，调度器频繁切换（非自愿切换为主）
- 锁竞争严重，大量线程在等锁（自愿切换为主）

**深入命令**：

```bash
# 区分自愿 vs 非自愿切换
pidstat -t -w -p <PID> 1
# 非自愿切换高 → 线程太多
# 自愿切换高 → 可能有锁竞争 → 下一步
perf lock record -p <PID> -- sleep 10
perf lock report
# 看调度延迟（线程在 runqueue 等了多久）
perf sched record -p <PID> -- sleep 10
perf sched latency
```

**线程数 vs CPU 核数速查**：

```bash
# 看进程有多少线程
cat /proc/<PID>/status | grep Threads
# 看系统有多少核
nproc
```

| 线程数 : 核数 | 判断 |
|:---:|------|
| ≤ 1.5:1 | 线程数合理 |
| 2:1 ~ 4:1 | 部分线程在等 IO，可以接受 |
| > 5:1 | 线程过多，调度开销大 |

### 5.3 模式 C："context-switches > 1000/s + voluntary 为主"——IO 型线程在狂调 syscall

**症状**：`perf stat` 显示 `context-switches` 很高 → `pidstat -w` 显示 `cswch/s`（voluntary）占主导。

**根因**：每次 read/write/epoll_wait/sleep 都是一次系统调用，每个系统调用都可能触发调度。

**深入命令**：

```bash
# 看哪个 syscall 调用最频繁
strace -c -p <PID> -o strace.log &
sleep 10
kill %1
head -20 strace.log
# 看是哪个线程在频繁 syscall
pidstat -t -u -w -p <PID> 1
# 看内核态都花在哪了
perf record -e 'syscalls:sys_enter_*' -p <PID> -- sleep 10
perf report
```

**常见优化方向**：

| 症状 | 方向 |
|------|------|
| `read(4096)` 几万次/秒 | 增大 buffer 或改用 `readv`/`splice` |
| `write(少量字节)` 极高频 | 用户态 buffer 攒一批再写，或用 `sendmmsg` |
| `futex` 极高频 | 锁竞争 → 回到模式 B |
| `epoll_ctl` 极高频 | 减少 epoll fd 的增删频率，或改用 `io_uring` |

### 5.4 模式 D："内核态 cycles 占比 > 40%"——IO 或内核路径是瓶颈

**症状**：`cycles:k / (cycles:u + cycles:k) > 0.4`。

**根因**：
- 纯 syscall-heavy 场景（每个请求触发几次 syscall）
- page fault 频繁（mmap + 突然访问大量页）
- 中断处理过多（网卡收发包走软中断）

**深入命令**：

```bash
# 看内核态最热函数
perf record -g -p <PID> -- sleep 10
perf report --stdio | head -30  # 看带 [k] 标记的内核函数
# 看 page fault 是否异常
perf stat -e page-faults -p <PID> -- sleep 10
# 区分 minor vs major fault
pidstat -r -p <PID> 1  # 看 minflt/s 和 majflt/s
```

**判断**：

| 情况 | 含义 |
|------|------|
| `cycles:k` 高 + syscall 频次高 | 减少 syscall 次数（批量 IO、mmap、io_uring） |
| `cycles:k` 高 + `page-faults` 急剧上升 | 内存映射/分配方式有问题 |
| `cycles:k` 高 + `%soft` 高（mpstat） | 网卡中断集中在少数核 |

### 5.5 模式 E："个别核跑满、其余核空闲"——单线程瓶颈 / 锁不均衡

**症状**：`mpstat -P ALL 1` 显示某几个核 100%，其余核 < 10%。

**根因**：热点线程没有分散，或者互斥锁保护了关键路径，其他线程都在等。

**深入命令**：

```bash
# 找跑满的单核上是谁
mpstat -P ALL 1  # 先看哪颗核满
ps -eo pid,tid,psr,comm | grep <PID>  # 看哪些线程在热核上
pidstat -t -u -p <PID> 1  # 确认哪个线程 CPU 高
# 看是不是锁在作祟
perf lock record -p <PID> -- sleep 10
perf lock report
```

**含义**：如果跑满的核上只有一个关键线程，说明程序是单线程瓶颈——加速方向是**减少关键线程的每单位工作量**或**拆分关键路径**，加更多 worker 线程没用。

---

## 六、异常模式速查总表

| # | 症状组合 | 最可能原因 | 先跑这个命令 | 再看这个 |
|---|---------|-----------|-------------|---------|
| A | 多线程 100% CPU + IPC < 0.5 | 低效计算（cache miss / 伪共享 / 分支预测失败） | `perf record -g` → 火焰图 | `perf annotate` / `perf c2c` |
| B | 多线程 30%~50% CPU + 切换多 | 线程过多 或 锁竞争 | `pidstat -t -w` | `perf lock` / `perf sched latency` |
| C | 切换 > 1000/s + voluntary 为主 | 频繁 IO syscall / sleep | `strace -c` | `pidstat -t` 找具体线程 |
| D | `cycles:k` > 40% | syscall 风暴 / page fault / 中断 | `perf record -g` 看 `[k]` 函数 | `pidstat -r` 查 page fault |
| E | 个别核 100% + 其余核空闲 | 单线程瓶颈 / 锁集中在一条路径 | `mpstat -P ALL` + `pidstat -t` 找热线程 | `perf lock` 确认 |
| F | CPU 不饱和 + context-switches 低 | IO 等待（线程阻塞在磁盘/网络） | `pidstat -t -d` 看 `iodelay` | `strace -p <TID>` 看阻塞点 |
| G | CPU 100%、IPC 正常、吞吐仍低 | 算法本身复杂度高 | `perf record -g` 看热点函数 | 改算法 / 改数据结枟 |
| H | LLC miss 率 > 20% | 工作集溢出 L3 cache / NUMA 远程访存 | `perf stat -e LLC-loads,LLC-misses` | `numastat` / `perf c2c` |

---

## 七、完整分析脚本（15 分钟版本）

按以下顺序执行，每步 2-5 分钟，拿到全部基础数据后再分析：

```bash
# ═══ 准备工作 ═══
PID=<填入目标进程 PID>
OUTDIR=perf_analysis_$(date +%Y%m%d_%H%M%S)
mkdir -p $OUTDIR
# ═══ 1. 宏观体检 (2 min) ═══
perf stat -p $PID \
  -e task-clock,context-switches,cpu-migrations,page-faults \
  -e cycles:u,cycles:k,instructions:u,instructions:k \
  -e cache-misses,cache-references,branch-misses,branches \
  -e stalled-cycles-frontend,stalled-cycles-backend \
  -- sleep 10 2>&1 | tee $OUTDIR/01_perf_stat.txt
# ═══ 2. 按线程拆分 (2 min) ═══
pidstat -t -u -w -d -p $PID 1 5 2>&1 | tee $OUTDIR/02_pidstat.txt
# ═══ 3a. 抓热点函数 (若 CPU 型线程存在) (3 min) ═══
perf record -g -F 99 -p $PID -- sleep 10 2>&1
perf report --stdio --no-children | head -40 | tee $OUTDIR/03_perf_report.txt
# ═══ 3b. 看 syscall 频次 (若 IO 型线程存在) (2 min) ═══
timeout 10 strace -c -p $PID -o $OUTDIR/04_strace.txt 2>&1
# ═══ 3c. 锁竞争分析 (若切换高) (3 min) ═══
perf lock record -p $PID -- sleep 5 2>&1
perf lock report 2>&1 | tee $OUTDIR/05_perf_lock.txt
# ═══ 3d. NUMA/核分布 (1 min) ═══
mpstat -P ALL 1 3 2>&1 | tee $OUTDIR/06_mpstat.txt
# ═══ 全部数据在 $OUTDIR/ 下 ═══
echo "分析数据已保存到: $OUTDIR"
```

---

## 八、快速判断：程序有没有明显问题？

对着收集到的数据，回答 6 个问题——如果全部 "否"，程序状态健康：

| # | 问题 | 看哪个数据 | 是 = 有问题 |
|---|------|----------|------------|
| 1 | 有没有线程空转 CPU 但 IPC < 0.5？ | `perf stat` IPC + `pidstat -t %CPU` | **模式 A** |
| 2 | 上下文切换 > 500/s/线程？ | `pidstat -t -w` 的 `cswch/s` | **模式 C** |
| 3 | 内核态 cycles > 40%？ | `cycles:k / (cycles:u+cycles:k)` | **模式 D** |
| 4 | 有核跑满、有核完全空闲（差 > 3×）？ | `mpstat -P ALL` | **模式 E** |
| 5 | LLC cache miss 率 > 10%？ | `perf stat -e LLC-loads,LLC-misses` | **模式 H** |
| 6 | 有线程 `iodelay` 持续 > 100ms？ | `pidstat -t -d` | **模式 F** |

> **评价**：如果 6 项全部正常，说明程序在多线程调度和 IO 层面没有明显结构性问题。如果吞吐仍不达标，瓶颈在业务逻辑算法本身——用 `perf record` 找到消耗最高的函数，从算法层面优化。

---

## 九、一句话总结

> **`perf stat` 四字段定类型（CPU 型 / IO 型 / 混合型）→ `pidstat -t` 拆线程分 CPU/IO 两类 → 异常模式对照表选深入工具**，15 分钟拿到多线程+IO 程序的完整诊断报告，6 个判断题决定有没有明显性能问题。

---

## 十、配套 demo

- [mt-io-demo 配套实验](/demos/mt-io-demo/experiment.md)——可运行程序说明 + 完整实验指南：参数一览、三步法诊断、四轮对比数据与分析结论

