btrace动态追踪技术解析

栏目: Java · 发布时间: 7年前

内容简介:btrace动态追踪技术解析

开发环境定位问题手段较多,可以加日志、远程调试hotswap等,但在生产环境就没这么方便了,服务上线后就不能随便重启,比如某个接口有时候返回的数据异常,日志又没打印详情,这时候又想知道方法的入参是什么、是否调用了内部某个方法,或者接口响应时间较长想排查具体在哪个方法上调用比较耗时,这些场景都需要用到动态追踪的技术,btrace就是一个能帮助你分析和监控JVM的工具,采用了动态attach到目标JVM的方法,非侵入式监控,主要使用了JVMT(JVM Tool Interface)和Instrumentation技术,国内介绍btrace的文章并不多,最近正好要在部门内分享btrace的使用心得,因此整理了这篇文档,希望能够把btrace里的技术讲清楚。

btrace工作流程

btrace主要采用了Java Compiler API、ASM字节码修改技术、JVMT(JVM Tool Interface)和jdk1.6开始提供的Instrumentation技术,Java Compiler API用于在运行时把 java 源码编码成class文件;通过ASM字节码修改框架来实现对类的修改,通过tools.jar里提供的attach接口将btrace-agent 动态attach到目标JVM,实现非侵入式监控,btrace-agent会在目标JVM中创建一个Socket服务端,用于实现和btrace-client JVM的通信, btrace-agent会根据你的追踪脚本来生成字节码修改 工具 类,注册到ClassFileTransformer上,当JVM加载类时会调用ClassFileTransformer的transfrom方法(首次建立连接时会获取所有加载的类触发一次transform),btrace-agent会在transform()方法内对类的字节码进行修改,从而达到追踪的目标。

整个btrace的流程图如下所示:

btrace动态追踪技术解析

Instrumentation技术简介

instrumentation技术提供了在运行时修改类的字节码的入口,你可以在启动脚本中通过-javaagent:jarpath[=options]选项添加到虚拟机参数中,jarpath是agent jar的路径,可以提供一些参数给agent,agent需要自己解析传递进来的参数,agent jar包的manifest文件必须包含Premain-Class属性,这个值定义了agent class的入口,JVM在初始化后会调用agent-class的premain方法,premain方法的定义如下:

public static void premain(String agentArgs, Instrumentation inst);

如果agent class没有实现上述方法,JVM会尝试调用下面这个重载方法:

public static void premain(String agentArgs);

同时你也可以在agent class中添加一个agentmain方法,这个方法主要是用于在JVM启动之后动态attach到目标JVM后调用的,如果agent是通过命令行参数加载的,则agentmain方法不会被调用;如果agent class无法加载或者agent class没有合适的premain方法,又或者premain方法内部抛出了未捕捉到的异常,JVM会退出。

如果需要在JVM启动之后动态attach agent到目标JVM,需要在agent jar包manifest文件包含Agent-Class属性,值为agent-class的全限定名称,agent class必须实agentmain方法,和premain方法类似,JVM会先尝试调用下面的agentmain方法:

public static void agentmain(String agentArgs, Instrumentation inst);

如果找不到上面的方法则尝试调用下面的重载方法:

public static void agentmain(String agentArgs);

btrace源码分析

btrace-client启动过程

使用btrace时需要给btrace脚本传递目标进程的pid以及用于追踪的脚本(java源码),这部分代码的入口在com.sun.btrace.client.Main类的main方法,btrace客户端启动后会先调用Java编译api将追踪的脚本编译成class文件,编译之后attach btrace-agent到目标进程,代码如下所示:

//com.sun.btrace.client.Main
Client client = new Client(port, OUTPUT_FILE, PROBE_DESC_PATH,
    DEBUG, TRACK_RETRANSFORM, TRUSTED, DUMP_CLASSES, DUMP_DIR, statsdDef);
if (! new File(fileName).exists()) {
    errorExit("File not found: " + fileName, 1);
}
byte[] code = client.compile(fileName, classPath, includePath);
if (code == null) {
    errorExit("BTrace compilation failed", 1);
}
client.attach(pid, null, classPath);

上面的includePath是通过-cp启动参数传递给btrace客户端进程的,用于把-cp指定的路径动态添加到目标虚拟机的bootClasspath上,attach方法先找到btrace-agent.jar的路径,然后继续:

//com.sun.btrace.client.Client
public void attach(String pid, String sysCp, String bootCp) throws IOException {
	String agentPath = "/btrace-agent.jar";
	String tmp = Client.class.getClassLoader().getResource("com/sun/btrace").toString();
	tmp = tmp.substring(0, tmp.indexOf('!'));
	tmp = tmp.substring("jar:".length(), tmp.lastIndexOf('/'));
	agentPath = tmp + agentPath;
	agentPath = new File(new URI(agentPath)).getAbsolutePath();
	attach(pid, agentPath, sysCp, bootCp);  
}

attach方法里先把tools.jar的路径找出来,这个路径后面要添加到systemClassPath(appClassLoader的加载路径)上,tools.jar是JDK的一个工具类库,包括javac、attach以及监控jvm的工具集比如jstack、jmap、jstat的入口都在这里面,如果没有tools.jar就无法执行这些命令,然后通过VirtualMachine的attach方法获取到目标虚拟机,最后调用loadAgent方法将btrace-agent动态加载,代码如下:

//com.sun.btrace.client.Client
VirtualMachine vm = null;
vm = VirtualMachine.attach(pid);
String toolsPath = getToolsJarPath(
    serverVmProps.getProperty("java.class.path"),
    serverVmProps.getProperty("java.home")
);
if (sysCp == null) {
    sysCp = toolsPath;
} else {
    sysCp = sysCp + File.pathSeparator + toolsPath;
}
agentArgs += ",systemClassPath=" + sysCp;
vm.loadAgent(agentPath, agentArgs);

btrace-agent初始化过程

前面将btrace-agent.jar attach到目标jvm后,jvm会调用btrace-agent.jar的Manifest文件中的Agent-Class的agentMain方法,manifest文件内容如下:

Manifest-Version: 1.0
Premain-Class: com.sun.btrace.agent.Main
Agent-Class: com.sun.btrace.agent.Main
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Boot-Class-Path: btrace-boot.jar

上面几个参数的作用简单讲一下:

  1. Premain-Class,前面提到过,包含了premain方法的类的全限定类名,JVM启动时调用premain-class的premain方法,如果是通过-javaagent参数传递的,该参数为必须项
  2. Agent-Class,和Premain-Class类似,动态attach到JVM时是必须参数
  3. Boot-Class-Path,可选参数,表示需要添加给bootstrap ClassLoader进行加载的路径,如果有多个路径通过空格进行分割
  4. Can-Redefine-Classes,可选参数,该agent是否需要重定义类,默认为false
  5. Can-Retransform-Classes,可选参数,该agent是否需要对字节码修改,默认为false

agentMain方法首先解析btrace-client传递进来的参数,启动追踪脚本,然后会启动一个socket服务端用来和btrace-client进行通信,JVM在调用agentMain方法时会传递一个Instrumentation对象进来,Btrace就是通过Instrumentation来做文章,下面代码的最后面agent给Instrumentation添加了一个BTraceTransformer,这个BTraceTransformer继承自java.lang.instrument.ClassFileTransformer类,用于对类的字节码进行修改,agentMain的主要代码如下所示:

private static synchronized void main(final String args, final Instrumentation inst) {
	//把Instrumentation引用赋值给inst变量
    Main.inst = inst;
	try {
		loadArgs(args);
		//解析参数
		parseArgs();
		//启动脚本
		int startedScripts = startScripts();
		//另起线程启动socketServer监听客户端连接
		Thread agentThread = new Thread(new Runnable() {
		            @Override
		            public void run() {
		                BTraceRuntime.enter();
		                try {
		                    startServer();
		                } finally {
		                    BTraceRuntime.leave();
		                }
		            }
		        });
	} finally {
			//添加transformer到Instrumentation
	        inst.addTransformer(transformer, true);
	    }

startScripts()方法内部调用了loadBTraceScript()来加载btrace脚本,然后初始化ClientContext和FileClient对象,最后调用handleNewClient()方法:

private static boolean loadBTraceScript(String filePath, boolean traceToStdOut) {
	SharedSettings clientSettings = new SharedSettings();
    clientSettings.from(settings);
    clientSettings.setClientName(scriptName);
	ClientContext ctx = new ClientContext(inst, transformer, clientSettings);
    Client client = new FileClient(ctx, traceScript);
    if (client.isInitialized()) {
        handleNewClient(client).get();
        return true;
    }
}

handleNewClient方法内部会调用 client.retransformLoaded() 来将所有的类进行替换,替换时先获取JVM加载的所有类,然后过滤那些不可修改的以及不在候选范围内的类,也就是说只会对匹配到的类进行替换,比如替换你的Btrace脚本的OnMethod方法里引用的clazz,通过ASM插入一些追踪的代码:

void retransformLoaded() throws UnmodifiableClassException {
	if (runtime != null) {
	    if (probe.isTransforming() && settings.isRetransformStartup()) {
	        ArrayList<Class> list = new ArrayList<>();
	        ClassCache cc = ClassCache.getInstance();
	        for (Class c : inst.getAllLoadedClasses()) {
	            if (c != null) {
	                cc.get(c);
	                if (inst.isModifiableClass(c) &&  isCandidate(c)) {
	                    debugPrint("candidate " + c + " added");
	                    list.add(c);
	                }
	            }
	        }
	        list.trimToSize();
	        int size = list.size();
	        if (size > 0) {
	            Class[] classes = new Class[size];
	            list.toArray(classes);
				//调用BTraceTransformer执行修改
	            inst.retransformClasses(classes);   
	        }
	    }
	}
    }

在FileClient初始化过程中会去编译btrace追踪脚本,首先调用readScript()把文件转换成字节数组,然后调用init方法,init方法内部把字节数组封装成一个InstrumentCommand对象,最后调用loadClass()方法来完成btrace脚本的加载,loadClass()方法内部创建了一个BTraceProbePersisted,一个Probe相当于是一个探针,探测具体方法的调用,最后把probe注册到BTraceTransformer上,BTraceTransformer对象里会保存所有的Probe列表:

FileClient(ClientContext ctx, File scriptFile) throws IOException {
        super(ctx);
        if (!init(readScript(scriptFile))) {
            debug.warning("Unable to load BTrace script " + scriptFile);
        }
    }
private boolean init(byte[] code) throws IOException {
        InstrumentCommand cmd = new InstrumentCommand(code, new String[0]);
        boolean ret = loadClass(cmd, canLoadPack) != null;
        if (ret) {
            super.initialize();
        }
        return ret;
    }
protected final Class loadClass(InstrumentCommand instr, boolean canLoadPack) throws IOException {
	//从InstrumentCommand对象中获取字节数组
    String[] args = instr.getArguments();
    this.btraceCode = instr.getCode();
	//创建BTraceProbePersisted
    probe = load(btraceCode, canLoadPack);
    this.runtime = new BTraceRuntime(probe.getClassName(), args, this, debug, inst);
	//最后调用register方法把probe注册到transformer上
    return probe.register(runtime, transformer);

    }

最后来看下probe的register()方法的实现,主要是调用BTraceTransformer.register()方法注册一个probe,然后调用了BTraceProbeSupport的defineClass来加载追踪脚本,实际上是通过Unsafe类来加载的,也就是说追踪脚本类是由JVM的启动类加载器加载的:

public Class register(BTraceRuntime rt, BTraceTransformer t) {
        byte[] code = dataHolder;
        Class clz = delegate.defineClass(rt, code);
		//调用BTraceTransformer.register()方法注册一个probe
        t.register(this);
        this.transformer = t;
        this.rt = rt;
        return clz;
    }
private Class defineClassImpl(byte[] code, boolean mustBeBootstrap) {
    ClassLoader loader = null;
    if (! mustBeBootstrap) {
        loader = new ClassLoader(null) {};
    }
    Class cl = unsafe.defineClass(className, code, 0, code.length, loader, null);
    unsafe.ensureClassInitialized(cl);
    return cl;
}

ClassFileTransformer实现修改类

最后我们来看一下最为关键的BTraceTransformer类的实现,JDK 1.6提供的Instrument技术新增了java.lang.instrument.ClassFileTransformer接口,所有的要加载到JVM中的transformer都要实现这个接口,并重写transform()方法,JVM在加载类的时候会把该类对应的ClassLoader和字节数组传递给transform()方法,实现类可以修改字节数字并把修改后的值返回,需要特别注意的是btrace先会过滤掉classLoader为null(由引导类加载器加载的类,大部分为JVM的核心类库)和系统类加载器加载的类,主要是出于保护JVM核心功能的目的,通过ASM来实现对类的修改:

public synchronized byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
    if (probes.isEmpty()) return null;
    className = className != null ? className : "<anonymous>";

    if ((loader == null || loader.equals(ClassLoader.getSystemClassLoader())) && isSensitiveClass(className)) {
        return null;
    }

    if (filter.matchClass(className) == Filter.Result.FALSE) return null;

    boolean entered = BTraceRuntime.enter();
    try {
        BTraceClassReader cr = InstrumentUtils.newClassReader(loader, classfileBuffer);
        BTraceClassWriter cw = InstrumentUtils.newClassWriter(cr);
        for(BTraceProbe p : probes) {
            cw.addInstrumentor(p, loader);
        }
        byte[] transformed = cw.instrument();
        if (transformed == null) {
            // no instrumentation necessary
            return classfileBuffer;
        }
        return transformed;
    } catch (Throwable th) {
        throw th;
    } finally {
        if (entered) {
            BTraceRuntime.leave();
        }
    }
}

前面总结了btrace的工作流程,需要注意的是,btrace监控退出后,原先所有的class都不会被恢复,你的所有的监控代码依然一直在运行,同时为了减少对目标JVM的影响,btrace对追踪脚本做了较多限制,比如不能创建新对象和数组,不能捕捉和抛出异常等,btrace-client在编译完追踪脚本之后会进行校验,校验的详细内容在com.sun.btrace.compilerVerifier类中,感兴趣的同学可以看看, 在btrace-agent端也会通过com.sun.btrace.runtime.instr.MethodInstrumentor类及其子类进行校验,尽量保证我们监控代码的安全。

参考文档:

  1. VirtualMachine
  2. instrument
  3. JVMTI 和 Agent 实现
  4. btrace一些你不知道的事

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Algorithms Unlocked

Algorithms Unlocked

Thomas H. Cormen / The MIT Press / 2013-3-1 / USD 25.00

Have you ever wondered how your GPS can find the fastest way to your destination, selecting one route from seemingly countless possibilities in mere seconds? How your credit card account number is pro......一起来看看 《Algorithms Unlocked》 这本书的介绍吧!

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具

HSV CMYK 转换工具
HSV CMYK 转换工具

HSV CMYK互换工具