performance - debugging

11. 性能与排障

0. 本章先解决什么问题

操作系统知识最终要能落到排障:

CPU 为什么高?
程序为什么卡住?
内存为什么涨?
文件为什么慢?
网络为什么超时?
线程为什么很多但吞吐低?

本章不是列命令,而是建立 OS 级排查模型:

  • 先判断瓶颈资源。
  • 再判断线程状态。
  • 再看内核路径和等待点。
  • 最后回到应用逻辑。

OS 资源生命周期

这张图怎么读

这张图的意义是:OS 排障不是看一个指标,而是追踪资源生命周期。程序慢时,问的不是“哪个参数调大”,而是:

资源是谁申请的?
现在被谁持有?
谁在等待?
谁负责唤醒?
什么时候释放?

1. 先分类:CPU、内存、IO、锁、网络

性能问题先分类。

类别典型表现
CPU-boundCPU 高,主要在计算
memory-boundcache miss、缺页、内存带宽瓶颈
IO-boundCPU 不高,大量等待文件/网络/设备
lock-bound线程等待锁,吞吐低
scheduling-boundrunnable 多,上下文切换高
network-boundconnect/read/write/重传/缓冲区问题

不要一开始就改代码。先判断资源瓶颈。

2. CPU 高怎么拆

CPU 高可能来自:

  • 正常计算。
  • 死循环。
  • 忙等。
  • 自旋锁竞争。
  • 频繁系统调用。
  • 中断处理。
  • 上下文切换。
  • 内核网络栈或设备路径。

要问:

  1. CPU 时间在用户态还是内核态?
  2. 哪些线程最忙?
  3. 忙线程在做计算、系统调用、锁,还是中断?
  4. 是否有大量上下文切换?
  5. 是否 CPU 高但吞吐低?

用户态高通常看应用热点。内核态高要看系统调用、网络、设备、中断、锁等。

3. CPU 低但程序慢

CPU 不高但慢,常见原因是等待。

等待什么?

  • IO。
  • 锁。
  • 队列。
  • 远端服务。
  • 定时器。
  • 调度。
  • 内存缺页。

典型:

线程大部分在 blocked
CPU 空闲
请求仍然慢

这不是 CPU 算力问题,而是等待路径问题。

4. 上下文切换高

上下文切换高可能来自:

  • 线程太多。
  • 任务太短。
  • 频繁阻塞和唤醒。
  • 锁竞争。
  • IO 事件频繁。
  • 时间片竞争。

影响:

  • CPU 花时间切换,不做有效工作。
  • Cache/TLB 局部性变差。
  • 延迟抖动。

排查:

线程数
runnable 数量
阻塞唤醒频率
锁等待
系统调用频率

5. 内存问题怎么拆

内存高不等于一定泄漏。

可能是:

  • 堆增长。
  • 线程栈多。
  • mmap 多。
  • 页缓存增长。
  • 共享内存。
  • 外部内存。
  • 内核缓冲区。
  • 容器限制。

要问:

  1. 是单进程高,还是系统整体高?
  2. 是虚拟地址空间高,还是常驻内存高?
  3. 是否持续增长不回落?
  4. 是否发生缺页或 swap?
  5. 是否 OOM kill?
  6. 是否读写大量文件导致页缓存高?

6. IO 慢怎么拆

文件 IO 慢可能来自:

  • 页缓存未命中。
  • 磁盘慢。
  • 小 IO 太多。
  • 同步刷盘。
  • 文件系统元数据。
  • 设备队列长。

网络 IO 慢可能来自:

  • DNS。
  • connect。
  • 发送缓冲区满。
  • 接收缓冲区堆积。
  • 对端慢。
  • 重传或丢包。
  • event loop 阻塞。
  • 线程池耗尽。

IO 排查要按路径拆,不要一句“IO 慢”结束。

7. 锁等待和死锁

锁问题常见现象:

  • 吞吐下降。
  • CPU 不高但请求慢。
  • 大量线程 blocked。
  • 某线程持锁时间长。
  • 多线程互相等待。

排查步骤:

  1. 找等待线程。
  2. 找它等的锁。
  3. 找锁持有者。
  4. 看持有者在做什么。
  5. 画等待关系图。
  6. 检查是否有环。

等待图有环,就是死锁方向。

8. 队列堆积

很多系统问题本质是队列堆积。

请求到达速度 > 处理速度

队列可能在:

  • 应用任务队列。
  • 线程池队列。
  • 内核 socket buffer。
  • 磁盘队列。
  • 设备队列。
  • 下游服务队列。

队列增长会带来:

  • 延迟升高。
  • 内存增长。
  • 超时。
  • 丢弃。
  • 级联故障。

9. 用户态和内核态证据

排障要区分:

证据说明
应用日志业务路径和状态
线程栈线程当前卡在哪里
系统指标CPU、内存、IO、网络
系统调用跟踪是否频繁进入内核或阻塞
网络抓包真实传输情况
文件系统指标IO 等待、队列、吞吐
进程资源fd、线程数、内存映射

不要用单一证据下结论。要形成证据链。

10. 联系实际:请求超时怎么按 OS 拆

现象:

请求偶尔 3 秒超时

拆解:

  1. 请求进入应用了吗?
  2. 处理线程拿到 CPU 了吗?
  3. 是否排队等线程池?
  4. 是否等锁?
  5. 是否读文件或网络阻塞?
  6. 是否系统调用卡住?
  7. 是否发生缺页或 swap?
  8. 是否 event loop 被阻塞?
  9. 是否 socket buffer 堆积?
  10. 是否对端慢或网络重传?

最终结论要能说:

时间花在哪一段
证据是什么
瓶颈资源是什么
修复后如何验证

证据链模板:不要只写“怀疑是 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. 学完本章你能解决什么问题

学完这一章,你应该能解决或开始分析这些问题:

  1. 性能问题为什么要先分类资源瓶颈?
  2. CPU 高可能有哪些 OS 方向?
  3. CPU 不高但程序慢通常在等什么?
  4. 上下文切换高为什么会降低吞吐?
  5. 内存高为什么不一定是堆泄漏?
  6. 文件 IO 和网络 IO 慢分别如何拆路径?
  7. 锁等待和死锁如何通过等待图分析?
  8. 队列堆积为什么会放大延迟?
  9. 请求超时如何建立 OS 级证据链?

OS 排障的核心是:找到线程在什么状态、资源卡在哪里、数据停在哪一层。

延伸阅读