Java 服务 CPU 飙高但无明显瓶颈?使用 perf + async-profiler 定位热点方法
大家好,今天我们来聊聊一个比较棘手的线上问题:Java 服务 CPU 飙高,但是通过常规的监控手段(比如 JVM 指标、GC 日志等)却找不到明显的瓶颈。遇到这种情况,我们需要更强大的工具来辅助我们定位问题,今天主要介绍 perf 和 async-profiler 这两个工具,并结合实际案例,讲解如何使用它们来找到 CPU 热点方法。
一、问题的背景与常见排查思路
首先,我们来明确一下问题的背景。一个运行良好的 Java 服务突然 CPU 占用率飙升,但通过观察 JVM 的内存使用情况、GC 频率、线程状态等,并没有发现明显的异常。例如:
- 内存使用正常: Heap 使用率不高,没有频繁的 Full GC。
- GC 频率正常: GC 日志显示 GC 频率和耗时都在正常范围内。
- 线程状态正常: 没有大量的线程处于 BLOCKED 或 WAITING 状态。
- 数据库负载正常: 数据库查询没有明显的变慢。
在这种情况下,常规的监控手段无法提供有效的信息,我们需要深入到 CPU 层面的性能分析。
在深入之前,我们先回顾一下常见的排查思路,排除一些简单的情况:
- 检查是否是代码缺陷导致无限循环或者递归调用。 这种问题通常可以通过代码审查或者简单的调试来发现。
- 检查是否是并发问题导致死锁或者过度竞争。 可以通过 jstack 来分析线程状态,查看是否有死锁或者大量的线程在等待同一个锁。
- 检查是否是外部依赖服务出现问题。 比如数据库连接池耗尽,导致大量的线程在等待数据库连接。
- 检查是否是系统资源限制。 比如文件句柄数达到上限,导致程序无法正常运行。
如果排除了以上这些常见情况,那么我们就需要借助专业的性能分析工具来定位问题了。
二、perf 简介:Linux 性能分析利器
perf 是 Linux 内核自带的性能分析工具,它可以收集 CPU 的各种事件,比如 CPU cycles、cache misses、branch misses 等,并生成性能报告。perf 的优点是功能强大,可以深入到内核层面进行分析;缺点是使用起来比较复杂,需要对 Linux 内核有一定的了解。
1. perf 的安装与使用
在大多数 Linux 发行版中,perf 已经默认安装。如果没有安装,可以通过以下命令安装:
# Debian/Ubuntu
sudo apt-get install linux-tools-common linux-tools-$(uname -r) linux-perf
# CentOS/RHEL
sudo yum install perf
安装完成后,就可以使用 perf 命令了。下面是一些常用的 perf 命令:
perf top: 实时显示 CPU 使用率最高的函数。perf record: 记录一段时间内的性能数据。perf report: 分析perf record记录的数据,生成性能报告。perf annotate: 将性能数据与源代码关联,方便定位问题。
2. 使用 perf 定位 CPU 热点
下面是一个使用 perf 定位 CPU 热点的例子:
-
找到 Java 进程的 PID:
jps -l假设 Java 进程的 PID 是 12345。
-
使用 perf record 记录性能数据:
sudo perf record -F 99 -p 12345 -g --call-graph lbr -o perf.data-F 99: 指定采样频率为 99Hz,即每秒采样 99 次。-p 12345: 指定要分析的进程的 PID。-g: 启用调用图(call graph)记录。--call-graph lbr: 使用 LBR (Last Branch Record) 记录调用图,这种方式更加准确,但需要 CPU 支持。如果你的 CPU 不支持 LBR,可以尝试使用fp(Frame Pointer) 或者dwarf。-o perf.data: 指定输出文件名为perf.data。
这个命令会记录一段时间内的性能数据,直到你手动停止它。
-
使用 perf report 分析性能数据:
sudo perf report -i perf.data这个命令会打开一个交互式的性能报告界面,你可以通过上下键来浏览不同的函数,查看它们的 CPU 使用率。
-
使用 perf annotate 查看源代码:
在
perf report界面中,选择一个 CPU 使用率较高的函数,然后按下a键,就可以查看该函数的源代码,并看到每一行代码的 CPU 使用率。
3. perf 的局限性
perf 虽然功能强大,但也存在一些局限性:
- 需要 root 权限:
perf需要访问内核数据,因此需要 root 权限才能运行。 - 对 Java 的支持有限:
perf只能看到 Java 虚拟机(JVM)的代码,无法直接看到 Java 应用的代码。需要结合其他工具才能定位到 Java 应用的热点方法。 - 使用复杂:
perf的命令行参数比较多,使用起来比较复杂,需要一定的学习成本。
三、async-profiler 简介:专为 Java 打造的火焰图工具
async-profiler 是一个专门为 Java 打造的性能分析工具,它可以生成火焰图,帮助我们快速定位 Java 应用的热点方法。async-profiler 的优点是使用简单,对 Java 的支持非常好,可以生成直观的火焰图;缺点是功能相对较少,不如 perf 强大。
1. async-profiler 的安装与使用
async-profiler 的安装非常简单,只需要下载它的压缩包,然后解压即可。
wget https://github.com/jvm-profiling-tools/async-profiler/releases/latest/download/async-profiler-2.9-linux-x64.tar.gz
tar -xzf async-profiler-2.9-linux-x64.tar.gz
cd async-profiler-2.9
解压后,就可以使用 profiler.sh 脚本来运行 async-profiler 了。下面是一些常用的 profiler.sh 命令:
profiler.sh start: 启动性能分析。profiler.sh stop: 停止性能分析,并生成火焰图。profiler.sh dump: 生成当前的火焰图。profiler.sh status: 查看async-profiler的状态。
2. 使用 async-profiler 定位 CPU 热点
下面是一个使用 async-profiler 定位 CPU 热点的例子:
-
找到 Java 进程的 PID:
jps -l假设 Java 进程的 PID 是 12345。
-
使用 profiler.sh start 启动性能分析:
./profiler.sh start -d 30 -e cpu 12345-d 30: 指定分析时间为 30 秒。-e cpu: 指定要分析的事件为 CPU cycles。12345: 指定要分析的进程的 PID。
这个命令会启动性能分析,并持续 30 秒。
-
使用 profiler.sh stop 停止性能分析,并生成火焰图:
./profiler.sh stop -f flamegraph.html 12345-f flamegraph.html: 指定输出文件名为flamegraph.html。
这个命令会停止性能分析,并生成一个名为
flamegraph.html的火焰图文件。 -
使用浏览器打开 flamegraph.html 文件,查看火焰图。
火焰图的横轴表示 CPU 时间,纵轴表示调用栈的深度。火焰越宽,表示该函数占用的 CPU 时间越多,越有可能是热点方法。
3. async-profiler 的优势
async-profiler 相比 perf,具有以下优势:
- 使用简单:
async-profiler的命令行参数比较少,使用起来非常简单。 - 对 Java 的支持非常好:
async-profiler可以直接看到 Java 应用的代码,无需进行额外的配置。 - 火焰图直观:
async-profiler生成的火焰图非常直观,可以快速定位 Java 应用的热点方法。 - 开销低: async-profiler 通过使用 AsyncGetCallTrace API以及巧妙的设计, 使其性能开销非常低,通常在生产环境中可以直接使用,而不需要担心对应用产生显著的影响。
四、案例分析:一个真实的 CPU 飙高问题
我们来看一个真实的案例,假设一个在线交易系统,突然CPU飙高,通过监控发现JVM的GC压力不大,也没有死锁,线程dump也没有发现异常。
-
使用 async-profiler 生成火焰图:
我们首先使用
async-profiler来生成火焰图,看看能否快速定位到问题。./profiler.sh start -d 60 -e cpu 12345 ./profiler.sh stop -f flamegraph.html 12345打开
flamegraph.html文件,我们发现火焰图中有一个函数com.example.OrderService.calculateDiscount的火焰非常宽。 -
分析代码:
我们查看
com.example.OrderService.calculateDiscount函数的代码:public class OrderService { public double calculateDiscount(Order order) { double discount = 0; List<DiscountRule> rules = discountRuleService.getDiscountRules(); //获取折扣规则列表 for (DiscountRule rule : rules) { if (rule.isApplicable(order)) { //判断规则是否适用 discount += rule.applyDiscount(order); //应用折扣 } } return discount; } } public interface DiscountRule { boolean isApplicable(Order order); double applyDiscount(Order order); }通过代码审查,我们发现
discountRuleService.getDiscountRules()返回的折扣规则列表非常大,导致calculateDiscount函数需要遍历大量的折扣规则,并且每个规则都需要进行isApplicable和applyDiscount操作,导致 CPU 占用率很高。 -
解决方案:
针对这个问题,我们可以采取以下解决方案:
- 优化折扣规则的存储结构: 可以使用 HashMap 或者其他更高效的数据结构来存储折扣规则,避免线性查找。
- 使用缓存: 可以将计算过的折扣规则缓存起来,避免重复计算。
- 异步处理: 可以将折扣计算操作异步处理,避免阻塞主线程。
在这个案例中,我们通过
async-profiler快速定位到了 CPU 热点方法,并找到了问题的根源。
五、perf 与 async-profiler 的结合使用
虽然 async-profiler 使用起来更简单,更直观,但在某些情况下,perf 仍然可以发挥重要的作用。比如,当 async-profiler 无法定位到问题时,我们可以使用 perf 来进行更深入的分析。
例如,我们可以使用 perf record 记录一段时间内的性能数据,然后使用 perf report 来查看 CPU 使用率最高的函数。如果发现 CPU 使用率最高的函数是 JVM 的内部函数,那么我们可以使用 perf annotate 来查看该函数的源代码,并分析其性能瓶颈。
此外,perf 还可以用来分析 CPU 的各种事件,比如 cache misses、branch misses 等,帮助我们更深入地了解程序的性能瓶颈。
六、一些使用技巧和注意事项
- 选择合适的采样频率: 采样频率越高,性能分析的结果越准确,但同时也会带来更大的性能开销。一般来说,99Hz 是一个比较合适的采样频率。
- 尽量在测试环境或者预发布环境进行性能分析: 在生产环境进行性能分析可能会对系统的性能产生一定的影响。
- 不要过度优化: 性能优化是一个迭代的过程,不要试图一次性解决所有问题。应该根据实际情况,逐步优化程序的性能。
- 注意安全:
perf和async-profiler都可以访问敏感数据,因此需要注意安全,避免泄露敏感信息。
七、其他可选方案
除了perf和async-profiler,还有一些其他的性能分析工具可以作为备选方案:
- JFR (Java Flight Recorder): Oracle JDK 自带的性能分析工具,可以收集 JVM 的各种事件,生成性能报告。
- BTrace: 一个动态追踪 Java 程序的工具,可以在运行时修改 Java 类的行为。
- YourKit Java Profiler/JProfiler: 商业的 Java 性能分析工具,功能强大,但需要付费。
选择哪个工具取决于你的具体需求和预算。
总结一下
遇到 Java 服务 CPU 飙高但无明显瓶颈时,perf 和 async-profiler 是非常有用的工具。async-profiler 易用且能生成直观的火焰图,可以快速定位 Java 应用的热点方法。perf 功能强大,可以深入到内核层面进行分析。根据实际情况选择合适的工具,并结合代码审查和分析,才能有效地解决 CPU 飙高问题。