JAVA 服务 CPU 飙高但无明显瓶颈?使用 perf + async-profiler 定位热点方法

好的,我们开始今天的讲座,主题是 “JAVA 服务 CPU 飙高但无明显瓶颈?使用 perf + async-profiler 定位热点方法”。

当Java服务的CPU使用率突然飙升,而我们通过常规的监控手段(例如JVM的堆内存、线程状态等)无法找到明显的瓶颈时,问题往往隐藏在代码的细节之中。这时,我们需要更深入的性能分析工具来定位热点方法,找出导致CPU过度消耗的罪魁祸首。今天,我们将重点介绍 perfasync-profiler 这两个强大的工具,它们可以帮助我们有效地解决这类问题。

一、问题背景:CPU 飙升的常见原因及排查思路

在深入使用工具之前,我们先来回顾一下Java服务CPU飙升的常见原因,以及一般的排查思路:

  1. 死循环或无限递归: 这是最常见的原因之一,代码中存在逻辑错误导致程序陷入无限循环或递归调用,持续占用CPU资源。
  2. 频繁的GC: 大量的对象创建和销毁会导致频繁的垃圾回收,尤其是在老年代GC时,会暂停整个应用程序,导致CPU使用率飙升。
  3. 锁竞争: 多线程环境下,如果存在激烈的锁竞争,线程会频繁地进行上下文切换,增加CPU的负担。
  4. 大量的I/O操作: 频繁的磁盘I/O或网络I/O也会占用大量的CPU资源,尤其是在阻塞I/O模型下。
  5. 复杂的计算或算法: 某些算法的复杂度过高,或者计算量过大,也会导致CPU使用率飙升。
  6. JIT编译: JVM在运行时会将字节码编译成本地机器码,这个过程也会消耗CPU资源,尤其是在程序启动初期。

排查思路:

  • 监控系统指标: 使用监控系统(例如Prometheus、Grafana)监控CPU使用率、内存使用率、GC情况、线程状态等关键指标。
  • 线程Dump: 使用jstack命令或Java VisualVM等工具生成线程Dump文件,分析线程的运行状态,查找是否存在死锁、阻塞等问题。
  • 堆Dump: 使用jmap命令或Java VisualVM等工具生成堆Dump文件,分析堆内存的使用情况,查找是否存在内存泄漏或大量的对象创建。
  • GC日志: 分析GC日志,了解GC的频率、耗时等信息,判断是否存在频繁的GC。
  • Profiling: 使用性能分析工具(例如perfasync-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使用率升高。

我们可以使用 perfperf-map-agent 来定位 calculatePrime 方法是热点方法。

  1. 运行 CPUDemo 程序。
  2. 找到 CPUDemo 程序的PID。
  3. 执行 sudo perf record -F 99 -p <pid> -g --call-graph dwarf java
  4. 运行一段时间后,停止 perf record
  5. 执行 sudo ./bin/perf-java-maps.sh <pid>
  6. 执行 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-profilerhttps://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 方法是热点方法。

  1. 运行 CPUDemo 程序。
  2. 找到 CPUDemo 程序的PID。
  3. 执行 ./profiler.sh -d 30 -f profile.html <pid>
  4. 打开 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 来定位热点方法。

  1. 找到订单服务的PID。
  2. 执行 ./profiler.sh -d 60 -f order_profile.html <pid>
  3. 打开 order_profile.html 文件,发现 com.example.order.service.OrderServiceImpl.createOrder 方法的火焰很宽。
  4. 进一步分析 createOrder 方法的代码,发现其中包含大量的数据库操作和复杂的业务逻辑。
  5. 针对 createOrder 方法进行优化,例如减少数据库操作次数、优化算法、使用缓存等。
  6. 优化后,再次使用 async-profiler 进行Profiling,确认CPU使用率是否降低。

六、总结:选择合适的工具解决问题

面对Java服务CPU飙高的问题,首先要明确问题背景,通过监控系统指标和线程/堆Dump等手段进行初步排查。当常规手段无法定位问题时,可以借助 perfasync-profiler 等性能分析工具。perf 提供了系统级别的性能分析能力,而 async-profiler 则专注于Java应用的Profiling。选择合适的工具,结合火焰图等可视化手段,能够帮助我们快速定位热点方法,从而解决CPU飙高的问题。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注