最近去哪网刚开源了Bistoury,一个集线上问题debug和监控于一身工具框架,能很方便的在不停止jvm的情况的下,模拟eclipse的debug功能,并且不会像原始debug那样会阻塞其他进程,只会在断点处监控某一次的请求。bistoury底层借助了阿里的Arthas和唯品会的vjtools,以及asm、javassist字节码指令框架,并增强实现了debug中的断点Breakpoint模式,并以前端ui界面的形式直接展示和操作,可以说用来调试线上问题变得非常方便
https://github.com/qunarcorp/bistoury/blob/master/docs/cn/debug.md
NASA要发射一个新型火箭,火箭发射升空后发现不行,NASA把火箭拖回来加了两行log,再次发射,发现又不行,又加了两行log发射,发现又不行....
当然这只是一个笑话,但这样的场景在我们的实际开发中却屡见不鲜,多少次系统重启后问题复现失败,多少次我们解决故障的时间就在不断地加log,发布,加log,发布的过程中溜走...
究竟无侵入debug技术、无侵入埋点、无侵入监控怎么实现的?我对此非常感兴趣,简单说就是获取正在执行jvm进程对应的目录的class文件,修改后重新classload到jvm中。属于jvm在线的字节码增强技术。本文只使用了asm字节码简单实现整个流程。
比如要生成com.dc.agent.ChangePointA.java的asm指令代码块,则使用mytest项目中com.dc.mytest.asm.Test.java
package com.dc.mytest.asm;
import org.objectweb.asm.util.ASMifier;
public class Test {
public static void main(String[] args) throws Throwable {
ASMifier.main(new String[] {"com.dc.mytest.debug.PointA"});
}
}
public int b() {
long time = System.currentTimeMillis();
byte aa = 1;
short b = 2222;
int a = 111111;
long c = 3;
BigDecimal dd = new BigDecimal("22222222222222222");
String e = "e";
PointA t = new PointA();
System.out.println("方法执行时间cost="+(System.currentTimeMillis()-time));
return a;
}
再使用ASMifier.main(new String[] {"com.dc.mytest.debug.PointA"});生成上面这段代码的asm结构,字节码太多,这里只展示部分字节码asm的字节码如下。。
{
methodVisitor = classWriter.visitMethod(ACC_PUBLIC, "b", "()I", null, null);
methodVisitor.visitCode();
Label label0 = new Label();
methodVisitor.visitLabel(label0);
methodVisitor.visitLineNumber(21, label0);
methodVisitor.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
methodVisitor.visitVarInsn(LSTORE, 1);
Label label1 = new Label();
methodVisitor.visitLabel(label1);
methodVisitor.visitLineNumber(22, label1);
methodVisitor.visitInsn(ICONST_1);
methodVisitor.visitVarInsn(ISTORE, 3);
Label label2 = new Label();
methodVisitor.visitLabel(label2);
methodVisitor.visitLineNumber(23, label2);
methodVisitor.visitIntInsn(SIPUSH, 2222);
methodVisitor.visitVarInsn(ISTORE, 4);
Label label3 = new Label();
methodVisitor.visitLabel(label3);
methodVisitor.visitLineNumber(24, label3);
methodVisitor.visitLdcInsn(new Integer(111111));
methodVisitor.visitVarInsn(ISTORE, 5);
Label label4 = new Label();
methodVisitor.visitLabel(label4);
methodVisitor.visitLineNumber(25, label4);
methodVisitor.visitLdcInsn(new Long(3L));
methodVisitor.visitVarInsn(LSTORE, 6);
Label label5 = new Label();
methodVisitor.visitLabel(label5);
methodVisitor.visitLineNumber(26, label5);
methodVisitor.visitTypeInsn(NEW, "java/math/BigDecimal");
methodVisitor.visitInsn(DUP);
methodVisitor.visitLdcInsn("22222222222222222");
methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/math/BigDecimal", "<init>", "(Ljava/lang/String;)V", false);
methodVisitor.visitVarInsn(ASTORE, 8);
Label label6 = new Label();
methodVisitor.visitLabel(label6);
methodVisitor.visitLineNumber(27, label6);
methodVisitor.visitLdcInsn("e");
methodVisitor.visitVarInsn(ASTORE, 9);
Label label7 = new Label();
methodVisitor.visitLabel(label7);
methodVisitor.visitLineNumber(28, label7);
methodVisitor.visitTypeInsn(NEW, "com/dc/mytest/debug/PointA");
methodVisitor.visitInsn(DUP);
methodVisitor.visitMethodInsn(INVOKESPECIAL, "com/dc/mytest/debug/PointA", "<init>", "()V", false);
methodVisitor.visitVarInsn(ASTORE, 10);
Label label8 = new Label();
methodVisitor.visitLabel(label8);
methodVisitor.visitLineNumber(29, label8);
methodVisitor.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
methodVisitor.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
本次运行的demo,asm字节码指令代码我已经生成完,在com.dc.agent.ChangePointA文件中,将原来PointA的b方法中的代码int a = 111111;修改成init a = 411111,并加上方法执行时间打印。
---------------------------------asm介绍割-------------------------------------
两个项目:一个正常运行的项目(模拟线上项目运行)和一个agent项目,源代码托管到github
程序运行前,先去myagent项目目录下执行一下mvn clean install(或者idea/eclipse中run maven一下),target目录中会生成对应jar文件,并把Main.java中的F:\eclipse-workspace\myagent\target\myagent.jar 改成 自己本地的目录的url即可。。pom.xml文件中的jdk自行配置(目前使用jdk12最高版本,其他版本自行修改完打包即可)
执行流程:先运行MainThread.java得main方法,保持jvm在线,模拟项目正在运行中。然后查看进程pid再去执行Main.java得main方法,执行完毕后,可以看到运行期间PointA内存被动态修改,打印出和原来不一样得结果,即测试完成。 本demo的agent 动态修改了PointA得方法b()中的局部变量int a =111111;改成了int a=411111。并加入一段方法执行cost时间信息打印。(其他更强大的功能,只需要用asm加强实现一下就行,本次只是简单玩法)
package com.dc.mytest.debug;
import java.util.List;
import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;
public class MainThread {
public static void main(String[] args) throws InterruptedException {
PointA aa = new PointA();
PointB bb = new PointB();
PointB bcc = new PointB();
bb.toString();
new Thread(new Runnable() {
@Override
public void run() {
List<VirtualMachineDescriptor> list = VirtualMachine.list();
for (VirtualMachineDescriptor vmd : list) {
System.out.println("pid:" + vmd.id() + ":" + vmd.displayName());
}
while(true) {
try {
bcc.b();
System.out.println(aa.b()+"--"+bb.b());
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
}
pid:14068:com.dc.mytest.debug.MainThread
pid:8760:
111111--113333
111111--113333
111111--113333
111111--113333
111111--113333
111111--113333
111111--113333
111111--113333
package com.dc.mytest.debug;
import com.sun.tools.attach.VirtualMachine;
public class Main {
public static void main(String[] args) throws Exception {
VirtualMachine vm = null;
String agentjarpath = "F:\\eclipse-workspace\\myagent\\target\\myagent.jar"; // agentjar路径
vm = VirtualMachine.attach("14068");// 目标JVM的进程ID(PID)
vm.loadAgent(agentjarpath,"com.dc.mytest.debug.PointA");
vm.detach();
}
}
MainThread已经发生改变,打印如下:
pid:14068:com.dc.mytest.debug.MainThread
pid:8760:
111111--113333
111111--113333
111111--113333
111111--113333
111111--113333
111111--113333
111111--113333
111111--113333
111111--113333
Agent Main called..agentArgs=com.dc.mytest.debug.PointA
agentmain load Class :com.dc.mytest.debug.PointA
changeA
411111--113333
方法执行时间cost=501
411111--113333
方法执行时间cost=2000
411111--113333
方法执行时间cost=0