ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Linux性能排查利器:perf工具实战定位CPU热点

Linux性能排查利器:perf工具实战定位CPU热点 简介面向Linux开发者与运维人员的perf性能测试工具资源包利用CPU硬件性能计数器进行低开销采样与事件统计可辅助定位热点函数、分析函数调用链、评估缓存命中率并将事件关联至源代码行支持用户态与内核态双重剖析适用于服务器性能调优、应用瓶颈排查和基准测试等场景。包内共8个文件核心为perf可执行程序另含3个so动态依赖库与1个自动化shell脚本so库承担底层符号解析、解压等功能脚本可一键执行多轮采集并汇总结果整体压缩包约9.08MB部署简便。资源已有2519人学习打包的依赖库和自动脚本能直接补齐运行环境减少版本匹配问题显著降低使用门槛尤其适合需要快速建立perf分析环境、完成系统级性能瓶颈定位的中高级开发者。 做后端服务这几年遇到最多的一类问题就是“机器负载不高但接口延迟上不去”或者“CPU 明明没跑满进程却卡得像死了一样”。这种时候如果手里没有趁手的工具排查效率会非常低只能靠 top 看一眼哪个进程高然后靠猜。perf 这个 Linux 内核自带的性能测试工具几乎是解决这类问题的标配——它不需要安装不需要改业务代码就能从内核层面告诉你 CPU 到底在忙什么、哪些函数吃掉了大部分时间片、锁竞争和缓存失效有多严重。我最早接触 perf 是为了排查一个 C 网关服务的 CPU 飙高问题当时折腾了两天才定位到热点函数后来熟练了基本半小时内就能给出结论。这篇文章不是我对着文档抄笔记而是把实际使用中真正高频、好用的用法整理出来。无论你是做后台开发、中间件运维还是搞数据库、网络服务perf 都值得花半小时掌握它解决问题的思路是通用的先测量再定位最后优化。1. perf 到底是什么先搞清楚它和 top、gprof 的区别1.1 一个典型场景为什么系统的 CPU 高到离谱却找不到原因你肯定遇到过这种情况top 显示某个进程 CPU 占用 800%但你只知道它吃 CPU不知道它具体在跑什么。用 strace 追踪系统调用只能看到内核交互看不到用户态函数内部逻辑用 gdb attach 上去又太重业务线程直接暂停线上根本不敢操作。而 perf 的优势恰好在于它通过内核的perf_event子系统利用 CPU 的硬件性能计数器PMU和软件事件对运行的进程进行采样你不需要重新编译程序也不需要改任何代码。我举个例子。有一次线上一个推送服务 CPU 使用率到了 900%我用perf top -p pid两分钟后直接看到热点集中在std::unordered_map::find相关的函数上再配合perf record抓调用栈确认是消息去重这段逻辑用错了容器哈希冲突严重。整个过程不到二十分钟这在以前靠猜是根本无法想象的。1.2 perf 的核心思路基于事件的采样而不是全量插桩perf 的设计哲学说到底就八个字用户态无感内核态采样。它不像 gprof 那样需要编译期插桩也不像 Valgrind 那样做指令级模拟——那两种方式虽然精确但对运行性能影响太大线上根本跑不了。perf 默认用的是采样模式内核每隔固定周期比如每毫秒触发一次 PMU 中断记录当前 CPU 正在执行的是哪个进程、哪个函数、调用栈长什么样。这就像你在考场外随机抽查学生的做题状态不需要每道题都盯着看抽样足够多就能准确判断大多数时间花在哪一科上。对比一下你就懂了工具原理性能开销是否需重编译典型场景top/htop读 /proc 统计进程信息极低否看整体负载strace跟踪系统调用高用户态/内核态切换放大否排查系统调用错误gprof编译期插桩函数入口/出口中高是本地性能剖析Valgrind/Callgrind动态二进制插桩极高10-50倍否找内存/缓存问题perf硬件PMU/软件事件采样低通常5%否线上热点定位、调用链分析2. 理解核心概念事件、采样频率与调用栈2.1 事件体系硬件事件、软件事件与 tracepointperf 能监控的事件非常多但日常真正需要掌握的就三类。第一类是硬件事件由 CPU 的 PMU 计数器提供比如cyclesCPU 周期数判断时钟消耗的总量instructions执行了多少条指令配合 cycles 算 IPC每周期指令数cache-misses缓存未命中次数这个值高往往意味着内存访问模式有问题branch-misses分支预测失败次数在分支密集的代码里很关键第二类是软件事件由内核模拟产生比如context-switches、page-faults、cpu-clock、task-clock这类事件适合在硬件计数器不可用或想观察调度行为时使用。第三类是tracepoint也就是内核源码里静态埋好的跟踪点例如sched:sched_switch进程切换、block:block_rq_complete磁盘IO完成、irq:irq_handler_entry中断入口。tracepoint 通常配合perf record -e指定来抓取特定子系统行为。实际使用中最常用的写法是perf stat -e cycles,instructions,cache-misses,cache-references ./your_app跑完会直接告诉你每个事件的计数。比如cache-misses和cache-references的比例超过 10%就要怀疑局部性不好IPC 如果低于 0.5说明 CPU 大量时间在等待数据而非执行指令。2.2 采样频率怎么选99 还是 1000采样频率是 perf 最容易踩坑的地方。默认频率是 4000Hz理论上够用但在低频任务或短生命周期进程上4000Hz 可能有明显开销。我的经验是线上排查用 -F 99本地压测可以用 -F 999。为什么是 99 而不是 100因为 100Hz 容易和系统的调度周期或其他 10ms 级别的任务对齐造成采样结果出现周期性的假象。99 是一个质数采样点分布更均匀这是业内火焰图讨论中常见的做法Brendan Gregg 也专门提过这一点。如果你想要更精确的时间花销可以固定周期采样而不固定频率perf record -F 99 -p pid -g -- sleep 60-g表示记录调用栈sleep 60表示持续采集 60 秒后自动停止。采集完成后当前目录会生成一个perf.data文件用perf report查看报告。2.3 调用栈为什么有时候不准帧指针和 DWARFperf record -g拿到调用栈后如果发现栈信息不完整或者干脆是乱的大概率是编译优化问题。现代编译器默认开启-O2会省略帧指针frame pointer导致 perf 无法通过常规的 rbp 寄存器回溯调用关系。这时候有两个解决办法编译时加-fno-omit-frame-pointer保留帧指针代价是性能略有下降用 DWARF 调试信息来展开栈perf record --call-graph dwarf -p pid -- sleep 60这种方式把栈上内容完整保存下来展开成功率更高但 perf.data 文件会大很多。线上二进制如果没法重新编译--call-graph dwarf往往是唯一可行的方案代价就是文件大、生成慢。如果栈还是有问题可以用perf script把原始采样数据导出来看具体哪一层断了再决定是补debuginfo包还是调整 unwinding 方式。3. 实操过程用一个完整案例跑通 perf 全流程3.1 准备一个复现用的程序为了不让你看一堆抽象概念我准备了一个模拟真实问题的 C 小程序。它生成大量字符串然后不断在一个无序的std::vector里做线性查找这在真实业务里很常见——比如用 vector 代替 set 存白名单数据量一涨就开始吃 CPU。// hot.cpp #include string #include vector #include algorithm #include cstdlib int main() { std::vectorstd::string dict; for (int i 0; i 50000; i) { dict.push_back(key_ std::to_string(rand() % 50000)); } volatile unsigned long sink 0; for (int i 0; i 1000000; i) { std::string target key_ std::to_string(rand() % 50000); auto it std::find(dict.begin(), dict.end(), target); if (it ! dict.end()) sink it-size(); } return (int)sink; }编译的时候注意如果要保留完整调用栈最好带调试信息g -O2 -g -o hot hot.cpp这样 perf 在符号解析时就能直接拿到函数名和行号省去后面装 debuginfo 的麻烦。3.2 第一步先跑 perf stat 看宏观数据运行这个程序用 perf stat 看看全局指标perf stat ./hot输出会类似这样5,204,678,201 cycles # 3.450 GHz 8,103,221,456 instructions # 1.56 insn per cycle 703,229,345 cache-references 581,934,289 cache-misses # 82.75% of all cache refs看到没有insn per cycle有 1.56不算太差但cache-misses高达 82.75%这说明程序大部分时间不是在做无意义的计算而是在等内存数据。这个信息已经明确告诉你问题方向不在 CPU 频率或者指令数量而在内存访问局部性。这就是perf stat最大的价值——在定位热点之前先给整体画像避免南辕北辙。3.3 第二步用 perf top 实时找热点接下来用perf top看热点函数不用先把程序跑完perf top -p $(pgrep hot)界面上会动态刷新各个符号的采样占比几秒钟后就能看到std::__cxx11::basic_string::compare和std::find相关符号占据了头部。这就说明了热点聚集在字符串比较和线性查找上而不是其他杂七杂八的代码。perf top和普通 top 的差别在于它精确到函数甚至能显示内核函数和动态库里的函数对快速定位非常有效。3.4 第三步perf record 抓离线样本perf top适合实时看但如果要写报告或者深入分析就必须用perf record记录采样数据perf record -F 99 -g --call-graph dwarf -p $(pgrep hot) -- sleep 30这条命令的意思是以 99Hz 的频率采集进程的所有 CPU 采样同时记录调用栈持续 30 秒。-g和--call-graph两个参数经常连用dwarf指定用调试信息展开栈防止 -O2 优化导致栈信息缺失。等它跑完后你会得到一个perf.data文件这个文件可能几百 KB 到几百 MB取决于采样时长和进程规模。3.5 第四步perf report 分析报告用perf report读取刚才的采样数据perf report --stdio --sortsymbol -i perf.data这里用--stdio是为了在终端里直接输出文本报告方便复制分析如果按默认模式则会进入一个类似 top 的交互界面用上下键选中某个函数后回车就能展开它的调用栈。--sortsymbol表示先按函数符号聚合方便看谁占用的采样最多。预期输出中热点会集中在std::find、std::string::compare和main循环上。选中std::find回车查看调用关系会发现它的父调用是main说明所有查找都直接在热点路径上。到这里问题已经很清楚了程序使用std::find在 50000 个元素的 vector 里做线性查找每次比较字符串复杂度 O(n)。当循环次数放大到百万级别CPU 就烧在缓存未命中上了。3.6 第五步用火焰图直观呈现perf report的字符界面用起来还行但分享截图和汇报时火焰图才是更直观的形式。生成火焰图需要两个脚本stackcollapse-perf.pl和flamegraph.pl这两个脚本来自 Brendan Gregg 的开源仓库 FlameGraph目前在 GitHub 上有官方维护。# 1. 将 perf.data 导出为脚本能识别的文本格式 perf script -i perf.data out.perf # 2. 折叠调用栈 stackcollapse-perf.pl out.perf out.folded # 3. 生成 SVG 火焰图 flamegraph.pl out.folded hot.svg用浏览器打开hot.svg看到的是一个从底部到顶部逐渐收窄的“火焰”形状底部是main再往上依次是std::find、字符串比较等函数。横轴宽度代表采样占比哪个函数宽就说明它吃 CPU 最凶。这张图拿去跟产品、跟其他同事解释问题一眼就能看懂不用解释采样原理。3.7 一个附加操作perf annotate 看指令级热点如果热点函数还不够精确连函数内部哪一行都想知道可以用perf annotateperf annotate -i perf.data --stdio它会打开热点函数的汇编代码并标注每条指令的采样占比。我见过最典型的案例某个排序函数一直跑得慢perf annotate一看热点集中在一条cmov条件移动指令上原来编译器把分支优化成了无条件执行执行代价更高。这种级别的问题用其他工具根本看不到只有perf annotate能帮你抠到指令级别。4. 常见问题与排查技巧实录4.1 权限不足perf_event_paranoid系列问题新装的系统上运行 perf最常见的报错是You may not have permission to collect stats.这是内核的perf_event_paranoid参数在起作用。可以通过以下命令查看当前级别cat /proc/sys/kernel/perf_event_paranoid数值含义如下值行为2只允许用户态自身的性能事件默认值很多发行版1允许 CPU 内核事件但不允许 CPU 上的原始跟踪点0允许所有用户态和内核态事件但仍禁止某些原始跟踪点-1不限制任何 perf 事件临时修改sudo sysctl kernel.perf_event_paranoid-1永久修改需要写入/etc/sysctl.conf。注意在部分云主机或者受管环境里这个 param 可能被安全策略锁住无法修改。这时候可以尝试用sudo perf配合-e cpu-clock这类不需要特殊权限的软件事件来绕过一部分限制。4.2 函数名全是地址看不到名字perf report里如果看到一堆0x1234abcd而不是函数名说明符号表没加载。排查顺序是二进制本身有没有-g或者至少没 strip用file命令看是不是with debug_info动态库的 debuginfo 装了没Debian/Ubuntu 上可以apt-get install xxx-dbgsymRHEL 系列是debuginfo-install xxx内核函数解析不上来是因为/proc/kallsyms权限限制普通用户看不到完整符号表需要 sudo 执行 perf 或者调低kptr_restrict。我在生产环境里处理过一个典型问题Java 服务的 perf 报告几乎全是看不懂的地址。原因是 JVM 的 JIT 编译出的机器码不在二进制符号表里。解决办法是使用-XX:PreserveFramePointer启动参数配合 perf-map-agent 导出 JIT 符号映射这个属于 Java 专项话题这里提一句让大家知道方向。4.3 采样数据量太大perf.data 占满磁盘--call-graph dwarf是 perf.data 膨胀的最大元凶因为每个采样点都会把整个栈的原始数据复制下来。如果你只关心采样频率低的问题比如一个低频定时任务用 DWARF 模式 30 秒可能产生数 GB 数据。我的经验做法尽量先用--call-graph fp默认帧指针模式观察如果栈信息完好就不升级到 dwarf确实需要 dwarf 时缩短采样时间比如-- sleep 10或者用-F 49降低频率采样结束后及时把 perf.data 移到专门的目录或者直接rm -f perf.data免得占满磁盘影响业务。4.4 容器和虚拟机环境下的特殊问题在容器里跑 perf 会遇到两类问题。第一类是权限即使宿主机的perf_event_paranoid允许容器默认的 seccomp 策略也可能屏蔽perf_event_open系统调用导致 perf 根本跑不起来。解决思路是让容器以 privileged 模式运行或者调整 seccomp profile 放行perf_event_open。第二类是虚拟化环境部分云实例没有透传 PMU硬件性能计数器给虚拟机cycles等硬件事件会拿到全 0 或者不可用。这时候改用软件事件备用perf stat -e cpu-clock,task-clock,context-switches ./your_app虽然精度不如硬件计数器但至少能观察调度和耗时比例。如果你的云平台支持也可以看下是否开启了 PMU 透传通常控制台有个“性能监控”开关。4.5 采样周期太短热点没抓到有次我排查一个启动即退出的小工具perf record 指定了-- sleep 5结果发现报告里几乎没有任何样本——程序 1 秒就退出了。这时候要意识到采样是概率性的程序运行越短有效样本越少。解决办法是让程序循环执行比如套一层for i in $(seq 1 100); do ./your_app; done或者用perf record -a全系统采样从进程启动那一刻就开始记录等进程结束再用perf script按 pid 过滤。我后来养成的习惯是先跑一个包含多轮循环的压测脚本在压测过程中用 perf 采样而不是直接针对单次运行的程序——这样样本量充足统计才有意义。5. 把 perf 融入日常工作流本质上perf 就是一个“性能问题 X 光机”它的学习曲线远比看起来平缓。你不需要背下所有参数只要记住perf stat看宏观、perf top看实时、perf record/report看离线调用链、perf annotate看指令级热点配合火焰图做可视化就已经能覆盖绝大多数性能排查场景。我在实际工作中积累了一套固定流程遇到 CPU 飙高先top看进程然后perf record -F 99 -g -p pid -- sleep 30抓 30 秒生成火焰图判断热点是在业务代码、GC 线程、锁竞争还是内核工作队列里。这一步定位到方向之后再决定用代码审查、JVM 调优还是系统参数调整来解决。多数情况下热点的位置和代码直觉是吻合的但总有 20% 的情况跟直觉完全相反——比如你以为瓶颈是数据库其实热点在 JSON 序列化你以为缓存命中率很高火焰图显示 cache-misses 吃掉了 40% 的周期。最后再分享一个小技巧perf top按z可以设置最小采样占比过滤我一般设成 0.5%这样界面会干净很多不会被长尾的小函数刷屏。第二个技巧是perf report里按a可以直接跳到 annotate 视图省得单独敲命令。这些交互快捷键虽然文档里有但不实际操作几次很容易错过希望这篇能帮你少踩几个我踩过的坑。本文还有配套的精品资源点击获取
返回列表