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

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

大家好,今天我们来聊聊一个比较棘手的线上问题:Java 服务 CPU 飙高,但是通过常规的监控手段(比如 JVM 指标、GC 日志等)却找不到明显的瓶颈。遇到这种情况,我们需要更强大的工具来辅助我们定位问题,今天主要介绍 perfasync-profiler 这两个工具,并结合实际案例,讲解如何使用它们来找到 CPU 热点方法。

一、问题的背景与常见排查思路

首先,我们来明确一下问题的背景。一个运行良好的 Java 服务突然 CPU 占用率飙升,但通过观察 JVM 的内存使用情况、GC 频率、线程状态等,并没有发现明显的异常。例如:

  • 内存使用正常: Heap 使用率不高,没有频繁的 Full GC。
  • GC 频率正常: GC 日志显示 GC 频率和耗时都在正常范围内。
  • 线程状态正常: 没有大量的线程处于 BLOCKED 或 WAITING 状态。
  • 数据库负载正常: 数据库查询没有明显的变慢。

在这种情况下,常规的监控手段无法提供有效的信息,我们需要深入到 CPU 层面的性能分析。

在深入之前,我们先回顾一下常见的排查思路,排除一些简单的情况:

  1. 检查是否是代码缺陷导致无限循环或者递归调用。 这种问题通常可以通过代码审查或者简单的调试来发现。
  2. 检查是否是并发问题导致死锁或者过度竞争。 可以通过 jstack 来分析线程状态,查看是否有死锁或者大量的线程在等待同一个锁。
  3. 检查是否是外部依赖服务出现问题。 比如数据库连接池耗尽,导致大量的线程在等待数据库连接。
  4. 检查是否是系统资源限制。 比如文件句柄数达到上限,导致程序无法正常运行。

如果排除了以上这些常见情况,那么我们就需要借助专业的性能分析工具来定位问题了。

二、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 热点的例子:

  1. 找到 Java 进程的 PID:

    jps -l

    假设 Java 进程的 PID 是 12345。

  2. 使用 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

    这个命令会记录一段时间内的性能数据,直到你手动停止它。

  3. 使用 perf report 分析性能数据:

    sudo perf report -i perf.data

    这个命令会打开一个交互式的性能报告界面,你可以通过上下键来浏览不同的函数,查看它们的 CPU 使用率。

  4. 使用 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 热点的例子:

  1. 找到 Java 进程的 PID:

    jps -l

    假设 Java 进程的 PID 是 12345。

  2. 使用 profiler.sh start 启动性能分析:

    ./profiler.sh start -d 30 -e cpu 12345
    • -d 30: 指定分析时间为 30 秒。
    • -e cpu: 指定要分析的事件为 CPU cycles。
    • 12345: 指定要分析的进程的 PID。

    这个命令会启动性能分析,并持续 30 秒。

  3. 使用 profiler.sh stop 停止性能分析,并生成火焰图:

    ./profiler.sh stop -f flamegraph.html 12345
    • -f flamegraph.html: 指定输出文件名为 flamegraph.html

    这个命令会停止性能分析,并生成一个名为 flamegraph.html 的火焰图文件。

  4. 使用浏览器打开 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也没有发现异常。

  1. 使用 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 的火焰非常宽。

  2. 分析代码:

    我们查看 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 函数需要遍历大量的折扣规则,并且每个规则都需要进行 isApplicableapplyDiscount 操作,导致 CPU 占用率很高。

  3. 解决方案:

    针对这个问题,我们可以采取以下解决方案:

    • 优化折扣规则的存储结构: 可以使用 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 等,帮助我们更深入地了解程序的性能瓶颈。

六、一些使用技巧和注意事项

  1. 选择合适的采样频率: 采样频率越高,性能分析的结果越准确,但同时也会带来更大的性能开销。一般来说,99Hz 是一个比较合适的采样频率。
  2. 尽量在测试环境或者预发布环境进行性能分析: 在生产环境进行性能分析可能会对系统的性能产生一定的影响。
  3. 不要过度优化: 性能优化是一个迭代的过程,不要试图一次性解决所有问题。应该根据实际情况,逐步优化程序的性能。
  4. 注意安全: perfasync-profiler 都可以访问敏感数据,因此需要注意安全,避免泄露敏感信息。

七、其他可选方案

除了perf和async-profiler,还有一些其他的性能分析工具可以作为备选方案:

  • JFR (Java Flight Recorder): Oracle JDK 自带的性能分析工具,可以收集 JVM 的各种事件,生成性能报告。
  • BTrace: 一个动态追踪 Java 程序的工具,可以在运行时修改 Java 类的行为。
  • YourKit Java Profiler/JProfiler: 商业的 Java 性能分析工具,功能强大,但需要付费。

选择哪个工具取决于你的具体需求和预算。

总结一下

遇到 Java 服务 CPU 飙高但无明显瓶颈时,perfasync-profiler 是非常有用的工具。async-profiler 易用且能生成直观的火焰图,可以快速定位 Java 应用的热点方法。perf 功能强大,可以深入到内核层面进行分析。根据实际情况选择合适的工具,并结合代码审查和分析,才能有效地解决 CPU 飙高问题。

发表回复

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