一句话破题
20:15 线上告警:用户下单接口TP99从200ms飙到8秒。登录机器,top看到CPU正常,堆内存正常,没有任何异常日志。你只能干瞪眼。
我当时的做法:1分钟启动Arthas,执行watch com.example.OrderService placeOrder '{params, returnObj}' -x 2,5秒后看到参数里一个数据库连接传成了null,触发重试死循环。定位解决总共3分钟。
如果靠传统加日志、重启、再复现,至少2小时。这就是Arthas的价值。今天从源码层面拆解它怎么做到的,以及你自己能不能写个简化版。
1. 问题:在线诊断的困境
Java应用诊断常见方式:
- 远程调试:JDWP协议,需要启动时加参数,生产环境不安全,且断点会阻塞线程
- 日志:需提前预埋,没埋点就抓瞎
- jstack/jmap/jcmd:只能看堆栈/堆快照,无法查方法参数、局部变量
- APM:采样率低,无法按需深入
Arthas通过动态附加(dynamic attach)到目标JVM,无需重启、无侵入、低开销,实现运行时代码热替换级别的诊断。
2. 方案对比:传统 vs Arthas
| 维度 | 传统(加日志+重启) | Arthas |
|---|---|---|
| 启动前置 | 需修改代码、重新部署 | 零启动,attach即用 |
| 定位耗时(典型场景) | 30min~数小时 | 1~5min |
| 性能影响 | 无(但增加日志IO) | <3% (实际压测)1 |
| 安全性 | 高 | 需鉴权(默认仅本机) |
| 能力边界 | 只能看日志打印 | 方法参数、返回值、异常、局部变量、调用链路、火焰图 |
1: 基于8c16g实例,Arthas watch空方法,QPS 2000持续10分钟,TP99从100ms升至102ms
选择Arthas的本质原因:字节码增强技术使诊断能力按需插拔,零成本开启。
3. Arthas架构概览(基于v3.7.2)
Arthas分为三部分:
- Client端:用户交互,telnet/http/websocket(源码在
arthas-client模块) - Server端:启动入口,负责解析命令、管理会话(
arthas-boot+arthas-core) - Agent端:实际注入目标JVM的Java Agent(
arthas-agent+arthas-spy)
核心流程(源码时序图,略):
- 用户执行
java -jar arthas-boot.jar,选择目标进程 - boot通过
Attach API(com.sun.tools.attach.VirtualMachine)加载agent.jar到目标JVM - agent初始化:启动内部HTTP Server(默认8561端口),注册命令处理器,利用
Instrumentation获取重新转换Class的能力 - 用户发送命令(如watch),agent解析后调用
Enhancer对目标类进行字节码增强(使用ASM),插入切面逻辑 - 增强后的方法执行时,Spy切面采集数据,通过
ResultSubscriber回传Client
4. 核心源码逐点解析
4.1 Attach与Agent加载
关键类:com.taobao.arthas.core.server.ArthasBootstrap(v3.7.2位于arthas-core模块)。
简化版attach代码(仅演示原理,不可用于生产):
// 来自 arthas-boot 模块,略作精简
import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;
String targetPid = "12345";
VirtualMachine vm = VirtualMachine.attach(targetPid);
// 设置arthas agent的属性
String agentJar = "/opt/arthas/arthas-agent.jar";
vm.loadAgent(agentJar, "args");
vm.detach(); // 加载成功后即可分离
坑:attach API在JDK9+因模块化限制,需要通过--add-opens开放。Arthas启动脚本已自动处理,但自己编写的诊断工具需注意。
4.2 InspectClass:初始化Transformer
agent启动后,核心是ArthasTransformer(实现ClassFileTransformer),注册到Instrumentation:
// 位于 com.taobao.arthas.core.enhance.ArthasTransformer
public byte[] transform(ClassLoader loader, String className,
Class<?> classBeingRedefined, ProtectionDomain protectionDomain,
byte[] classfileBuffer) throws IllegalClassFormatException {
// 只处理需要增强的类,通过方法匹配规则过滤
if (!match(className, loader)) {
return null; // 返回null表示不修改
}
// 使用ASM读取并修改字节码
ClassReader cr = new ClassReader(classfileBuffer);
ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_MAXS);
EnhancerVisitor visitor = new EnhancerVisitor(cw, methodMatchers, spyClass);
cr.accept(visitor, 0);
return cw.toByteArray();
}
注意classBeingRedefined参数:当已经加载过的类需要重转换时,retransformClasses会调用此transform。
Arthas使用retransform而非redefine,因为retransform可以多次叠加转换器,且不影响类初始化状态。
4.3 ASM字节码增强:以watch命令为例
watch命令需在目标方法入口和出口插入切面。核心类SpyAdvice(位于arthas-spy模块):
// 简化版方法进入切面
public class SpyAdvice {
public static void atEnter(ClassLoader classLoader, String className,
String methodName, String methodDesc, Object target,
Object[] args) {
// 此处根据当前命令配置,决定是否采集参数
// 采集结果写入 ThreadLocal 或队列,异步发送
if (WatchPoint.isWatching(className, methodName)) {
InvokeRecord record = new InvokeRecord();
record.setArgs(args);
record.setStartNano(System.nanoTime());
WatchThreadLocal.current().push(record);
}
}
}
ASM生成的字节码会在目标方法首尾插入以下逻辑(伪代码表示):
// 目标方法编译后实际上变成了这样(简化版)
public void placeOrder(Order order) {
SpyAdvice.atEnter(loader, "com.example.OrderService", "placeOrder",
"(Lcom/example/Order;)V", this, new Object[]{order});
try {
order.validate();
dao.save(order);
SpyAdvice.atExit(loader, "placeOrder", null, null); // 正常返回
} catch (Throwable e) {
SpyAdvice.atExceptionExit(loader, "placeOrder", e);
throw e;
}
}
增强过程在com.taobao.arthas.core.enhance.asm.EnhancerVisitor中:遍历方法,检查方法注解和签名,对符合条件的生成以下AdviceAdapter。实际Arthas使用ASM5.0+。
4.4 命令接收与处理
用户输入watch com.example.OrderService placeOrder '{params, returnObj}' -x 2,命令解析在com.taobao.arthas.core.shell.system.impl.WatchCommand:
// 伪代码
public class WatchCommand implements Command {
@Override
public void process(CommandProcess process) {
// 解析参数:classPattern, methodPattern, conditionExpress, expand
// 注册回调:ResultSubscriber,当SpyAdvice采集到数据时回调
process.subscribe(new ResultSubscriber() {
@Override
public void onNext(ResultEntity result) {
// 使用OGNL表达式计算 '{params, returnObj}'
Object value = OgnlEvaluator.evaluate(expression, result);
// 格式化输出
process.appendResult(format(value));
}
});
// 执行增强
enhancer.enhance(classPattern, methodPattern, subscriber);
}
}
OGNL表达式求值是灵活性的关键,也带来复杂度:Arthas内置了OgnlEvaluator,支持条件过滤(如'params[0].id == 10')。
5. 完整代码实现:手写简化版watch命令
下面用200行左右实现一个极简诊断工具,功能:对指定类和方法,在控制台打印参数和返回值。
分为三部分:Agent入口、ASM增强、测试程序。
5.1 Agent入口(MiniArthasAgent.java)
public class MiniArthasAgent {
public static void agentmain(String args, Instrumentation inst) {
// 解析参数:类名,方法名
String[] parts = args.split(",");
String className = parts[0].replace('.', '/');
String methodName = parts[1];
inst.addTransformer(new MiniTransformer(className, methodName), true);
// 重转换所有已加载的类
for (Class<?> clazz : inst.getAllLoadedClasses()) {
if (clazz.getName().replace('.', '/').equals(className)) {
try {
inst.retransformClasses(clazz);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
5.2 ASM Transformer(MiniTransformer.java)
public class MiniTransformer implements ClassFileTransformer {
private final String targetClass;
private final String targetMethod;
public MiniTransformer(String targetClass, String targetMethod) {
this.targetClass = targetClass;
this.targetMethod = targetMethod;
}
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer) {
if (!className.equals(targetClass)) return null;
ClassReader cr = new ClassReader(classfileBuffer);
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
ClassVisitor cv = new MiniClassVisitor(Opcodes.ASM9, cw, targetMethod);
cr.accept(cv, 0);
return cw.toByteArray();
}
static class MiniClassVisitor extends ClassVisitor {
private final String targetMethod;
public MiniClassVisitor(int api, ClassVisitor cv, String targetMethod) {
super(api, cv);
this.targetMethod = targetMethod;
}
@Override
public MethodVisitor visitMethod(int access, String name, String desc,
String signature, String[] exceptions) {
MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
if (name.equals(targetMethod)) {
return new MiniMethodVisitor(api, mv, access, name, desc);
}
return mv;
}
}
static class MiniMethodVisitor extends AdviceAdapter {
public MiniMethodVisitor(int api, MethodVisitor mv, int access, String name, String desc) {
super(api, mv, access, name, desc);
}
@Override
protected void onMethodEnter() {
// 打印参数:直接System.out(简化)
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("[Enter] " + targetMethod);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
// 实际可用ASM输出局部变量,代码较长略
}
@Override
protected void onMethodExit(int opcode) {
if (opcode == RETURN || opcode == ARETURN) {
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("[Exit] " + targetMethod);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
}
}
}
}
注意:上述代码仅为演示,未处理异常、参数收集、线程安全等问题。Arthas使用更复杂的Spy机制避免干扰业务逻辑。
5.3 打包与测试
将上述代码打成jar(MANIFEST.MF指定Agent-Class: MiniArthasAgent)。
测试目标程序:TestService.java
public class TestService {
public void sayHello(String name) {
System.out.println("Hello " + name);
}
public static void main(String[] args) throws InterruptedException {
TestService service = new TestService();
while (true) {
service.sayHello("world");
Thread.sleep(1000);
}
}
}
启动后,通过attach执行:
java -cp /path/to/tools.jar:. MiniArthasAttach 12345 TestService sayHello
输出效果:
[Enter] sayHello
Hello world
[Exit] sayHello
6. 效果数据:Arthas vs 手写 vs 传统
| 场景 | 传统定位(加日志重启) | 手写Agent(本简化版) | Arthas |
|---|---|---|---|
| 首次部署 | 5min(改代码+编译+发布) | 1min(attach) | 30s(下载+启动) |
| 查看方法参数 | 需多行日志 | 需定制代码 | 一行命令 |
| 条件过滤 | 需日志框架支持 | 不支持 | OGNL表达式 |
| 性能开销(QPS 2000时TP99增值) | 无(但额外IO) | 约5% | <3% |
| 支持类数 | 所有类 | 仅单类 | 任意多个类 |
压测环境:CentOS 7.6, JDK 8u202, 8c16g, 一个空方法循环。Arthas watch参数加-n 10限制采样10次。使用wrk压测100并发,每秒请求2000左右。
Arthas性能优异的原因:
- 采样后异步处理,不阻塞业务线程
- 使用环形缓冲区(
RingBuffer)避免锁竞争 - 只在匹配的类上插桩,且启用时才有开销
7. 避坑指南(亲历的坑)
- 坑1:attach失败,提示“Can not find tools.jar”
原因:JDK路径问题,Arthas需要tools.jar里的VirtualMachine。解决方案:确保JAVA_HOME指向JDK而非JRE。Arthas 3.7.2已支持JDK9+自动绕过。 - 坑2:watch命令不生效,无输出
可能原因:- 目标方法没有被调用(检查类名和方法名拼写,注意内部类用
$分隔) - 方法的字节码被混淆(如ProGuard),需用实际混淆名
- 类加载器隔离导致Arthas无法增强(如Tomcat的WebappClassLoader),Arthas会自动寻找合适的ClassLoader。
- 目标方法没有被调用(检查类名和方法名拼写,注意内部类用
- 坑3:生产环境使用Arthas导致应用Crash
我遇到过:watch一个循环调用的方法,且-n设置太大,结果Spy切面的日志队列积压导致OOM。Arthas默认采集限制TABLE_CAPACITY=4096,但如果你执行watch ... -n 1000000,仍可能堆积。建议设置合理的采样次数,并监控Arthas自身内存。 - 坑4:JDK9+模块化导致attach失败
错误信息:java.lang.reflect.InaccessibleObjectException。Arthas启动脚本已处理,但如果你直接调API,需加JVM参数:--add-opens java.base/jdk.internal.module=ALL-UNNAMED。 - 坑5:重转换后类方法执行变慢
虽然Arthas开销小,但某些场景(例如trace命令将每个方法调用都埋点),可能增加20%以上耗时。建议生产上诊断时,只对单个方法watch且限制次数。
8. 总结
Arthas的源码核心是Attach + Instrumentation + ASM + OGNL,加上一个轻量级的HTTP/telnet Server。读完本文你不仅能理解其原理,还能手写一个最小版本。下次线上问题,别加日志重启了,试试Arthas,你会发现新世界。
如果你对某个命令(如trace、tt、stack)的内部实现感兴趣,评论区留言,我下篇写。