一. 简介

Arthas是阿里在2019年9月份开源的一款java在线诊断工具,可能剖析、诊断、定位java利用问题,例如:JVM信息、线程信息、搜寻类中的办法、 跟踪代码执行、观测办法的入参和返回参数等等。

Arthas最大的特点是能在不批改代码和不须要从新公布的状况下,对业务问题进行诊断,包含查看办法调用的出参入参、异样、监测办法执行耗时、类加载信息等,大大晋升线上问题排查效率。

二. 实用场景

  1. 线上环境某个办法数据处理有问题,但没有日志埋点等记录入参和出参信息,无奈debug,并且本地环境无奈重现问题
  2. 线上接口调用响应迟缓,耗时高,但接口逻辑简单,接口外部又调用很多其余零碎的接口或第三方的jar,不确定是哪个办法导致的耗时高,无奈定位到具体方法
  3. 出问题的办法被执行的门路十分多,无奈确定该办法是在哪些具体的中央被调用或执行,这个办法也可能是第三方的jar包里的
  4. 无奈确定线上环境是否是最新提交的代码,只能把服务器上的class文件下载下来应用反编译工具关上确认
  5. 线上呈现偶发问题或只是某些条件下才会触发,通过日志不容易排查

三. 装置应用

目前的arthas版本都是基于命令行的交互方式,所以上面会依照下面的实用场景列出一些重要和罕用的命令,全副命令请查看官网装置。

这里有一个坑,如果在widows环境装置,本地之前装置了多个版本的jdk,在Attach到指标过程时有可能会提醒tools.jar包找不到的异样,如下图(没有这个问题能够疏忽):

因为Arthas应用了非零碎环境变量版本的jdk运行本身,而不是环境变量JAVA_HOME设置的jdk,能够先切换到JAVA_HOME设置的目录,而后再运行 java -jar arthas-boot.jar 即可,这个算是arthas的一个bug,后续版本会优化掉。

四. 罕用指令

  1. watch命令(察看指定办法的调用状况,包含返回值、异样、入参、对象属性值)

watch命令还能够依据耗时和具体的入参条件筛选过滤,只有合乎Ognl语法,能够满足很多监控维度,如:基于Ognl的一些非凡语法

  1. trace命令(办法外部调用门路,并输入办法门路上的每个节点上耗时)

该命令次要用于统计整个调用链路上的所有性能开销和追踪调用链路,应用下来感觉这个命令也是很有用的,包含本地环境,尤其是要排查接口响应工夫慢这样的场景下,能够疾速定位到具体哪个办法或哪些办法导致的,甚至包含第三方jar包的办法

  1. stack命令(输入以后办法被调用的门路),同样也能够查看依赖的jar里的办法被谁调用
  2. tt命令(time tunnel 时间轴,记录下指定办法每次调用的入参和返回信息)

相当于watch指令的屡次记录)但watch命令须要提前察看并拼写表达式,tt则不须要,这里着重说下 -n 参数,当你执行一个调用量不高的办法时可能你还能有足够的工夫用 CTRL+C 中断 tt 命令记录的过程,但如果遇到调用量十分大的办法,霎时就能将你的 JVM 内存撑爆!当咱们改了问题后,比方改了配置,须要在线上测试下是否修复的时候,可能会用到该性能,因为环境和数据的问题本地可能无奈验证,但线上环境不可能让用户再调用一次,所以这个参数 -p 就能够再从新发动一次调用。然而是由阿尔萨斯外部发动的线程实现的,所以调用方不一样,而且如果之前的调用数据有从threaLocal里获取的话,这次调用代码里也无奈获取,应用时须要留神。其实最重要的还是要结合实际场景,因为线上实在环境去模仿用户再次发动调用如果牵涉到下单或领取流程的话还是要谨慎的,否则可能引起一些非幂等的结果。

  1. jobs 后盾异步工作命令

当线上呈现偶发的问题时,比方须要watch某个条件,而这个条件一天可能才会呈现一次时,这种状况能够应用异步工作将命令在后盾运行,而且能够保留到指定的文件,不便查看。

这里须要留神:应用异步工作时,请勿同时开启过多的后盾异步命令,免得对指标JVM性能造成影响

  1. redefine命令(加载内部的.class文件)

相似于热加载或热修复的性能,批改java文件后,将替换掉jvm已加载的class类,然而因为jdk自身的限度,批改的class文件里不容许新减少成员变量和办法。

基于这个性能能够模仿一个简略的监控性能,比方在java文件的某个办法里加上调用耗时和申请参数的打印性能,而后应用redefine即可看到该办法的耗时工夫和参数值,并且不必重启服务。

  1. jad命令(反编译指定已加载类的源码,能够查看部署在线上服务器的.class文件对应的java源码)

该性能基于一个第三方的反编译工具CFR实现

全副命令请查看官网文档: Arthas用户文档

五. 实现原理

  • Java Agent
  • JDK Instrumentation 和 Attach API 机制
  • ASM字节码加强技术
  • JVMTI
  1.  sun.instrument.InstrumentationImpl 通过instrument机制]的实现能够构建一个独立于应用程序的代理程序Agent,再联合attach机制来绑定咱们的应用程序的pid就能够实现监控和帮助运行在JVM上的程序,还能够替换和批改类的定义(次要通过redefine,addTransformer函数),比方实现虚拟机级别反对的AOP实现形式。attach机制能够提供一种jvm过程间通信的能力,能让一个过程传命令给另外一个过程,并让它执行外部的一些操作,instrument 和AttachAPI 是btrace,greys,arthas等监控工具的原理根底。
  2. ASM是一个java字节码操作框架,它能被用来动静生成类或者加强既有类的性能。ASM能够从类文件中读入信息后,可能扭转类行为,剖析类信息,可能依据用户要求生成新类,当然除了asm还有javassist字节码工具,尽管在反射性能上不如asm(但必定优于jdk原生的反射),但提供了基于java语法实现操作字节码api,学习老本上比asm低。
  3. JVMTI是Java虚拟机所提供的 native 编程接口,下面提到的instrument 底层就是基于此实现的,JVMTI提供了可用于 debug 和 profiler 的接口,在 Java 5/6 中,虚拟机接口也减少了监听(Monitoring),线程剖析(Thread analysis)以及覆盖率剖析(Coverage Analysis)等性能。正是因为 JVMTI的弱小性能,它是实现 Java 调试器,以及其它 Java 运行态测试与剖析工具的根底,Instrumentation底层也是基于JVMTI实现的。另外还有Eclipse,IntellJ Idea 等编译期的debug性能都是基于JPDA(Java Platform Debugger Architecture)实现的,如下图:

Arthas正是应用Java的Instrumentation个性,联合ASM等第三方字节码操作框架的动静加强性能来实现的(外围性能实现在 com.taobao.arthas.core.advisor.Enhancer enhance() 办法中)

六. 源码剖析

源码局部目前只列出次要实现, 一些细节来不及看, 感兴趣的能够本人去git上下载下来看 https://github.com/alibaba/arthas

依据官网入门手册里的 java -jar arthas-boot.jar 可知程序入口在这个jar包下, 查看META-INF下的MANIFEST.MF文件可知(SPI机制)

这是java的一种机制, 告知jdk jar包执行入口通过.MF, 具体可参考 java.util.ServiceLoader 实现, 感兴趣的也能够理解下 SPI 机制

上面是疏导程序Bootstrap的入口main办法, 只列出次要代码逻辑, 可对照源码查看, 上面的所有代码剖析中加正文"//"阐明的都是要害中央

public static void main(String[] args) throws ParserConfigurationException, SAXException, IOException, ClassNotFoundException, NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {    ...... 省略局部代码AnsiLog.info("Try to attach process " + pid);    AnsiLog.debug("Start arthas-core.jar args: " + attachArgs);    ProcessUtils.startArthasCore(pid, attachArgs); //加载arthas-agent.jar和arthas-core.jar, startArthasCore办法次要是利用了tool.jar这个包中的VirtualMachine.attach(pid)来实现    AnsiLog.info("Attach process {} success.", new Object[]{pid});    ......             Class<?> telnetConsoleClas = classLoader.loadClass("com.taobao.arthas.client.TelnetConsole"); //通过反射机制调用控制台命令行交互    Method mainMethod = telnetConsoleClas.getMethod("main", String[].class); //TelnetConsole用到了JLine工具, JLine是一个用来解决控制台输出的Java类库,能够轻松实现Java命令行输出}

通过下面的startArthasCore()办法外部ProcessBuilder类调用 arthas-core.jar 的过程服务, 上面就是arthas-core.jar包和入口执行类, 同样也能够通过查看MANIFEST.MF取得,

上面的attachAgent办法正是应用了tool.jar这个包中的VirtualMachine.attach(pid)来实现,同时下面加载了自定义的agent代理,见上面 virtualMachine.loadAgent

这样就建设了连贯,在运行前或者运行时,将自定义的 Agent加载并和 VM 进行通信

Main-Class: com.taobao.arthas.core.Arthas-------------------------------------------------------------------------- private void attachAgent(Configure configure) throws Exception {    VirtualMachineDescriptor virtualMachineDescriptor = null;    Iterator var3 = VirtualMachine.list().iterator();    String targetJavaVersion;    while(var3.hasNext()) {        VirtualMachineDescriptor descriptor = (VirtualMachineDescriptor)var3.next();        targetJavaVersion = descriptor.id();        if (targetJavaVersion.equals(Integer.toString(configure.getJavaPid()))) {            virtualMachineDescriptor = descriptor;        }    }    VirtualMachine virtualMachine = null;    try {        if (null == virtualMachineDescriptor) {            virtualMachine = VirtualMachine.attach("" + configure.getJavaPid()); //外围性能正是调用了com.sun.tools.attach.VirtualMachine类, 底层又调用了WindowsAttachProvider类, 这个类又是调用jdk的native办法实现的        } else {            virtualMachine = VirtualMachine.attach(virtualMachineDescriptor);        }        Properties targetSystemProperties = virtualMachine.getSystemProperties();        targetJavaVersion = targetSystemProperties.getProperty("java.specification.version");        String currentJavaVersion = System.getProperty("java.specification.version");        if (targetJavaVersion != null && currentJavaVersion != null && !targetJavaVersion.equals(currentJavaVersion)) {            AnsiLog.warn("Current VM java version: {} do not match target VM java version: {}, attach may fail.", new Object[]{currentJavaVersion, targetJavaVersion});            AnsiLog.warn("Target VM JAVA_HOME is {}, try to set the same JAVA_HOME.", new Object[]{targetSystemProperties.getProperty("java.home")});        }        virtualMachine.loadAgent(configure.getArthasAgent(), configure.getArthasCore() + ";" + configure.toString()); //这里通过loadAgent将咱们自定义的Agent(arthas-core.jar)加载并和咱们应用程序所在的JVM进行通信    } finally {        if (null != virtualMachine) {            virtualMachine.detach();        }    }}

而后是arthas-agent.jar代理包的MANIFEST.MF文件, 该jar曾经被第一步arthas-boot.jar里的ProcessUtils.startArthasCore办法加载

Manifest-Version: 1.0Premain-Class: com.taobao.arthas.agent.AgentBootstrap //jdk5的intrument机制,只能反对jvm启动前指定监控的类Built-By: hengyunabcAgent-Class: com.taobao.arthas.agent.AgentBootstrap //jdk6之后对intrument机制改良,能够在jvm启动后实时批改类,arthas的很多性能都是通过这个设置失效的Can-Redefine-Classes: true //从新定义类, 正如下面介绍的redefine -p 指令一样, 通过这个属性设置告知jvmCan-Retransform-Classes: true //转换类, watch, trace, monitor等命令都是动静批改类, 和Redefine-Classes的区别是间接在现有加载的class字节码根底上批改, 不须要一个新的class文件替换Created-By: Apache Maven 3.5.3Build-Jdk: 1.8.0_181--------------------------------------------------------------------------public static void premain(String args, Instrumentation inst) { //同上,main办法执行前,jdk5的intrument机制, 这里你曾经拿到了Instrumentation对象实例    main(args, inst);} public static void agentmain(String args, Instrumentation inst) { //main执行后, jdk6的intrument机制, 这里你曾经拿到了Instrumentation对象实例    main(args, inst);}private static synchronized void main(String args, final Instrumentation inst) {    try {        ps.println("Arthas server agent start...");        int index = args.indexOf(59);        String agentJar = args.substring(0, index);        final String agentArgs = args.substring(index, args.length());        File agentJarFile = new File(agentJar); //拿到arthas-agent.jar        if (!agentJarFile.exists()) {            ps.println("Agent jar file does not exist: " + agentJarFile);        } else {            File spyJarFile = new File(agentJarFile.getParentFile(), "arthas-spy.jar"); //拿到arthas-spy.jar, spy外面次要是些钩子类,基于aop有前置办法,后置办法,这样动静加强类,实现相应command性能            if (!spyJarFile.exists()) {                ps.println("Spy jar file does not exist: " + spyJarFile);            } else {                final ClassLoader agentLoader = getClassLoader(inst, spyJarFile, agentJarFile); //类加载器加载agent和spy, 具体见上面的getClassLoader办法解析                initSpy(agentLoader); //初始化钩子,这外面次要是通过反射的形式获取AdviceWeaver编织类, 比方前置办法,后置办法, 并配合asm实现类的动静加强                Thread bindingThread = new Thread() {                    public void run() {                        try {                            AgentBootstrap.bind(inst, agentLoader, agentArgs); //bind办法又通过反射调用了arthas-core.jar的ArthasBootstrap.bind办法, bind办法这里就不列出了, 能够本人看下                        } catch (Throwable var2) {                            var2.printStackTrace(AgentBootstrap.ps);                        }                    }                };                bindingThread.setName("arthas-binding-thread");                bindingThread.start();                bindingThread.join();            }        }    } catch (Throwable var10) {        var10.printStackTrace(ps);        try {            if (ps != System.err) {                ps.close();            }        } catch (Throwable var9) {            ;        }        throw new RuntimeException(var10);    }} private static ClassLoader getClassLoader(Instrumentation inst, File spyJarFile, File agentJarFile) throws Throwable {    inst.appendToBootstrapClassLoaderSearch(new JarFile(spyJarFile)); //这里把spy增加到jdk的启动类加载器里, 就是咱们熟知的BootstrapClassLoader加载, 这样做的目标是为了上面的子加载器能共享spy, 我了解可能是很多命令都不是实时返回的,须要异步获取    return loadOrDefineClassLoader(agentJarFile); //而agent是交给arthas自定义的classLoader加载的, 这样做的目标应该是不对咱们的业务代码侵入}

接下来就看core外围包里的AgentBootstrap.bind办法做了什么

public void bind(Configure configure) throws Throwable {    long start = System.currentTimeMillis();    if (!this.isBindRef.compareAndSet(false, true)) {        throw new IllegalStateException("already bind");    } else {        try {            ShellServerOptions options = (new ShellServerOptions()).setInstrumentation(this.instrumentation).setPid(this.pid).setSessionTimeout(configure.getSessionTimeout() * 1000L);            this.shellServer = new ShellServerImpl(options, this); //ShellServer服务初始化, 应该就是咱们的命令行窗口服务            BuiltinCommandPack builtinCommands = new BuiltinCommandPack(); //这一步就是初始化下面讲到各种命令的类, 比方"watch,trace,redefine...", 每个命令对应一个Command类,具体怎么实现能够看下一个源码剖析            List<CommandResolver> resolvers = new ArrayList();            resolvers.add(builtinCommands);            if (configure.getTelnetPort() > 0) {//注册telnet通信形式, 这个注册办法应用了一个第三方的termd工具,termd是一个命令行程序开发框架(termd外部又是基于netty实现的通信,可见netty的弱小,韩国棒子思密达)                this.shellServer.registerTermServer(new TelnetTermServer(configure.getIp(), configure.getTelnetPort(), options.getConnectionTimeout()));            } else {                logger.info("telnet port is {}, skip bind telnet server.", new Object[]{configure.getTelnetPort()});            }            if (configure.getHttpPort() > 0) {                this.shellServer.registerTermServer(new HttpTermServer(configure.getIp(), configure.getHttpPort(), options.getConnectionTimeout())); //注册websocket通信形式            } else {                logger.info("http port is {}, skip bind http server.", new Object[]{configure.getHttpPort()});            }            Iterator var7 = resolvers.iterator();            while(var7.hasNext()) {                CommandResolver resolver = (CommandResolver)var7.next();                this.shellServer.registerCommandResolver(resolver); //注册命令解析器            }            this.shellServer.listen(new BindHandler(this.isBindRef));            logger.info("as-server listening on network={};telnet={};http={};timeout={};", configure.getIp(), new Object[]{configure.getTelnetPort(), configure.getHttpPort(), options.getConnectionTimeout()});            UserStatUtil.arthasStart(); //这里就是启动命令行服务器,开始监听,到这步就能够接管客户端的命令输出了            logger.info("as-server started in {} ms", new Object[]{System.currentTimeMillis() - start});        } catch (Throwable var9) {            logger.error((String)null, "Error during bind to port " + configure.getTelnetPort(), var9);            if (this.shellServer != null) {                this.shellServer.close();            }            throw var9;        }    }}

剩下的就可以看下罕用的命令是怎么实现逻辑了, 比方 redefine, watch, jad 等, 上面只列举了局部命令, 感兴趣的能够看源码, 大同小异。
RedefineCommand源码,对应"redefine"命令(每个命令都是继承AnnotatedCommand类,重写他的process办法实现)

public void process(CommandProcess process) {    if (this.paths != null && !this.paths.isEmpty()) {        ......省略局部代码        Instrumentation inst = process.session().getInstrumentation(); //还是通过Instrumentation实现        File file = new File(path); //path就是咱们的redefine -p 前面指定的class文件门路, 而后上面还会校验文件是否存在        f = new RandomAccessFile(path, "r"); //读取咱们批改的class为byte[]字节数组        ......省略局部代码        Class[] var25 = inst.getAllLoadedClasses(); //通过Instrumentation获取jvm所有加载的类            ......省略局部代码            try {                inst.redefineClasses((ClassDefinition[])definitions.toArray(new ClassDefinition[0])); //最终还是调用Instrumentation的redefineClasses办法实现的                process.write("redefine success, size: " + definitions.size() + "n");            } catch (Exception var18) {                process.write("redefine error! " + var18 + "n");            }            process.end();        }    }}

WatchCommand源码,对应"watch"指令(WatchCommand的实现是在EnhancerCommand里, 因为这个指令和trace,stack, tt等都有雷同的性能,所以放在父类里实现了)

public class Enhancer implements ClassFileTransformer {    public static synchronized EnhancerAffect enhance(Instrumentation inst, int adviceId, boolean isTracing, boolean skipJDKTrace, Matcher classNameMatcher, Matcher methodNameMatcher) throws UnmodifiableClassException {        ......省略局部代码        inst.addTransformer(enhancer, true); //将enhancer实例增加到转换器里,enhancer是ClassFileTransformer的实现类, ClassFileTransformer正是instrument的另一个要害组件,所有的转换实现都是基于ClassFileTransformer实现的        if (GlobalOptions.isBatchReTransform) {            ......省略局部代码                while(var17.hasNext()) {                    Class clazz = (Class)var17.next();                     try {                        inst.retransformClasses(new Class[]{clazz}); //从新转换指定的类,即动静批改原来的class文件,他和redefineClass办法的区别就是不须要源class文件,而是间接在现有的class文件上做批改,见上面的transform()办法                        logger.info("Success to transform class: " + clazz);                    } catch (Throwable var15) {                        ......省略局部代码                        throw new RuntimeException(var15);                    }                }            }        } finally {            inst.removeTransformer(enhancer);        }        return affect;    }      public byte[] transform(final ClassLoader inClassLoader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {        // 这个办法正是重载了ClassFileTransformer.transform办法, 通过asm字节码工具的ClassReader和ClassWriter实现批改咱们的class文件的        // 代码这里就不开展了(其实我也看不懂... 外部都是些字节码语法,如果是用javassist还勉强能看)    }}

最初一个JadCommand命令实现比较简单, 次要是通过一个第三方的反编译框架CFR实现的,cfr反对java8的一些新个性,比方lambda表达式的反编译, 对新的jdk反对比拟好

private void processExactMatch(CommandProcess process, RowAffect affect, Instrumentation inst, Set<Class<?>> matchedClasses, Set<Class<?>> withInnerClasses) {    ......省略局部代码    try {        ClassDumpTransformer transformer = new ClassDumpTransformer(allClasses);        Enhancer.enhance(inst, transformer, allClasses);        ......省略局部代码        String source = Decompiler.decompile(classFile.getAbsolutePath(), this.methodName); //decompile()办法就是通过CFR实现的反编译        ......省略局部代码        process.write("");        affect.rCnt(classFiles.keySet().size());    } catch (Throwable var12) {        logger.error((String)null, "jad: fail to decompile class: " + c.getName(), var12);    }}

总结:

通过下面的代码剖析咱们晓得了JDK的这两项性能: VirtualMachine Instrumentation

Arthas的整体逻辑也是在jdk的Instrumentation根底上实现的,所有加载的类会通过Agent加载,addTransformer之后再进行加强,

而后将对应的Advice织入进去,对于类的查找,办法的查找,都是通过SearchUtil来进行的,通过InstrumentloadAllClass办法将所有的JVM加载的class按名字进行匹配,再进行后续解决

这些机制在当前的工作中如果遇到相似的问题也会给咱们带来启发, 嗯, Instrumentation是个好货色 : )

七. 注意事项

  1. 只有利用在线上业务的诊断上,能力体现它的价值。然而真正将这种相似的技术落地还是有很多事件要做的,阿里也只是开源了他的源码,并没有开源他的具体实施过程,因为这个货色不可能让所有人都在线上搞的,必定有一套严格的审核权限机制,以及配合这个工具应用的相干配套设施,比方只能针对一台机器操作,线上环境个别都是集群部署,须要OPS和架构组的反对,在可行性上还有很多事件要做。
  2. 对应用程序所在的服务器性能的影响。个别命令使用不当的话,可能会撑爆jvm内存或导致应用程序响应变慢,命令的输入太多,接口调用太频繁会记录过多的数据变量到内存里,比方tt指令,倡议加 -n 参数 限度输入次数,sc * 通配符的使用不当,范畴过大,应用异步工作时,请勿同时开启过多的后盾异步命令,免得对指标JVM性能造成影响,一把双刃剑(它甚至能够批改jdk里的原生类),所以在线上运行必定是须要权限和流程管制的。

文章起源:http://javakk.com/153.html

八. 相干材料

git地址:https://github.com/alibaba/arthas

官网文档:https://alibaba.github.io/arthas/index.html