性能分析

线上服务突然变慢,CPU 飙到 100%,内存持续增长——这些问题每个后端工程师都会遇到。但大多数人的排查方式是”凭感觉改配置、凭经验加缓存”。这篇文章给你一套系统化的 Linux 性能分析方法,从工具到方法论,一次讲透。

性能分析的黄金法则

在动手之前,先记住 Brendan Gregg(Netflix 高级性能架构师)的三条原则:

  1. 不要猜测,要测量。你的直觉至少有 50% 是错误的。
  2. 从宏观到微观。先用系统级工具定位瓶颈类型(CPU?内存?I/O?网络?),再用进程级工具深入。
  3. 一次只改一个变量。同时改三个配置,性能变好了,你不知道是哪个起的作用。

推荐使用 USE 方法(Utilization, Saturation, Errors)作为分析框架:对每个资源(CPU、内存、磁盘、网络),依次检查利用率、饱和度和错误数。

第一层:系统级监控

CPU 分析:不只是看使用率

大多数人只看 top 里的 CPU 使用率。但这远远不够。你需要拆解 CPU 时间花在了哪里:

# vmstat 1:每秒输出系统状态
$ vmstat 1
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
2  0      0 823456 234567 1234567    0    0    12    34  456  789 45  5 48  2  0

# 关键指标:
# r: 运行队列长度(> CPU核心数说明CPU饱和)
# us: 用户态CPU时间(你的应用程序)
# sy: 内核态CPU时间(系统调用、中断)
# wa: I/O等待(磁盘瓶颈的明确信号)
# cs: 上下文切换次数(过高说明线程太多或锁竞争严重)

真实案例:一个 Web 服务 CPU 使用率只有 60%,但响应延迟却很高。vmstat 显示 r 值经常到 16(只有 4 核),说明大量请求在排队。原来是一个全局锁导致所有 worker 线程串行化——不是 CPU 不够,是并发度被锁限制了。

内存分析:区分”用满了”和”浪费了”

# free -h:快速查看内存概况
$ free -h
              total    used    free   shared  buff/cache   available
Mem:           7.6G    2.1G    1.2G    234M      4.3G        5.0G

# 关键理解:Linux 会积极使用空闲内存做缓存
# available 才是真正可用的内存
# used - buff/cache 才是应用程序实际消耗的内存

如果 available 持续下降而 used 不增,通常是缓存占用——正常现象。如果 used 持续增长,可能是内存泄漏,需要进入进程级分析。

第二层:进程级分析

进程分析

perf:Linux 性能分析的瑞士军刀

perf 是 Linux 内核自带的性能分析工具,不需要安装任何额外软件。它能告诉你 CPU 周期花在了哪些函数上:

# 实时采样某个进程的 CPU 使用情况(-g 生成调用图)
perf record -g -p $(pgrep myservice) -- sleep 30

# 查看报告
perf report

# 输出示例(部分):
#   Children      Self  Command  Symbol
#  +   85.23%     0.01%  myservice  [.] main
#  +   85.22%     0.02%  myservice  [.] handle_request
#  +   60.15%    35.42%  myservice  [.] json_parse  ← 热点!
#  +   24.80%    24.75%  myservice  [.] memcpy       ← 热点!
#  +   15.30%    15.28%  myservice  [.] strlen

这个输出告诉我们:35% 的 CPU 时间花在 JSON 解析上,25% 花在内存拷贝上。优化方向瞬间明确了。

火焰图:让性能数据可视化

火焰图由 Brendan Gregg 发明,是 perf 数据的可视化方式:

# 生成火焰图
perf script > out.perf
# 使用 FlameGraph 工具(github.com/brendangregg/FlameGraph)
./stackcollapse-perf.pl out.perf > out.folded
./flamegraph.pl out.folded > flamegraph.svg

火焰图的阅读方法:

  • X 轴宽度 = 占用 CPU 时间占比——越宽的函数越值得优化
  • Y 轴高度 = 调用栈深度——底部是调用者,顶部是被调用者
  • 颜色没有特殊含义——只是为了区分不同函数
  • “平顶”函数是优化目标——调用栈顶部、宽度大的函数

strace:追踪系统调用

当 perf 显示大量 CPU 时间花在 sy(内核态)时,用 strace 找出元凶:

# 追踪进程的系统调用
strace -c -p $(pgrep myservice)
# 等待 10 秒后 Ctrl+C,获得统计:

% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
 89.23    2.345678         234     10023           read
  5.12    0.134567          56      2401           write
  2.34    0.061234          12      5123           futex
  1.11    0.029012          29      1000           epoll_wait

89% 的时间花在 read 上,每次调用 234 微秒。结合 perf 的结果(JSON 解析慢),可以推断出文件读取是瓶颈——可能是从磁盘读取大 JSON 文件,每次只读一小块。

第三层:内存泄漏排查

valgrind:内存问题的终极武器

# 检测内存泄漏(显著降低程序运行速度)
valgrind --leak-check=full --show-leak-kinds=all ./myservice

# 输出示例:
==12345== 1,024 bytes in 1 blocks are definitely lost
==12345==    at 0x4C2AB80: malloc (vg_replace_malloc.c:296)
==12345==    by 0x400F3E: create_session (session.c:47)
==12345==    by 0x401B2C: handle_login (auth.c:123)
==12345==    by 0x402A1D: process_request (server.c:256)

valgrind 不仅告诉你”泄漏了 1024 字节”,还给出完整的调用链——从 process_requesthandle_logincreate_sessionmalloc,直接定位到 session.c:47

但 valgrind 会让程序慢 10-20 倍,不适合生产环境。生产环境用 jemalloctcmalloc 的内存分析功能,性能损耗在 5% 以内。

查看 /proc 获取进程实时信息

# 进程的内存映射
cat /proc/$(pgrep myservice)/smaps | grep -E '^(Size|Rss|Pss)'

# 打开的文件描述符(排查 fd 泄漏)
ls -la /proc/$(pgrep myservice)/fd | wc -l

# 线程数
ls /proc/$(pgrep myservice)/task | wc -l

真实案例:一个 Go 服务内存持续增长,但 Go 的 GC 正常。通过 /proc/PID/smaps 发现是 Rss(常驻内存)和 VmSize(虚拟内存)的差距在不断扩大——说明大量内存被分配但未实际使用(被 mmap 预留)。最终定位到日志库预分配了过大的缓冲区。

第四层:I/O 与网络

iostat:磁盘 I/O 分析

$ iostat -x 1
Device   r/s   w/s   rkB/s   wkB/s  await  svctm  %util
sda     45.0  12.0  2345.6   567.8   12.3   2.1   98.5

# await > 10ms → 磁盘可能成为瓶颈
# %util > 80%  → 磁盘已饱和
# r/s 和 w/s 的比值 → 判断是读密集还是写密集

ss(取代 netstat):网络连接分析

# 查看 TCP 连接状态分布
$ ss -s
Total: 2456
TCP:   2430 (estab 1200, closed 1230, timewait 0)

# 查看具体的连接
$ ss -tan state time-wait | wc -l  # TIME_WAIT 数量
$ ss -tan state established sport = :80 | wc -l  # 80端口活跃连接

# 大量 TIME_WAIT → 检查是否短连接过多,考虑启用 tcp_tw_reuse

性能分析工具速查表

问题类型         │ 第一层(系统)   │ 第二层(进程)    │ 第三层(代码)
─────────────────┼─────────────────┼──────────────────┼──────────────
CPU 高           │ vmstat, mpstat  │ perf top/record  │ 火焰图, gprof
内存高/泄漏      │ free, vmstat    │ /proc/PID/smaps  │ valgrind, heaptrack
磁盘 I/O 高      │ iostat, iotop   │ strace -c        │ lsof, /proc/PID/fd
网络延迟/丢包    │ ss, netstat     │ tcpdump, strace  │ wireshark, ngrep
锁竞争/并发问题  │ vmstat (cs列)   │ perf lock        │ pstack, gdb

实战:一个完整的排查案例

线上 Python Web 服务间歇性超时,平均延迟从 50ms 飙升到 2s。排查过程:

  1. vmstat 1r 值正常,wa 值高(15%)→ 磁盘 I/O 问题
  2. iostat -x 1%util 100%,await 高达 200ms → 确认磁盘瓶颈
  3. iotop → 发现不是我们的进程,而是一个日志轮转脚本在大量写磁盘
  4. strace -p 跟踪 Web 进程 → write 系统调用频繁返回 EAGAIN → 磁盘缓冲区满
  5. 根本原因:日志文件写入阻塞了事件循环(Python 的 write 默认是阻塞的)
  6. 解决方案:把日志写入改为异步(logging.handlers.QueueHandler

总结

性能分析不是玄学,而是有一套成体系的方法论:

  • USE 方法提供分析框架,避免盲目猜测
  • 从系统到进程到代码逐层深入,每一步都有对应工具
  • 测量优于直觉——没有数据支撑的”优化”通常只是偶然

最好的性能优化是不需要的优化。但当你确实需要的时候,希望这篇文章能让你少走弯路。

本站所有原创文章采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。