performance - debugging
11. 性能与排障
0. 本章先解决什么问题
操作系统知识最终要能落到排障:
CPU 为什么高?
程序为什么卡住?
内存为什么涨?
文件为什么慢?
网络为什么超时?
线程为什么很多但吞吐低?
本章不是列命令,而是建立 OS 级排查模型:
- 先判断瓶颈资源。
- 再判断线程状态。
- 再看内核路径和等待点。
- 最后回到应用逻辑。
这张图怎么读
这张图的意义是:OS 排障不是看一个指标,而是追踪资源生命周期。程序慢时,问的不是“哪个参数调大”,而是:
资源是谁申请的?
现在被谁持有?
谁在等待?
谁负责唤醒?
什么时候释放?
1. 先分类:CPU、内存、IO、锁、网络
性能问题先分类。
| 类别 | 典型表现 |
|---|---|
| CPU-bound | CPU 高,主要在计算 |
| memory-bound | cache miss、缺页、内存带宽瓶颈 |
| IO-bound | CPU 不高,大量等待文件/网络/设备 |
| lock-bound | 线程等待锁,吞吐低 |
| scheduling-bound | runnable 多,上下文切换高 |
| network-bound | connect/read/write/重传/缓冲区问题 |
不要一开始就改代码。先判断资源瓶颈。
2. CPU 高怎么拆
CPU 高可能来自:
- 正常计算。
- 死循环。
- 忙等。
- 自旋锁竞争。
- 频繁系统调用。
- 中断处理。
- 上下文切换。
- 内核网络栈或设备路径。
要问:
- CPU 时间在用户态还是内核态?
- 哪些线程最忙?
- 忙线程在做计算、系统调用、锁,还是中断?
- 是否有大量上下文切换?
- 是否 CPU 高但吞吐低?
用户态高通常看应用热点。内核态高要看系统调用、网络、设备、中断、锁等。
3. CPU 低但程序慢
CPU 不高但慢,常见原因是等待。
等待什么?
- IO。
- 锁。
- 队列。
- 远端服务。
- 定时器。
- 调度。
- 内存缺页。
典型:
线程大部分在 blocked
CPU 空闲
请求仍然慢
这不是 CPU 算力问题,而是等待路径问题。
4. 上下文切换高
上下文切换高可能来自:
- 线程太多。
- 任务太短。
- 频繁阻塞和唤醒。
- 锁竞争。
- IO 事件频繁。
- 时间片竞争。
影响:
- CPU 花时间切换,不做有效工作。
- Cache/TLB 局部性变差。
- 延迟抖动。
排查:
线程数
runnable 数量
阻塞唤醒频率
锁等待
系统调用频率
5. 内存问题怎么拆
内存高不等于一定泄漏。
可能是:
- 堆增长。
- 线程栈多。
- mmap 多。
- 页缓存增长。
- 共享内存。
- 外部内存。
- 内核缓冲区。
- 容器限制。
要问:
- 是单进程高,还是系统整体高?
- 是虚拟地址空间高,还是常驻内存高?
- 是否持续增长不回落?
- 是否发生缺页或 swap?
- 是否 OOM kill?
- 是否读写大量文件导致页缓存高?
6. IO 慢怎么拆
文件 IO 慢可能来自:
- 页缓存未命中。
- 磁盘慢。
- 小 IO 太多。
- 同步刷盘。
- 文件系统元数据。
- 设备队列长。
网络 IO 慢可能来自:
- DNS。
- connect。
- 发送缓冲区满。
- 接收缓冲区堆积。
- 对端慢。
- 重传或丢包。
- event loop 阻塞。
- 线程池耗尽。
IO 排查要按路径拆,不要一句“IO 慢”结束。
7. 锁等待和死锁
锁问题常见现象:
- 吞吐下降。
- CPU 不高但请求慢。
- 大量线程 blocked。
- 某线程持锁时间长。
- 多线程互相等待。
排查步骤:
- 找等待线程。
- 找它等的锁。
- 找锁持有者。
- 看持有者在做什么。
- 画等待关系图。
- 检查是否有环。
等待图有环,就是死锁方向。
8. 队列堆积
很多系统问题本质是队列堆积。
请求到达速度 > 处理速度
队列可能在:
- 应用任务队列。
- 线程池队列。
- 内核 socket buffer。
- 磁盘队列。
- 设备队列。
- 下游服务队列。
队列增长会带来:
- 延迟升高。
- 内存增长。
- 超时。
- 丢弃。
- 级联故障。
9. 用户态和内核态证据
排障要区分:
| 证据 | 说明 |
|---|---|
| 应用日志 | 业务路径和状态 |
| 线程栈 | 线程当前卡在哪里 |
| 系统指标 | CPU、内存、IO、网络 |
| 系统调用跟踪 | 是否频繁进入内核或阻塞 |
| 网络抓包 | 真实传输情况 |
| 文件系统指标 | IO 等待、队列、吞吐 |
| 进程资源 | fd、线程数、内存映射 |
不要用单一证据下结论。要形成证据链。
10. 联系实际:请求超时怎么按 OS 拆
现象:
请求偶尔 3 秒超时
拆解:
- 请求进入应用了吗?
- 处理线程拿到 CPU 了吗?
- 是否排队等线程池?
- 是否等锁?
- 是否读文件或网络阻塞?
- 是否系统调用卡住?
- 是否发生缺页或 swap?
- 是否 event loop 被阻塞?
- 是否 socket buffer 堆积?
- 是否对端慢或网络重传?
最终结论要能说:
时间花在哪一段
证据是什么
瓶颈资源是什么
修复后如何验证
证据链模板:不要只写“怀疑是 IO”
一个合格的性能结论至少包含四层:
| 层 | 要写什么 | 例子 |
|---|---|---|
| 现象 | 用户或系统看到什么 | P99 从 120ms 升到 3s |
| 分段 | 时间主要花在哪段 | 队列等待 2.4s,实际处理 80ms |
| 资源 | 哪个资源或边界饱和 | worker 队列堆积,线程都在等下游 IO |
| 证据 | 用什么证明 | 线程栈、队列长度、下游耗时、时间戳 |
| 验证 | 改完怎么证明 | 队列长度下降,P99 回落,错误率不升 |
写成句子就是:
请求慢不是 CPU 计算慢,而是进入 worker 前排队。
证据是入口时间戳到 worker-start 的差值占总耗时 80%,
同时 worker 队列长度持续增长,线程栈显示多数 worker 在等待下游 IO。
这类结论比“可能是 IO 慢”更接近工程事实。
再加一层证据表:
| 证据 | 说明 |
|---|---|
| CPU 高且用户态高 | 更像应用计算热点 |
| CPU 高且内核态高 | 系统调用、网络栈、设备、中断方向 |
| CPU 低但请求慢 | 等待 IO、锁、队列、远端响应 |
| 上下文切换高 | 线程太多、频繁阻塞唤醒、锁竞争 |
| 内存持续涨 | 泄漏、缓存无上限、对象生命周期异常 |
| 缺页或 swap 明显 | 工作集超过内存或访问模式差 |
| CLOSE_WAIT 多 | 应用没有关闭 socket |
| TIME_WAIT 多 | 短连接和主动关闭频繁,需要判断是否端口压力 |
这类表格适合放在 blog 末尾,读者遇到问题时可以直接反查。
排障卡:一分钟性能分诊表
性能问题先做分诊,再做深挖:
| 证据 | 初步判断 | 下一步 |
|---|---|---|
| CPU 高且调用栈集中 | 计算或热循环 | 找热点函数和重复工作 |
| CPU 低但响应慢 | 等待 IO、锁、网络或队列 | 看等待状态和队列长度 |
| 上下文切换高 | 线程过多或频繁阻塞唤醒 | 看线程数、锁、IO 模型 |
| 内存持续增长 | 泄漏、缓存无界或工作集扩大 | 看对象生命周期和可回收性 |
| 尾延迟高但平均正常 | 峰值、抖动、排队 | 看 P95/P99、批量任务、扩容点 |
练习:对一个超时请求写四个时间戳:进入队列、开始处理、开始 IO、返回结果。只要这四个点能记录下来,性能排查就从“整体慢”变成“哪段时间变长”。
失败指纹表:OS 视角下的性能症状
| 现象 | 更像哪类瓶颈 | 关键证据 | 容易误判成 |
|---|---|---|---|
| CPU 100%,响应变慢 | 计算热点、忙等、频繁系统调用 | 用户态/内核态 CPU 占比、热点栈、syscall 频率 | 机器不够强 |
| CPU 很低,但请求排队 | IO 等待、锁等待、队列堵塞 | 线程状态、IO wait、锁等待时间、队列长度 | 代码没有运行 |
| 平均延迟正常,P99 很差 | 峰值扩容、刷盘、回收、批任务 | 分位数、周期性任务、扩容/flush 时间点 | 网络偶发异常 |
| 上下文切换异常高 | 线程过多、锁竞争、阻塞唤醒频繁 | context switch、线程数、锁竞争、调度等待 | CPU 算不过来 |
| 内存足够但仍然慢 | 工作集大、cache/TLB miss、页缓存抖动 | 缺页、cache miss、工作集大小、页缓存变化 | 没有内存问题 |
| IO 吞吐不高但等待很长 | 设备队列、同步刷盘、小 IO 太多 | IO 队列深度、请求大小、fsync 次数、设备延迟 | 磁盘带宽不够 |
11. 学完本章你能解决什么问题
学完这一章,你应该能解决或开始分析这些问题:
- 性能问题为什么要先分类资源瓶颈?
- CPU 高可能有哪些 OS 方向?
- CPU 不高但程序慢通常在等什么?
- 上下文切换高为什么会降低吞吐?
- 内存高为什么不一定是堆泄漏?
- 文件 IO 和网络 IO 慢分别如何拆路径?
- 锁等待和死锁如何通过等待图分析?
- 队列堆积为什么会放大延迟?
- 请求超时如何建立 OS 级证据链?
OS 排障的核心是:找到线程在什么状态、资源卡在哪里、数据停在哪一层。