Arthas 实战:从入门到线上排障,使用火焰图定位 Java 性能热点

1. Arthas 是什么

Arthas 是阿里开源的 Java 诊断工具。它以 Java Agent 的方式附着到目标 JVM,在不修改业务代码、不重启服务的前提下,帮助开发者查看线程、内存、类加载、方法调用、入参、返回值、异常和执行耗时。

你是不是也有遇到过这些问题:

  • 某个接口突然变慢,慢在哪个方法?是不是死锁了?
  • 某个方法的入参到底是什么?
  • 线上运行的代码和本地代码是否一致?
  • 某个异常是谁抛出的?
  • CPU 为什么突然飙高,是什么原因造成的呢?
  • 我写的代码没有执行,是部署分支不对,还是我压根没提交呢?

那arthas刚好就是解决这些问题的,简单来说,日志告诉你“发生过什么”,Arthas 让你看到“此刻正在发生什么”。

2. Arthas 的工作原理

Arthas 会通过 Java 的 Attach API 连接到指定 JVM,并利用 Instrumentation 对目标类进行增强。

因此,它能观察方法执行过程,但也意味着线上使用必须谨慎:范围要小、次数要有限制、观察结束后要退出或恢复增强。

3.安装与启动

在上面我们了解了它是什么,以及怎么工作的,现在我们就来安装启动实操一下。

安装:curl -O https://arthas.aliyun.com/arthas-boot.jar
启动:java -jar arthas-boot.jar

在自己需要安装的地方,打开终端命令窗口执行安装指令

注意:然后启动的时候要有一段jvm进程是在运行的

我这边为了简单做个demo,已经把代码粘贴到这边了

import java.lang.management.ManagementFactory;
import java.time.LocalTime;
import java.util.concurrent.ThreadLocalRandom;

/**
 * A small long-running JVM for practicing Arthas commands.
 *
 * Compile: javac ArthasDemoApplication.java
 * Run:     java ArthasDemoApplication
 */
public class ArthasDemoApplication {
    public static void main(String[] args) throws InterruptedException {
        String processName = ManagementFactory.getRuntimeMXBean().getName();
        System.out.println("Arthas demo started. JVM process: " + processName);
        System.out.println("Keep this window open, then attach Arthas from another terminal.");

        OrderService orderService = new OrderService();
        int sequence = 1;

        while (true) {
            Order order = new Order("order-" + sequence, 99 + sequence);
            try {
                OrderResult result = orderService.createOrder(order);
                System.out.println(LocalTime.now() + " success: " + result);
            } catch (IllegalStateException exception) {
                System.out.println(LocalTime.now() + " failed: " + exception.getMessage());
            }

            sequence++;
            Thread.sleep(1000);
        }
    }
}

class OrderService {
    private final InventoryService inventoryService = new InventoryService();
    private final PaymentService paymentService = new PaymentService();

    public OrderResult createOrder(Order order) {
        validate(order);
        inventoryService.reserve(order);
        int payableAmount = calculatePayableAmount(order);
        String paymentId = paymentService.pay(order, payableAmount);
        return new OrderResult(order.getId(), paymentId, payableAmount);
    }

    private void validate(Order order) {
        if (order.getAmount() <= 0) {
            throw new IllegalArgumentException("amount must be positive");
        }
    }

    private int calculatePayableAmount(Order order) {
        return order.getAmount() * 9 / 10;
    }
}

class InventoryService {
    public void reserve(Order order) {
        int number = order.getNumber();
        if (number % 5 == 0) {
            sleep(1500); // Deliberately slow: useful for trace.
        }
        if (number % 7 == 0) {
            throw new IllegalStateException("inventory is unavailable for " + order.getId());
        }
    }

    private void sleep(long milliseconds) {
        try {
            Thread.sleep(milliseconds);
        } catch (InterruptedException exception) {
            Thread.currentThread().interrupt();
        }
    }
}

class PaymentService {
    public String pay(Order order, int payableAmount) {
        if (ThreadLocalRandom.current().nextInt(10) == 0) {
            throw new IllegalStateException("payment gateway timeout for " + order.getId());
        }
        return "pay-" + order.getNumber() + "-" + payableAmount;
    }
}

class Order {
    private final String id;
    private final int amount;

    Order(String id, int amount) {
        this.id = id;
        this.amount = amount;
    }

    String getId() {
        return id;
    }

    int getAmount() {
        return amount;
    }

    int getNumber() {
        return Integer.parseInt(id.substring("order-".length()));
    }
}

class OrderResult {
    private final String orderId;
    private final String paymentId;
    private final int payableAmount;

    OrderResult(String orderId, String paymentId, int payableAmount) {
        this.orderId = orderId;
        this.paymentId = paymentId;
        this.payableAmount = payableAmount;
    }

    @Override
    public String toString() {
        return "OrderResult{orderId='" + orderId + "', paymentId='" + paymentId
                + "', payableAmount=" + payableAmount + "}";
    }
}

终端打开,到这个代码的目录下,编译和启动这个代码,他会持续的运行,并打印 JVM 进程号

cd D:\Self-Media
javac ArthasDemoApplication.java
java ArthasDemoApplication

保持这个窗口不关闭,另开一个窗口,启动arthas

java -jar arthas-boot.jar

可以看到1和2两个进程,第一个就是我们的demo,运行成功如下:

4. 最重要的命令

我们可以输入  help  ,可以查看有哪些指令

然后我们来介绍几个比较重要和常用的指令

命令一句话作用什么时候用
dashboard实时看板,看CPU、内存、        线程一上来就用,看整体有没有异常
thread看线程栈定位CPU飙高thread -n 3)或死锁thread -b
jad反编译类确认线上代码是不是最新版本
watch看方法的入参、返回值、异常接口返回错或没日志时,抓现场
trace看方法内部调用链路的耗时接口慢,查瓶颈在哪一步
ognl执行表达式,查看/修改静态变量查配置开关、改线上开关(谨慎)
profilerCPU性能分析生成火焰图,定位CPU热点
tt记录方法调用回放和分析历史调用

5. 三个核心命令:watch、trace、tt

5.1 watch:看入参、返回值和异常

watch OrderService createOrder '{params, throwExp}' -e -x 2 -n 5

参数含义:

  • params:方法参数
  • returnObj:返回值
  • throwExp:抛出的异常
  • -x 2:对象展开层级,避免输出过深
  • -n 5:最多采集 5 次,线上必须限制次数

5.2 trace:定位慢调用

接口变慢时,不要先猜数据库或网络问题,先追踪真实方法路径:

trace OrderService createOrder -n 5

输出会显示调用树及每个节点耗时。

5.3 tt:记录并分析调用现场

tt(Time Tunnel)会保存一次方法调用现场,适合偶发异常或难以复现的问题。

tt -t com.example.order.OrderService createOrder

查看已经记录的数据:

tt -l

查看指定记录的入参、返回值或异常:

tt -i 1000

线上使用 tt 时要注意:记录对象会占用内存,必须控制记录范围和次数,排查结束后及时清理。

6. profiler 指令生成火焰图

Arthas 的 profiler 命令底层依赖 async-profiler。当前环境下,该功能只支持 Linux 和 macOS,Windows 不能使用。

火焰图适合分析:

  • CPU 持续升高,定位最消耗 CPU 的调用链
  • 接口执行慢,分析 CPU 密集型代码
  • 大量对象创建,定位内存分配热点
  • 线程阻塞,分析锁竞争或等待调用

不过需要注意:不同问题应使用不同的采集事件。

# CPU 火焰图
profiler start --event cpu

# 等待采集一段时间
profiler status

# 停止并生成 HTML 文件
profiler stop --file /tmp/cpu-flamegraph.html

也可以直接指定采集时长:

profiler start --event cpu --duration 120

采集 120 秒后,文件会自动生成。

把生成的 HTML 文件复制到自己的电脑上,然后用浏览器打开。

不同事件的用途:

# CPU 消耗
profiler start --event cpu

# 网络、锁、sleep 等等待
profiler start --event wall

# 对象分配热点
profiler start --event alloc

# 锁竞争
profiler start --event lock

事件是否可用取决于当前 Arthas 和 async-profiler 版本。

需要特别说明:

  • CPU 火焰图不能直接证明 GC 频繁
  • alloc 火焰图适合定位大量对象创建
  • wall 火焰图适合分析线程等待
  • trace 更适合分析某一个接口的单次调用耗时

7. 火焰图怎么看

普通火焰图中:

  • 横向宽度:该方法及其子调用占用的采样数量
  • 纵向高度:调用栈深度
  • 越宽:采样占比越高
  • 越往上:调用层级越深
  • 底部:调用方或线程入口
  • 顶部:当前正在执行的方法

例如:

main
└── OrderService.createOrder
    └── PaymentService.pay
        └── JsonSerializer.serialize

如果 JsonSerializer.serialize 这一块很宽,说明采样期间大量 CPU 时间消耗在 JSON 序列化相关调用上。

注意,火焰图横轴通常不是时间轴,左右位置没有特殊含义,重点看色块的宽度。

    评论
    添加红包

    请填写红包祝福语或标题

    红包个数最小为10个

    红包金额最低5元

    当前余额3.43前往充值 >
    需支付:10.00
    成就一亿技术人!
    领取后你会自动成为博主和红包主的粉丝 规则
    hope_wisdom
    发出的红包
    实付
    使用余额支付
    点击重新获取
    扫码支付
    钱包余额 0

    抵扣说明:

    1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
    2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

    余额充值