Arthas 是 Alibaba 开源的 Java 诊断工具,开发者无需修改代码或重启 JVM 就可以排查生产环境问题,本质上使用 agent 机制,动态的修改字节码,从而实现插桩。
本文会对 Arthas 启动流程分析,从启动流程来理解 Arthas 工作原理。
Arthas 启动方式 根据 Arthas github README 所示,Arthas 提供了两种启动方式:
Use arthas-boot(Recommended)
Use as.sh
官方更推荐第一种启动方式,这种方式需要在本地下载 arthas-boot jar 包,通过 java -jar 命令启动arthas-boot jar。第二种方式通过脚本方式,会从云端下载相应的 jar 包,之后通过命令行进行启动。这两种方式实现方式相同,目的为了运行 arthas-core.jar。
Bootstrap 源码分析
com.taobao.arthas.boot.Bootstrap
arthas 的 boot模块会在打包时打包成 arthas-boot.jar,首先需要找到该模块的启动类,如 pom.xml 声明的 main-class 属性所示,该 jar 在启动时会运行 Bootstrap 类。
Bootstrap 类使用了 @Description 注解,该注解标注了 arthas-boot 的启动 usage,若读者不熟悉启动参数的话,可以阅读该类的 @Description 注解。
main 方法作为该类的入口点,该方法目的是收集启动 arthas-boot.jar 时传入参数,然后将这些参数作为启动 arthas-core.jar 参数。换句话说,该方法是为了启动 arthas-core jar。因篇幅问题,代码就不完全贴出来了,读者可以在该方法中找到如下的代码,ProcessUtils#startArthasCore 方法中会将参数拼接成一个命令行,执行该命令行,从而启动 arthas-core。
1 ProcessUtils.startArthasCore(pid, attachArgs);
Arthas 源码分析
com.taobao.arthas.core.Arthas
在 main 方法中,将 args 用于创建 Arthas 实例。之后来看到 Arthas 的构造方法,首先调用 parse 将 args 解析成 Configure 实例,然后调用了 attachAgent 方法。因为解析参数流程是无关重要的,所以我们重点关注 attachAgent 方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 public static void main (String[] args) { try { new Arthas(args); } catch (Throwable t) { AnsiLog.error("Start arthas failed, exception stack trace: " ); t.printStackTrace(); System.exit(-1 ); } } private Arthas (String[] args) throws Exception { attachAgent(parse(args)); }
attachAgent 中 3-9 行代码,目的是为了从 VirtualMachine 列表中查找到与之 pid 对应的 VirtualMachineDescriptor,如若未找到则通过 VirtualMachine#attach(String) 方法直接获取 VirtualMachine,反之则通过 VirtualMachine#attach(VirtualMachineDescriptor) 获取 VirtualMachine。
注意 attach 是重载方法
获取到 VirtualMachine 后,调用其 loadAgent 方法加载 arthas-agent jar 包,从而实现后续操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 private void attachAgent (Configure configure) throws Exception { VirtualMachineDescriptor virtualMachineDescriptor = null ; for (VirtualMachineDescriptor descriptor : VirtualMachine.list()) { String pid = descriptor.id(); if (pid.equals(Long.toString(configure.getJavaPid()))) { virtualMachineDescriptor = descriptor; break ; } } VirtualMachine virtualMachine = null ; try { if (null == virtualMachineDescriptor) { virtualMachine = VirtualMachine.attach("" + configure.getJavaPid()); } else { virtualMachine = VirtualMachine.attach(virtualMachineDescriptor); } Properties targetSystemProperties = virtualMachine.getSystemProperties(); String targetJavaVersion = JavaVersionUtils.javaVersionStr(targetSystemProperties); String currentJavaVersion = JavaVersionUtils.javaVersionStr(); if (targetJavaVersion != null && currentJavaVersion != null ) { if (!targetJavaVersion.equals(currentJavaVersion)) { AnsiLog.warn("Current VM java version: {} do not match target VM java version: {}, attach may fail." , currentJavaVersion, targetJavaVersion); AnsiLog.warn("Target VM JAVA_HOME is {}, arthas-boot JAVA_HOME is {}, try to set the same JAVA_HOME." , targetSystemProperties.getProperty("java.home" ), System.getProperty("java.home" )); } } String arthasAgentPath = configure.getArthasAgent(); configure.setArthasAgent(encodeArg(arthasAgentPath)); configure.setArthasCore(encodeArg(configure.getArthasCore())); try { virtualMachine.loadAgent(arthasAgentPath, configure.getArthasCore() + ";" + configure.toString()); } catch (IOException e) { if (e.getMessage() != null && e.getMessage().contains("Non-numeric value found" )) { AnsiLog.warn(e); AnsiLog.warn("It seems to use the lower version of JDK to attach the higher version of JDK." ); AnsiLog.warn( "This error message can be ignored, the attach may have been successful, and it will still try to connect." ); } else { throw e; } } } finally { if (null != virtualMachine) { virtualMachine.detach(); } } }
AgentBootstrap 源码分析
com.taobao.arthas.agent334.AgentBootstrap
调用 VirtualMachine#loadAgent 加载 agent 后,AgentBootstrap 作为 agent 启动点,然后启动该类的 agentMain 方法,该方法直接调用了 main(String args, final Instrumentation inst) 方法。
1 2 3 public static void agentmain (String args, Instrumentation inst) { main(args, inst); }
该方法首先加载 java.arthas.SpyAPI 类,SpyAPI 起到了方法拦截通知作用。之后通过 getClassLoader 方法来创建 ArthasClassloader 实例,然后创建一个新的线程,该线程调用了 bind 方法对 Arthas 进行绑定。
1 2 3 4 5 6 7 8 9 10 11 12 final ClassLoader agentLoader = getClassLoader(inst, arthasCoreJarFile);Thread bindingThread = new Thread() { @Override public void run () { try { bind(inst, agentLoader, agentArgs); } catch (Throwable throwable) { throwable.printStackTrace(ps); } } };
bind 方法实现很简单,首先加载 ArthasBootstrap 类,通过反射获取该类 getInstance 方法,之后进行调用。ArthasBootstrap#getInstance 方法会创建 ArthasBootstrap 实例,然后将其返回。
ArthasBootstrap 与当前的 ArthasBootstrap 是不同的,这里加载的 ArthasBootstrap 是 com.taobao.arthas.core.server 包下的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 private static void bind (Instrumentation inst, ClassLoader agentLoader, String args) throws Throwable { Class<?> bootstrapClass = agentLoader.loadClass(ARTHAS_BOOTSTRAP); Object bootstrap = bootstrapClass.getMethod(GET_INSTANCE, Instrumentation.class, String.class).invoke(null , inst, args); boolean isBind = (Boolean) bootstrapClass.getMethod(IS_BIND).invoke(bootstrap); if (!isBind) { String errorMsg = "Arthas server port binding failed! Please check $HOME/logs/arthas/arthas.log for more details." ; ps.println(errorMsg); throw new RuntimeException(errorMsg); } ps.println("Arthas server already bind." ); }
最后看到 com.taobao.arthas.core.server.ArthasBootstrap 的构造方法,该方法主要做了如下的操作:
初始化 spy
初始化 arthas 环境变量
初始化 Logger
增强 ClassLoader
初始化 bean
启动 arthas agent Server:启动 Server,接收客户端命令,来调用指定的方法将其结果返回给客户端
最后创建了 TransformerManager 实例,用于管理 watchTransformers、traceTransformers、reTransformers 类型的 ClassFileTransformer,这些 ClassFileTransformer 会对 class 进行不同方面的增强,从而实现 Arthas 不同的功能。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 private ArthasBootstrap (Instrumentation instrumentation, Map<String, String> args) throws Throwable { this .instrumentation = instrumentation; initFastjson(); initSpy(); initArthasEnvironment(args); String outputPathStr = configure.getOutputPath(); if (outputPathStr == null ) { outputPathStr = ArthasConstants.ARTHAS_OUTPUT; } outputPath = new File(outputPathStr); outputPath.mkdirs(); loggerContext = LogUtil.initLogger(arthasEnvironment); enhanceClassLoader(); initBeans(); bind(configure); executorService = Executors.newScheduledThreadPool(1 , new ThreadFactory() { @Override public Thread newThread (Runnable r) { final Thread t = new Thread(r, "arthas-command-execute" ); t.setDaemon(true ); return t; } }); shutdown = new Thread("as-shutdown-hooker" ) { @Override public void run () { ArthasBootstrap.this .destroy(); } }; transformerManager = new TransformerManager(instrumentation); Runtime.getRuntime().addShutdownHook(shutdown); }
Arthas 启动流程分析就到此为止了,本文并未进行深入的讲解,后续读者可以围绕着 agent Server 启动流程、TransformerManager、XXXCommand 类进行分析,从而深入理解 Arthas 实现原理。
参考文档
Arthas Github