好的,我们开始今天的讲座,主题是 “JAVA 服务 CPU 飙高但无明显瓶颈?使用 perf + async-profiler 定位热点方法”。
当Java服务的CPU使用率突然飙升,而我们通过常规的监控手段(例如JVM的堆内存、线程状态等)无法找到明显的瓶颈时,问题往往隐藏在代码的细节之中。这时,我们需要更深入的性能分析工具来定位热点方法,找出导致CPU过度消耗的罪魁祸首。今天,我们将重点介绍 perf 和 async-profiler 这两个强大的工具,它们可以帮助我们有效地解决这类问题。
一、问题背景:CPU 飙升的常见原因及排查思路
在深入使用工具之前,我们先来回顾一下Java服务CPU飙升的常见原因,以及一般的排查思路:
- 死循环或无限递归: 这是最常见的原因之一,代码中存在逻辑错误导致程序陷入无限循环或递归调用,持续占用CPU资源。
- 频繁的GC: 大量的对象创建和销毁会导致频繁的垃圾回收,尤其是在老年代GC时,会暂停整个应用程序,导致CPU使用率飙升。
- 锁竞争: 多线程环境下,如果存在激烈的锁竞争,线程会频繁地进行上下文切换,增加CPU的负担。
- 大量的I/O操作: 频繁的磁盘I/O或网络I/O也会占用大量的CPU资源,尤其是在阻塞I/O模型下。
- 复杂的计算或算法: 某些算法的复杂度过高,或者计算量过大,也会导致CPU使用率飙升。
- JIT编译: JVM在运行时会将字节码编译成本地机器码,这个过程也会消耗CPU资源,尤其是在程序启动初期。
排查思路:
- 监控系统指标: 使用监控系统(例如Prometheus、Grafana)监控CPU使用率、内存使用率、GC情况、线程状态等关键指标。
- 线程Dump: 使用
jstack命令或Java VisualVM等工具生成线程Dump文件,分析线程的运行状态,查找是否存在死锁、阻塞等问题。 - 堆Dump: 使用
jmap命令或Java VisualVM等工具生成堆Dump文件,分析堆内存的使用情况,查找是否存在内存泄漏或大量的对象创建。 - GC日志: 分析GC日志,了解GC的频率、耗时等信息,判断是否存在频繁的GC。
- Profiling: 使用性能分析工具(例如
perf、async-profiler)对应用程序进行Profiling,定位热点方法。
二、perf:Linux下的性能分析利器
perf 是Linux内核自带的性能分析工具,它可以收集系统级别的性能数据,包括CPU周期、指令数、缓存命中率等。虽然 perf 本身不直接支持Java方法的Profiling,但它可以收集JVM的性能数据,然后结合perf-map-agent工具,将JVM的地址映射到Java方法名,从而实现Java方法的Profiling。
2.1 perf的安装与配置
perf 通常已经安装在Linux系统中,如果没有安装,可以使用以下命令安装:
sudo apt-get update
sudo apt-get install linux-tools-common linux-tools-$(uname -r)
安装完成后,需要配置 /proc/sys/kernel/perf_event_paranoid 的值,以允许非root用户使用 perf。
sudo sysctl kernel.perf_event_paranoid=-1
或者修改 /etc/sysctl.conf 文件,添加以下内容:
kernel.perf_event_paranoid = -1
然后执行 sudo sysctl -p 使配置生效。
2.2 使用perf收集JVM性能数据
使用 perf record 命令收集JVM的性能数据。
sudo perf record -F 99 -p <pid> -g --call-graph dwarf java
-F 99:指定采样频率为99Hz,即每秒采样99次。-p <pid>:指定要分析的Java进程的PID。-g:启用调用图的收集。--call-graph dwarf:使用DWARF格式生成调用图。java:指定要分析的程序是Java。
2.3 使用perf-map-agent将JVM地址映射到Java方法名
perf-map-agent 是一个用于将JVM地址映射到Java方法名的工具。它可以将 perf 收集的地址数据转换为Java方法名,方便我们分析Java代码的性能瓶颈。
首先,需要下载 perf-map-agent。可以从GitHub上下载:https://github.com/jvm-profiling-tools/perf-map-agent
下载后,编译并运行 perf-map-agent:
git clone https://github.com/jvm-profiling-tools/perf-map-agent.git
cd perf-map-agent
make
sudo ./bin/perf-java-maps.sh <pid>
这将在 /tmp/perf-<pid>.map 文件中生成JVM地址到Java方法名的映射关系。
2.4 使用perf report分析性能数据
使用 perf report 命令分析 perf record 收集的性能数据。
sudo perf report -i perf.data -n --stdio
-i perf.data:指定输入文件为perf.data。-n:显示符号名。--stdio:将报告输出到标准输出。
在 perf report 的输出中,可以看到每个函数的CPU占用率,以及调用关系。结合 /tmp/perf-<pid>.map 文件,可以将JVM地址转换为Java方法名,从而定位热点方法。
2.5 示例代码
假设我们有以下Java代码:
public class CPUDemo {
public static void main(String[] args) throws InterruptedException {
while (true) {
calculatePrime(10000);
Thread.sleep(1);
}
}
public static boolean isPrime(int number) {
if (number <= 1) return false;
for (int i = 2; i <= Math.sqrt(number); i++) {
if (number % i == 0) return false;
}
return true;
}
public static void calculatePrime(int limit) {
for (int i = 2; i <= limit; i++) {
isPrime(i);
}
}
}
这个程序会不断地计算素数,导致CPU使用率升高。
我们可以使用 perf 和 perf-map-agent 来定位 calculatePrime 方法是热点方法。
- 运行
CPUDemo程序。 - 找到
CPUDemo程序的PID。 - 执行
sudo perf record -F 99 -p <pid> -g --call-graph dwarf java。 - 运行一段时间后,停止
perf record。 - 执行
sudo ./bin/perf-java-maps.sh <pid>。 - 执行
sudo perf report -i perf.data -n --stdio。
在 perf report 的输出中,可以找到 calculatePrime 方法,并且它的CPU占用率很高。
三、async-profiler:专为Java设计的Profiling工具
async-profiler 是一个专门为Java设计的Profiling工具,它可以在不安全点(safepoint)的情况下进行采样,从而减少对应用程序性能的影响。async-profiler 支持多种Profiling模式,包括CPU profiling、Heap profiling、Lock profiling等。
3.1 async-profiler的安装与配置
从GitHub上下载 async-profiler:https://github.com/jvm-profiling-tools/async-profiler
下载后,编译 async-profiler:
git clone https://github.com/jvm-profiling-tools/async-profiler.git
cd async-profiler
make
编译完成后,会在 build 目录下生成 libasyncProfiler.so 文件。
3.2 使用async-profiler进行CPU profiling
使用 profiler.sh 脚本进行CPU profiling。
./profiler.sh -d 30 -f profile.html <pid>
-d 30:指定Profiling的时间为30秒。-f profile.html:指定输出文件为profile.html。<pid>:指定要分析的Java进程的PID。
这将在当前目录下生成 profile.html 文件,可以使用浏览器打开该文件,查看Profiling结果。
3.3 async-profiler的Profiling模式
async-profiler 支持多种Profiling模式:
| Profiling Mode | Description |
|---|---|
cpu |
CPU profiling,默认模式,用于分析CPU占用率高的代码。 |
alloc |
Allocation profiling,用于分析内存分配情况,找出频繁分配对象的代码。 |
lock |
Lock profiling,用于分析锁竞争情况,找出锁竞争激烈的代码。 |
wall |
Wall-clock profiling,用于分析程序的整体执行时间,包括I/O等待时间等。 |
itimer |
ITimer profiling,使用ITimer信号进行采样,精度较低,但对应用程序的影响较小。 |
perf |
Perf profiling,使用Linux perf工具进行采样,可以收集更详细的性能数据。 |
tracing |
Tracing profiling,使用Btrace进行动态追踪,可以收集更详细的函数调用信息。 |
safepoint |
Safepoint profiling,分析JVM safepoint的耗时情况,用于诊断GC问题。 |
context |
Context profiling,分析线程的上下文切换情况,用于诊断线程调度问题。 |
可以使用 -e 参数指定Profiling模式。例如,使用 alloc 模式进行内存分配分析:
./profiler.sh -d 30 -e alloc -f alloc.html <pid>
3.4 async-profiler的火焰图
async-profiler 生成的 profile.html 文件是一个交互式的火焰图,可以方便地查看函数的调用关系和CPU占用率。火焰图的横轴表示函数的执行时间,纵轴表示函数的调用深度。火焰越宽,表示函数的执行时间越长。
3.5 示例代码
仍然使用上面的 CPUDemo 代码,我们可以使用 async-profiler 来定位 calculatePrime 方法是热点方法。
- 运行
CPUDemo程序。 - 找到
CPUDemo程序的PID。 - 执行
./profiler.sh -d 30 -f profile.html <pid>。 - 打开
profile.html文件,可以看到calculatePrime方法的火焰很宽,说明它的CPU占用率很高。
四、perf vs async-profiler:选择合适的工具
| Feature | perf |
async-profiler |
|---|---|---|
| 采样方式 | 系统级别采样,需要结合perf-map-agent才能分析Java方法。 |
Java级别采样,直接分析Java方法。 |
| 精度 | 较高,可以收集更详细的系统性能数据。 | 较高,但在某些情况下可能会受到JVM safepoint的影响。 |
| 对应用程序的影响 | 较大,尤其是在高并发场景下。 | 较小,可以在生产环境中使用。 |
| 易用性 | 较低,需要一定的Linux系统知识。 | 较高,使用简单,生成的火焰图易于理解。 |
| 功能 | 主要用于CPU profiling,也可以收集其他系统性能数据。 | 支持多种Profiling模式,包括CPU profiling、Heap profiling、Lock profiling等。 |
| 适用场景 | 适用于需要深入了解系统性能的场景,例如分析JVM的JIT编译过程、GC过程等。 | 适用于需要快速定位Java代码性能瓶颈的场景,例如分析CPU占用率高的代码、锁竞争激烈的代码、内存分配频繁的代码等。 |
总的来说,perf 更加底层,可以收集更详细的系统性能数据,但使用起来也更加复杂。async-profiler 更加易用,可以直接分析Java方法,对应用程序的影响也较小,更适合在生产环境中使用。
五、实际案例分析
假设一个电商平台的订单服务CPU使用率持续居高不下,通过初步的监控发现,数据库连接池没有瓶颈,GC也比较正常。此时,我们可以使用 async-profiler 来定位热点方法。
- 找到订单服务的PID。
- 执行
./profiler.sh -d 60 -f order_profile.html <pid>。 - 打开
order_profile.html文件,发现com.example.order.service.OrderServiceImpl.createOrder方法的火焰很宽。 - 进一步分析
createOrder方法的代码,发现其中包含大量的数据库操作和复杂的业务逻辑。 - 针对
createOrder方法进行优化,例如减少数据库操作次数、优化算法、使用缓存等。 - 优化后,再次使用
async-profiler进行Profiling,确认CPU使用率是否降低。
六、总结:选择合适的工具解决问题
面对Java服务CPU飙高的问题,首先要明确问题背景,通过监控系统指标和线程/堆Dump等手段进行初步排查。当常规手段无法定位问题时,可以借助 perf 和 async-profiler 等性能分析工具。perf 提供了系统级别的性能分析能力,而 async-profiler 则专注于Java应用的Profiling。选择合适的工具,结合火焰图等可视化手段,能够帮助我们快速定位热点方法,从而解决CPU飙高的问题。