BTrace实战:Arthas搞不定的那些场景

BTrace实战:Arthas搞不定的那些场景

Arthas是Java线上诊断的神器,thread、trace、watch、stack几乎覆盖了日常排查的90%场景。但总有一些问题,Arthas解决不了------比如"我想知道这个方法内部到底new了多少个大对象"、"我想持续监控某个方法的GC行为,而不是只看一次"。这就是BTrace的战场。

一、Arthas vs BTrace 定位差异

先明确两者的定位:

维度 Arthas BTrace
交互方式 命令行实时交互 编写脚本,持续采集
采集方式 一次性探查 脚本化持续监控
适用场景 "现在出了什么问题" "这个问题持续多久了、频率多高"
数据输出 终端直接看 可输出到文件、累计统计
侵入性 无侵入,attach到JVM 无侵入,动态字节码注入

简单来说:Arthas是手电筒,照一下看到问题;BTrace是监控摄像头,持续记录问题的发生规律。

二、Arthas搞不定的4个场景

场景1:追踪特定方法内部的对象分配

问题描述:线上Old区内存持续增长,怀疑某方法创建了大量大对象,但需要确认到底是哪个方法、分配了什么对象、分配了多少。

Arthas的局限

  • watch 可以看到方法的入参和返回值,但看不到方法内部new了什么
  • stack 可以看到调用栈,但看不到对象分配行为
  • heapdump 可以dump堆,但无法关联到具体分配方法,且dump本身有STW风险

BTrace解法

java 复制代码
@BTrace
public class ObjectAllocTrace {

    // 追踪某方法内部分配的所有BigObject
    @OnMethod(
        clazz = "com.example.service.DataSyncService",
        method = "syncData",
        location = @Location(value = Kind.NEW, clazz = "com.example.model.BigObject")
    )
    public static void onNewBigObject(@Self Object self) {
        // 记录分配次数和时间
        println(strcat("BigObject allocated at: ", str(timeMillis())));
        // 打印当前线程栈,确认分配上下文
        jstack();
    }
}

这是BTrace唯一能做到Arthas完全做不到的事情 :拦截特定方法内部对特定类的new操作。Arthas没有任何命令可以实现这个能力。

场景2:持续监控GC行为与业务方法的关联

问题描述:Full GC频率异常,但不确定是哪个业务方法触发的。需要持续监控GC与业务方法的时序关系。

Arthas的局限

  • dashboard 可以看实时GC,但无法关联到业务方法
  • watch 可以看方法调用,但无法同时看GC事件
  • 两者没有统一的时间线来关联分析

BTrace解法

java 复制代码
@BTrace
public class GCMethodCorrelation {

    private static AtomicLong gcCount = newAtomicLong(0);

    // 监控GC事件
    @OnMethod(
        clazz = "sun.gc.GC",
        method = "notifyGC"
    )
    public static void onGC() {
        long count = incrementAndGet(gcCount);
        println(strcat("GC event #", str(count)));
        jstack(); // 打印GC触发时的调用栈
    }

    // 同时监控可疑业务方法的调用频率
    @OnMethod(
        clazz = "com.example.service.DataSyncService",
        method = "batchSync",
        location = @Location(Kind.ENTRY)
    )
    public static void onBatchSyncEntry() {
        println(strcat("batchSync called at: ", str(timeMillis())));
    }
}

把GC事件和业务方法调用记录到同一输出流,可以清晰看到时序关系:是batchSync之后触发了GC,还是GC与业务无关。

场景3:复杂条件下的方法耗时统计

问题描述:需要统计某个方法在特定条件下的耗时分布,比如"只统计参数size > 1000时的耗时",且需要累计统计(P50/P90/P99),而不是只看一次。

Arthas的局限

  • trace 可以看单次调用耗时,但不支持条件过滤 ,且无法累计统计
  • watch 可以加条件表达式,但每次只看一条,无法聚合

BTrace解法

java 复制代码
@BTrace
public class ConditionalTiming {

    private static AtomicLong count = newAtomicLong(0);
    private static AtomicLong totalMs = newAtomicLong(0);
    private static AtomicLong maxMs = newAtomicLong(0);

    @OnMethod(
        clazz = "com.example.service.DataSyncService",
        method = "syncBatch",
        location = @Location(Kind.RETURN)
    )
    public static void onSyncBatchReturn(
        @Self Object self,
        int batchSize,    // 方法入参
        @Duration long duration  // 耗时(纳秒)
    ) {
        // 只统计batchSize > 1000的情况
        if (batchSize > 1000) {
            long durationMs = duration / 1_000_000;
            incrementAndGet(count);
            addAndGet(totalMs, durationMs);

            long currentMax = get(maxMs);
            if (durationMs > currentMax) {
                set(maxMs, durationMs);
            }

            println(strcat("Slow sync: batchSize=", str(batchSize)));
            println(strcat("  duration=", str(durationMs) + "ms"));
        }
    }

    @OnTimer(60000) // 每分钟输出一次统计
    public static void printStats() {
        long c = get(count);
        if (c > 0) {
            long avg = get(totalMs) / c;
            println("=== Last minute stats (batchSize > 1000) ===");
            println(strcat("  count: ", str(c)));
            println(strcat("  avg: ", str(avg) + "ms"));
            println(strcat("  max: ", str(get(maxMs)) + "ms"));
        }
        // 重置计数器
        set(count, 0);
        set(totalMs, 0);
        set(maxMs, 0);
    }
}

这个能力是Arthas完全不具备的:条件过滤 + 持续采集 + 定时聚合输出

场景4:多方法调用链路的持续追踪

问题描述:一个请求会经过A→B→C三个方法,需要持续监控这条链路的调用关系和各环节耗时,确认是否偶尔出现B方法超时导致整体变慢。

Arthas的局限

  • trace 只能trace一个方法及其子调用,无法跨方法持续关联
  • 每次trace只看一次调用,无法统计"偶尔超时"的频率

BTrace解法

java 复制代码
@BTrace
public class ChainTrace {

    // 用ThreadLocal追踪同一请求的调用链
    @TLS
    private static long startTime;

    @OnMethod(
        clazz = "com.example.service.MethodA",
        method = "process",
        location = @Location(Kind.ENTRY)
    )
    public static void onAEntry() {
        startTime = timeMillis();
        println("=== Chain start ===");
    }

    @OnMethod(
        clazz = "com.example.service.MethodB",
        method = "handle",
        location = @Location(Kind.RETURN)
    )
    public static void onBReturn(@Duration long duration) {
        long bMs = duration / 1_000_000;
        if (bMs > 500) { // B方法超过500ms才告警
            println(strcat("  WARNING: MethodB slow: ", str(bMs) + "ms"));
            jstack(); // 打印慢调用的线程栈
        }
    }

    @OnMethod(
        clazz = "com.example.service.MethodC",
        method = "execute",
        location = @Location(Kind.RETURN)
    )
    public static void onCReturn(@Duration long duration) {
        long totalMs = timeMillis() - startTime;
        long cMs = duration / 1_000_000;
        if (totalMs > 1000) { // 整体超过1s
            println(strcat("  Chain total: ", str(totalMs) + "ms, C: " + str(cMs) + "ms"));
        }
    }
}

通过@TLS(ThreadLocal)把同一请求的多次方法调用关联起来,实现了跨方法的调用链追踪,且只输出异常情况,不会刷屏。

三、BTrace使用注意事项

安全限制

BTrace默认有安全限制,防止脚本破坏业务进程:

  • 不能调用BTrace脚本自身以外的任何类(防止死循环和副作用)
  • 不能创建新对象(防止内存泄漏)
  • 不能抛出异常(防止影响业务逻辑)
  • 不能修改被追踪程序的变量(只读)

如果确实需要突破限制(如调用外部类),需要在启动时加 --unsafe 参数,但生产环境慎用

性能影响

  • BTrace通过动态字节码注入实现,每次方法调用都会经过注入的探针代码
  • 高频方法慎用:如果追踪的方法QPS极高,探针本身的开销不可忽略
  • 建议:先在预发环境验证脚本正确性和性能影响,再上生产

与Arthas配合使用

实际工作中,两者是互补的:

  1. 先用Arthas快速定位问题方向thread看线程、trace看耗时、watch看参数

  2. 再用BTrace持续监控确认:写脚本持续采集数据,确认问题规律

  3. 最后用Arthas深入分析 :定位到具体方法后,watch+stack深入看细节

    问题出现 → Arthas快速定位方向 → BTrace持续监控确认 → Arthas深入分析细节

四、总结

场景 Arthas BTrace
快速查看线程/方法耗时/参数 最佳选择 不适合
追踪方法内部的new操作 做不到 唯一能做
持续监控GC与业务的关联 做不到 可以
条件过滤+累计统计耗时 做不到 可以
跨方法调用链持续追踪 做不到 可以

BTrace不是Arthas的替代品,而是Arthas的互补。当Arthas这把手电筒照不到的时候,上BTrace这个监控摄像头。

一句话总结:Arthas解决"现在出了什么问题",BTrace解决"这个问题持续多久了、频率多高、条件是什么"。

相关推荐
王码码20352 小时前
Go语言中的配置管理:从Viper到环境变量
后端·golang·go·接口
Bug终结者_3 小时前
别只会写 Java 了!LangChain4J 带你弯道超车 AI 赛道
后端·langchain·ai编程
Oneslide3 小时前
MySQL性能排查实战:大量Sleep空闲连接导致数据库写入缓慢解决方案
后端
码界奇点3 小时前
基于Spring Boot的前后端分离商城系统设计与实现
java·spring boot·后端·java-ee·毕业设计·源代码管理
fox_lht4 小时前
7.3.结构体-方法
开发语言·后端·rust
掘金者阿豪4 小时前
一个权限配置错误引发的“血案”:数据库访问控制手记
后端
消失的旧时光-19434 小时前
Spring Boot 接口设计进阶:POST / PUT / DELETE 的本质区别与工程实践
spring boot·后端
StackNoOverflow4 小时前
Spring Cloud的注册中心和配置中心(Nacos)
后端·spring cloud
SamDeepThinking5 小时前
秒杀系统需求PRD
java·后端·架构