内容简介: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的流程图如下所示:
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
上面几个参数的作用简单讲一下:
- Premain-Class,前面提到过,包含了premain方法的类的全限定类名,JVM启动时调用premain-class的premain方法,如果是通过-javaagent参数传递的,该参数为必须项
- Agent-Class,和Premain-Class类似,动态attach到JVM时是必须参数
- Boot-Class-Path,可选参数,表示需要添加给bootstrap ClassLoader进行加载的路径,如果有多个路径通过空格进行分割
- Can-Redefine-Classes,可选参数,该agent是否需要重定义类,默认为false
- 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类及其子类进行校验,尽量保证我们监控代码的安全。
参考文档:
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- golang中xml的动态解析
- 成本计算引擎动态规则解析技术详解
- MyBatis使用动态表或列代码解析
- 基于CoreDNS和etcd实现动态域名解析
- 使用Go语言解析动态JSON格式的方法
- Leetcode动态规划之PHP解析(120. Triangle)
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
数据结构与算法分析
Frank.M.Carrano / 金名 / 清华大学出版社 / 2007-11 / 98.00元
“数据结构”是计算机专业的基础与核心课程之一,Java是现今一种热门的语言。本书在编写过程中特别考虑到了面向对象程序设计(OOP)的思想与Java语言的特性。它不是从基于另一种程序设计语言的数据结构教材简单地“改编”而来的,因此在数据结构的实现上更加“地道”地运用了Java语言,并且自始至终强调以面向对象的方式来思考、分析和解决问题。 本书是为数据结构入门课程(通常课号是CS-2)而编写的教......一起来看看 《数据结构与算法分析》 这本书的介绍吧!