为什么 top 和 sar 之后还需要 perf
服务器 CPU 占用高的时候,站长们第一反应是登录上去敲 top,看到一个进程占了 90% CPU,然后呢?接下来常见的动作是 strace、strace、再 strace,或者干脆重启服务。问题是 top 只能告诉你「哪个进程在烧 CPU」,strace 只能告诉你「这个进程在调哪些系统调用」,而真正的问题往往在于——这个进程内部的哪个函数、哪一行代码在烧 CPU。
strace 还有个更致命的限制:它的开销极大,一个高频系统调用的程序被 strace 挂上去之后可能直接慢十倍,观测行为本身改变了被观测对象。sar 和 iostat 是周期采样,粒度粗、看不到调用栈。要回答「CPU 到底烧在哪个函数上」这个问题,需要的是一套基于硬件性能计数器、能采样调用栈、开销可控的工具——这就是 perf。
perf 是 Linux 内核自带的性能分析工具集(tools/perf),它利用了 CPU 的 PMU(Performance Monitoring Unit)硬件计数器,可以做到接近零开销的采样。它能给出函数级、甚至指令级的 CPU 热点分布,是排查「CPU 高但看不出原因」这类问题的终极手段。
安装与第一个必踩的权限坑
perf 本身通常不在最小安装的镜像里,Debian/Ubuntu 下装对应内核版本的工具包:
# 先查内核版本 uname -r # 安装(版本要跟内核匹配,否则符号解析会缺失) apt-get install -y linux-perf # 或者按具体版本 apt-get install -y linux-tools-$(uname -r)
装好之后第一件事往往是撞到权限报错:
$ perf top Error: Access to performance monitoring and observability operations is limited.
这是内核的 perf_event_paranoid 安全策略在起作用。它的取值含义是:
-1:几乎不限制,允许所有事件(最宽松,也最危险)0:允许访问 CPU 相关事件,但限制原始 tracepoint1:只允许对当前用户自己的进程做性能分析(很多发行版的默认值)2:只允许用户态采样,禁止内核态采样(更严)3:完全禁止
查看当前值并临时放开:
cat /proc/sys/kernel/perf_event_paranoid # 临时放开(重启失效) sysctl -w kernel.perf_event_paranoid=1
需要说明的是,这本身是一个安全边界:perf_event_open 曾经被用于本地提权攻击(比如 CVE-2013-2094),所以把 paranoid 调到 -1 是不负责任的。生产环境建议保持 1 或 2,只在排查期间临时放宽、排查完立刻改回。永久修改要写进 /etc/sysctl.d/,这样重启后策略不会莫名丢失。
第一层:perf top 实时看热点函数
perf top 是最高频使用的入口,效果类似 top 但展示的是函数级热点:
perf top -p $(pgrep -f php-fpm | head -1) # 或者整机 perf top
输出里几列要会读。最右侧的符号是函数名,前面是 DSO(动态共享对象,即函数属于哪个库或可执行文件),百分比是采样占比。判读时有几个经验:
如果热点集中在某个业务函数上,那是应用层代码问题;如果热点都在 [kernel.kallsyms] 下的 copy_user_enhanced_fast_string、page_fault 之类的内核函数上,那多半是内存拷贝或缺页问题,要去看内存而不是 CPU;如果一大堆采样落在 [unknown] 上,那是符号解析失败,通常是没装对应版本的 perf 工具包,或者程序被 strip 了符号。
还有一个细节:perf top 默认按 CPU cycles 采样,如果你的场景是「CPU 不高但很慢」,那瓶颈很可能不在 CPU 上,用 perf top 只会看到一片空白,这时候该换的是 off-CPU 分析或者直接看 IO/锁等待。
第二层:perf record 采样,事后慢慢分析
perf top 是实时滚动的,来得快去得也快,适合快速定位;真正做分析要用 perf record 把采样落盘。关键是加上调用栈记录:
# 采集 30 秒,记录调用栈,采样频率设为 999Hz(避开 1000Hz 的整数倍干扰) perf record -F 999 -g -p $(pgrep -f php-fpm | head -1) -- sleep 30 # 采集整个系统 perf record -F 999 -a -g -- sleep 30 # 采集指定进程的所有线程,包含子进程 perf record -F 999 -g --call-graph dwarf -p 12345 -- sleep 30
几个参数的含义:-F 是采样频率,标 999 而不是 1000 是个小技巧,因为很多系统时钟中断也是 1000Hz,同频采样会产生系统性的偏差,错开一位能避免锁相;-g 是记录调用图;--call-graph dwarf 用 DWARF 调试信息展开调用栈,比默认的 frame pointer 更准确,代价是需要程序带调试信息、且采集的数据量更大。
这里有个高频故障:采到的调用栈只有一层,看不到调用链。原因通常是程序编译时省略了帧指针(-fomit-frame-pointer,很多发行版默认开启),或者用了 --call-graph fp 但栈被优化掉了。解决办法是改用 dwarf,或者给程序加 -fno-omit-frame-pointer 重新编译。
第三层:perf report 与火焰图
采样完成后,先用文本方式看:
perf report --stdio --sort comm,dso,symbol -g none | head -40 perf report --stdio -g graph,0.5,caller
--sort 决定聚合维度。按 comm,dso,symbol 聚合得到的是「哪些函数最热」,适合快速定位;-g graph 则是展示调用关系树,能看到热点是从哪条调用路径来的。排障时我习惯先看不带调用图的排序,锁定嫌疑函数,再用调用图确认它是被谁调起来的。
文本终究不如图直观。火焰图(flame graph)是把调用栈按宽度堆叠成一张图,横轴宽度代表采样占比、纵向深度代表调用层级,一眼就能看出哪条调用链最宽。生成方式是先转成折叠格式,再用 FlameGraph 工具渲染:
# 导出原始数据 perf script > out.perf # 折叠(需要 Brendan Gregg 的 FlameGraph 仓库) git clone https://github.com/brendangregg/FlameGraph cd FlameGraph ./stackcollapse-perf.pl ../out.perf > out.folded ./flamegraph.pl out.folded > flame.svg
生成的 SVG 在浏览器里打开,可以直接点击放大、搜索函数名高亮。对于「CPU 热点分散在很多小函数上」的场景,火焰图比任何文本报表都直观。
排查实例:一次「CPU 不高但响应很慢」的诊断
说一个真实排查思路。某天站点响应明显变慢,但 top 显示 CPU 空闲、iostat 显示磁盘也不忙、MySQL 的慢查询日志里也没有明显慢语句。这种情况下我把 perf 的视角换成了 off-CPU——因为进程慢,不一定是忙,也可能是「在等」。
# on-CPU:进程占用 CPU 的时间分布(本例意义不大) perf record -F 999 -g -p $pid -- sleep 30 # 更该关注的是:进程被调度出去时在等什么 # 用 perf 的 sched 事件统计上下文切换 perf stat -e context-switches,cpu-migrations,page-faults -p $pid -- sleep 10
perf stat 是另一个非常实用的子命令,它不采样调用栈,而是统计硬件/软件事件计数。上面这条能看到十秒内进程发生了多少次上下文切换、多少次缺页。如果上下文切换次数异常高,说明进程被频繁唤醒又睡下,很可能是锁竞争;如果 minor page fault 极高,说明内存在被反复分配释放,要去看内存池配置。
本例最后定位到是 opcache 配置不当导致频繁重新编译脚本,属于「CPU 上不去但一直在做无用功」的典型。如果只看 top,这个进程的 CPU 占比只有百分之几,根本不会被怀疑。
perf stat:不做采样,只数事件
perf 里最容易被忽略、但排障时性价比极高的子命令是 perf stat。它不抓调用栈,只是把一段程序运行期间的硬件和软件事件计数汇总出来。相比 perf record,它的开销更低、结论更直接,适合作为第一步的「体检」。
# 统计某条命令运行期间的关键指标 perf stat -d dd if=/dev/zero of=/dev/null bs=1M count=10000 # 统计正在运行的进程 10 秒 perf stat -e cycles,instructions,cache-misses,branch-misses,context-switches \ -p $pid -- sleep 10
输出里最该看的是派生指标而不是原始计数。IPC(instructions per cycle,每周期指令数)等于 instructions 除以 cycles,它是判断 CPU 效率的核心:IPC 明显低于 1,说明 CPU 大部分周期在等内存或等停顿,瓶颈在访存而非计算,这时候加 CPU 是没用的;IPC 接近 2 甚至更高,说明流水线跑得饱满,属于真正的计算密集。cache-misses 占 cache-references 的比例则能揭示是不是缓存不友好——比例高说明数据布局有问题,循环访问的内存跨度太大。
还有一个经常被忽略的指标是 context-switches。上下文切换本身不是坏事,但次数异常高就说明进程在频繁被抢占或主动让出 CPU,通常指向锁竞争、线程数开得过多、或者 IO 模型设计有问题。对 PHP-FPM 这类每请求一进程的模型,上下文切换高往往意味着并发数配置得远超实际负载。
perf 与其他工具的分工
把工具的分工理清楚,排障时就不会乱用:top/htop 用来看「谁在烧 CPU」;vmstat/sar 看「系统层面的趋势」;iostat/iotop 看「磁盘 IO 是不是瓶颈」;strace 看「进程在调哪些系统调用」(只适合低频调用、有明确嫌疑的函数);tcpdump/ss 看网络。而 perf 回答的是「CPU 周期具体消耗在代码的哪一行」,它是这几个工具里粒度最细、也最需要一点硬件知识才能用好的。
使用 perf 的三个纪律:一是生产环境采样频率别开太高(-F 超过 1000 一般没必要),二是采完立刻清理 `/proc/sys/kernel/perf_event_paranoid` 的临时放宽,三是注意 perf 数据文件里的调用栈可能包含函数名和路径信息,属于内部信息,不要随手分享出去。
对个人站长来说,perf 的价值不在于每天用,而在于当 top、sar、日志都给不出答案的时候,你手里还有一张能下探到函数级的底牌。