btrace动态追踪技术解析

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

内容简介: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一些你不知道的事

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

查看所有标签

猜你喜欢:

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

内容创业:内容、分发、赢利新模式

内容创业:内容、分发、赢利新模式

张贵泉、张洵瑒 / 电子工业出版社 / 2018-6 / 49

越来越多的内容平台、行业巨头、资本纷纷加入内容分发的战争中,竞争非常激烈。优质的原创性内容将成为行业中最宝贵的资源。在这样的行业形势下,如何把内容创业做好?如何提高自身竞争力?如何在这场战争中武装自己?是每一位内容创业者都应该认真考虑的问题。 《内容创业:内容、分发、赢利新模式》旨在帮助内容创业者解决这些问题,为想要进入内容行业的创业者出谋划策,手把手教大家如何更好地进行内容创业,获得更高的......一起来看看 《内容创业:内容、分发、赢利新模式》 这本书的介绍吧!

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具

随机密码生成器
随机密码生成器

多种字符组合密码

MD5 加密
MD5 加密

MD5 加密工具