Linux服务器如何使用perf分析性能瓶颈?CPU采样与热点函数定位

线上服务 CPU 跑满了,top 告诉你是一个叫 java 或 php-fpm 的进程在吃 CPU,但再往下呢?是哪个函数、哪段代码在烧 CPU?top 只能告诉你"是哪个进程",回答不了"是哪行代码"。这时候就该 perf 出场了。

perf 是 Linux 内核自带的性能分析工具,基于内核的 perf_events 子系统,能做到对 CPU 做采样、对函数调用栈做统计、追踪系统调用和内核事件。它最大的价值在于:不需要改代码、不需要重启服务、对目标进程的额外开销很小,就能把"热点函数"精确定位到某个函数甚至某行汇编。

这篇文章会带你从零上手 perf:怎么装、怎么采、怎么看报告、怎么生成火焰图,以及几个最实用的排查场景。目标很实际——下次遇到 CPU 高或者程序卡,你能用一套固定命令快速找到"罪魁祸首函数",而不是靠猜和加日志。

先说明前提:perf 依赖内核支持 perf_events(2.6.31 之后的标准内核都有),需要 root 或具备 CAP_PERFMON/CAP_SYS_ADMIN 权限。部分云主机和容器环境会限制 perf 使用,遇到 perf_event_paranoid 限制时下面会讲怎么处理。

一、安装与初步验证

perf 一般包含在 linux-tools 或 linux-perf 包里,且版本要和内核版本匹配。

# 先看内核版本
uname -r

# RHEL/CentOS/Rocky 8/9
dnf install -y perf
# 若提示找不到,可尝试内核版本对应包
dnf install -y kernel-devel-$(uname -r) kernel-tools

# Debian/Ubuntu
apt install -y linux-perf
# Ubuntu 上有时包名带版本,例如 linux-tools-$(uname -r)
apt install -y linux-tools-common linux-tools-$(uname -r)

# 验证安装
perf --version

装完之后,如果 perf 报 Permission denied 或采样不到数据,多半是内核的 perf_event_paranoid 限制。

# 查看当前限制级别
cat /proc/sys/kernel/perf_event_paranoid
# 取值含义(数值越大限制越严):
#  -1  允许几乎所有操作
#   0  允许访问原始数据、允许大部分采样
#   1  允许内核 profiling(默认常见)
#   2  只允许用户态采样(较严)
#   3  几乎不允许采样(很严)

# 临时放开(会降低系统安全性,仅排查期间使用)
sysctl -w kernel.perf_event_paranoid=1

风险提示:perf_event_paranoid 调低会放宽内核层面的观测限制,属于降低隔离性的操作,排查完应改回原值。永久生效需写入 /etc/sysctl.d/99-perf.conf,回滚方式是删除该配置或改回原数值并 sysctl --system。容器环境通常不建议全局放开,优先在宿主机上分析。

二、三种最常用的采样方式

perf 的能力很多,日常最常用的是这三条命令:

# 方式一:perf top,实时看当前系统的热点函数(类似 top,但看到的是函数而不是进程)
perf top
# 交互键:按 d 看某函数详情,按 k 切换显示内核/用户符号,按 z 切换缩放

# 指定只统计某个进程
perf top -p 

# 只统计用户态,减少噪声
perf top --sort comm,dso,symbol

# 找内核函数热点
perf top -k
# 方式二:perf record + perf report,先采样保存再离线分析(最常用)
# -F 99 表示每秒采样 99 次(避开 100 的整数倍减少巧合误差)
# -g 记录调用栈,-p 指定进程,-- sleep 30 表示采样 30 秒
perf record -F 99 -g -p  -- sleep 30

# 会生成 perf.data 文件,然后分析:
perf report
# 交互界面里可以展开调用链,按 Enter 进入下一层
# 方式三:perf stat,看宏观计数器(不定位具体函数,看整体表现)
perf stat -p  sleep 5
# 输出示例:
#  Performance counter stats for process id '12345':
#    12345.67 msec task-clock         #   3.20 CPUs utilized
#           1234 context-switches     #    0.10 K/sec
#            123 cpu-migrations       #    0.01 K/sec
#      5,678,901,234 cycles             #    0.460 GHz
#      1,234,567,890 instructions       #    0.22 insn per cycle
# 其中 IPC(instructions per cycle)很低说明流水线效率差

三条命令各有侧重:perf top 适合快速看一眼当前热点,perf record/report 适合深入分析调用链,perf stat 适合看整体指标(如 IPC、缓存命中)。三者对比如下:

方式 典型命令 输出粒度 是否保留数据 适用场景
perf top perf top -p PID 函数级实时热点 否(滚动即失) 快速判断"谁在烧 CPU"
perf record + perf report perf record -F 99 -g -p PID -- sleep 30 函数级 + 调用栈 是(生成 perf.data) 深度分析调用链、定位根因
perf stat perf stat -p PID sleep 5 整体计数器 否 看 IPC、缓存命中、上下文切换等宏观指标

实战中通常先用 perf top 快速定位到热点函数,再用 perf record 采一段时间、perf report 顺调用栈深挖,最后用 perf stat 验证优化前后的整体指标变化。

三、读懂 perf 的输出

perf report 的输出每一行大致长这样:

Overhead  Command  Shared Object       Symbol
  35.20%  java     libjvm.so           [.] JIT_compiled_method_xyz
  18.40%  java     [kernel.kallsyms]   [k] copy_user_generic_string
  12.10%  java     libc.so.6           [.] __memmove_avx_unaligned

几个关键点:

  • Overhead:该函数占采样总数的比例,最高的就是最大热点。
  • Shared Object:函数属于哪个库或可执行文件。[kernel.kallsyms] 是内核,libjvm.so 是 JVM,[unknown] 通常是符号没解析出来。
  • Symbol 前的 [.] 和 [k]:[.] 表示用户态,[k] 表示内核态。
  • 符号缺失:如果大量显示 [unknown],通常是缺少调试符号(debuginfo)或二进制被 strip 了。

对高 CPU 的排查,最重要的动作是"顺着调用栈往下挖",看热点函数是被谁调用的,一层层摸到业务代码。perf report 里按 + 可以展开调用链。

四、生成火焰图:让热点一目了然

perf report 是文本的,看调用关系不够直观。生产上更常用的是把 perf 数据转成火焰图(flame graph),一张图就能看出哪条代码路径最宽、最耗时。

# 采集数据,注意 --call-graph 指定栈回溯方式
perf record -F 99 -g -p  -- sleep 30

# 生成原始栈数据(需要 FlameGraph 脚本,从 GitHub 上的 Brendan Gregg 项目获取)
perf script > /tmp/out.perf

# 用 Perl 脚本折叠栈
# stackcollapse-perf.pl 和 flamegraph.pl 来自 FlameGraph 工具集
./stackcollapse-perf.pl /tmp/out.perf > /tmp/out.folded
./flamegraph.pl /tmp/out.folded > /tmp/flamegraph.svg
# 用浏览器打开 /tmp/flamegraph.svg,火焰越宽表示该函数占用 CPU 越多

火焰图的读法:横轴代表采样占比(越宽越耗时),纵轴代表调用栈深度。从底部向上看,找到最宽的那块,顺着往上看它被谁调用,就能定位到具体的代码路径。注意火焰图里没有时间先后顺序,它是统计聚合的结果。

除了 CPU 火焰图,perf 还能做 off-CPU 分析(分析进程不在 CPU 上时在等什么),需要结合 eBPF 工具,那是更进阶的话题。

五、几个实战定位场景

场景一:CPU 高,热点在内核函数。
如果 perf top 里大量符号是 [k],比如 copy_user_generic_string、__memset 之类,说明 CPU 大量消耗在内存拷贝或系统调用上。常见于频繁的网络收发、大文件读写、字符串处理。方向是减少拷贝(零拷贝、sendfile)、批量处理、优化数据结构。

场景二:CPU 高,热点在锁相关函数。
看到 futex、pthread_mutex_lock、__lll_lock_wait 占比高,说明是锁竞争。这时加 CPU 没用,应该去看线程栈、减少锁粒度、改用无锁结构或分段锁。

# 追踪锁竞争(futex 调用)
perf trace -e futex -p 
# 或统计系统调用
perf stat -e 'syscalls:sys_enter_futex' -p  sleep 5

场景三:怀疑是缓存命中率低。
perf stat 可以看缓存 miss:

perf stat -e cache-references,cache-misses,LLC-loads,LLC-load-misses -p  sleep 5
# cache-misses 占比高说明内存访问模式不友好,数据局部性差

场景四:程序卡顿但没有明显 CPU 高。
那可能是 IO 等待或锁等待(off-CPU),perf 的 CPU 采样帮不上,需要看 offcputime、strace、iostat 这些工具配合。

场景五:排查完要清理。
perf.data 文件可能很大,采样结束记得删掉。

# 查看采样文件大小并清理
ls -lh perf.data
rm -f perf.data

六、perf 与其他工具的配合

perf 不是万能的,很多问题需要和其他工具组合。理解每类工具的边界,排查时才不会用错方向。

perf 擅长的是 CPU 上的执行热点,也就是"进程在 CPU 上烧时间烧在哪"。但如果进程大量的时间是在等——等 IO、等锁、睡在 git 上——CPU 采样是看不到的,因为那些时刻进程根本没在跑。这个盲区要靠 off-CPU 分析来补:看进程"不在 CPU 上时在等什么、等了多久"。

# strace 看系统调用的时间分布,判断是否卡在某个 syscall
strace -c -f -p           # 统计各类系统调用耗时占比
strace -T -tt -p          # 每条调用带时间戳和耗时

# ltrace 看库函数调用(比 strace 更上层)
ltrace -c -p 

# pidstat 看进程的 CPU、内存、IO、上下文切换综合指标
pidstat -u -r -d -w -p  1 10

# 看进程卡在内核的哪个函数(D 状态进程)
cat /proc//wchan

分工可以这样记:perf 回答"CPU 花在哪段代码",strace/ltrace 回答"卡在哪个系统调用或库函数",pidstat/vmstat 回答"整体资源在哪个维度异常",iostat/iotop 回答"磁盘是不是瓶颈"。先把问题归到某一类(CPU 密集、IO 等待、锁竞争),再选对应工具,效率最高。

一个典型的组合排查案例:CPU 使用率高,perf top 显示热点是 futex。这说明存在锁竞争,但 perf 只能告诉你"在等锁",不能告诉你"谁拿着锁不放"。这时需要 perf trace -e futex 看调用方,再结合线程栈分析持有锁的线程,才能找到根因。

# 追踪 futex 调用,找出锁竞争的调用来源
perf trace -e futex -p  -- sleep 5
# 若能看到大量 futex(FUTEX_WAIT),配合 jstack(Java)或 pstack(C/C++)看栈
pstack           # 打印 C/C++ 进程的线程栈

七、使用注意事项

第一,采样频率和开销。-F 99 是常用值,开销很小(通常 1% 到 3%,经验值)。若把频率调到几千赫兹,开销会明显上升,反而影响被观测程序。生产环境建议 99 到 997 之间。

第二,符号解析。要让 perf 显示有意义的函数名,程序最好带调试符号。Java 应用需要配合 perf-map-agent 生成 JIT 符号映射,否则 JIT 编译的方法显示为未知。生产环境可以保留一份未 strip 的符号表。

第三,容器环境限制。容器里默认往往无法直接 perf,需要加 --cap-add SYS_ADMIN 或 --privileged,还要挂载内核符号。更推荐在宿主机上针对容器内进程分析。

第四,kernel.perf_event_paranoid 的取舍。为了能看到内核符号,可能需要把它降到 1 甚至 0。这属于降低系统观测隔离的操作,务必用完恢复。

第五,权限。生产环境不要长期用 root 跑 perf,排查完就退出。有条件的话用 CAP_PERFMON 单独授权。

常见问题

Q:perf report 里全是 [unknown] 怎么办?
通常是缺调试符号。给程序编译时加 -g,或安装对应的 debuginfo 包(RHEL 系在 debuginfo 仓库,Debian 系用 -dbgsym 仓库)。Java 应用需要生成 JIT 符号映射。

Q:perf top 显示的热点函数一直在变,正常吗?
正常。perf top 是实时滚动视图,采样窗口内的热点会变化。要看稳定的热点,用 perf record 采一段固定时长再 perf report。

Q:采样的 CPU 开销会不会拖慢线上服务?
在 -F 99 这种常用频率下,开销很小,一般不影响生产。但采样频率过高、同时采多个进程、或者系统本身已经过载时,可能叠加影响。建议先低频率试。

Q:云主机上 perf 用不了,提示权限不足?
先看 perf_event_paranoid 值。如果是 3,基本是被内核或虚拟化层限制了,调低它试试。若被云厂商限制在 hypervisor 层,就只能用宿主机侧工具或应用自带 profiler 代替。

Q:火焰图里最宽的就是罪魁祸首吗?
通常是,但要结合调用栈判断。最宽的那个函数可能是被上游某个逻辑反复调用的公共函数,真正的根因在调用它的地方。顺着栈往上看,找到"业务逻辑层"的那个调用点。

总结

perf 能把 CPU 热点从"哪个进程"精确到"哪个函数",是排查 CPU 高、程序卡的核心工具,内核自带、无需改代码。

三条主力命令:perf top 实时看热点,perf record -F 99 -g -p PID -- sleep 30 加 perf report 做深度分析,perf stat 看宏观计数器(IPC、缓存命中、上下文切换)。

配合火焰图(perf script + stackcollapse + flamegraph)能直观看出最宽的代码路径,是定位复杂调用链的利器。

典型场景要会读:内核拷贝函数高是内存拷贝瓶颈,futex 高是锁竞争,缓存 miss 高是数据局部性差,没有 CPU 高但程序卡则要转去查 IO 和 off-CPU 等待。

注意权限与安全边界:调低 perf_event_paranoid 和容器里加特权都降低隔离性,用完立即恢复;采样频率保持 99 左右,生产环境开销可控。