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配合使用
实际工作中,两者是互补的:
-
先用Arthas快速定位问题方向 :
thread看线程、trace看耗时、watch看参数 -
再用BTrace持续监控确认:写脚本持续采集数据,确认问题规律
-
最后用Arthas深入分析 :定位到具体方法后,
watch+stack深入看细节问题出现 → Arthas快速定位方向 → BTrace持续监控确认 → Arthas深入分析细节
四、总结
| 场景 | Arthas | BTrace |
|---|---|---|
| 快速查看线程/方法耗时/参数 | 最佳选择 | 不适合 |
| 追踪方法内部的new操作 | 做不到 | 唯一能做 |
| 持续监控GC与业务的关联 | 做不到 | 可以 |
| 条件过滤+累计统计耗时 | 做不到 | 可以 |
| 跨方法调用链持续追踪 | 做不到 | 可以 |
BTrace不是Arthas的替代品,而是Arthas的互补。当Arthas这把手电筒照不到的时候,上BTrace这个监控摄像头。
一句话总结:Arthas解决"现在出了什么问题",BTrace解决"这个问题持续多久了、频率多高、条件是什么"。