当前位置:   article > 正文

arthas源码分析_arthas-client

arthas-client

arthas简介

arthas 是Alibaba开源的Java诊断工具,基于jvm Agent方式,使用Instrumentation方式修改字节码方式以及使用java.lang.management包提供的管理接口的方式进行java应用诊断。详细的介绍可以参考官方文档。
官方文档地址:https://alibaba.github.io/arthas/
GitHub地址:https://github.com/alibaba/arthas/
本文主要分析arthas源码,主要分成下面几个部分:

  1. arthas组成模块
  2. arthas服务端代码分析
  3. arthas客户端代码分析

arthas组成模块

arthas有多个模块组成,如下图所示:

 

arthas模块图.png

  1. arthas-boot.jar和as.sh模块功能类似,分别使用java和shell脚本,下载对应的jar包,并生成服务端和客户端的启动命令,然后启动客户端和服务端。服务端最终生成的启动命令如下:

 

  1. ${JAVA_HOME}"/bin/java \
  2. ${opts} \
  3. -jar "${arthas_lib_dir}/arthas-core.jar" \
  4. -pid ${TARGET_PID} \ 要注入的进程id
  5. -target-ip ${TARGET_IP} \ 服务器ip地址
  6. -telnet-port ${TELNET_PORT} \ 服务器telnet服务端口号
  7. -http-port ${HTTP_PORT} \ websocket服务端口号
  8. -core "${arthas_lib_dir}/arthas-core.jar" \ arthas-core目录
  9. -agent "${arthas_lib_dir}/arthas-agent.jar" arthas-agent目录
  1. arthas-core.jar是服务端程序的启动入口类,会调用virtualMachine#attach到目标进程,并加载arthas-agent.jar作为agent jar包。
  2. arthas-agent.jar既可以使用premain方式(在目标进程启动之前,通过-agent参数静态指定),也可以通过agentmain方式(在进程启动之后attach上去)。arthas-agent会使用自定义的classloader(ArthasClassLoader)加载arthas-core.jar里面的com.taobao.arthas.core.config.Configure类以及com.taobao.arthas.core.server.ArthasBootstrap。 同时程序运行的时候会使用arthas-spy.jar。
  3. arthas-spy.jar里面只包含Spy类,目的是为了将Spy类使用BootstrapClassLoader来加载,从而使目标进程的java应用可以访问Spy类。通过ASM修改字节码,可以将Spy类的方法ON_BEFORE_METHODON_RETURN_METHOD等编织到目标类里面。Spy类你可以简单理解为类似spring aop的Advice,有前置方法,后置方法等。
  4. arthas-client.jar是客户端程序,用来连接arthas-core.jar启动的服务端代码,使用telnet方式。一般由arthas-boot.jar和as.sh来负责启动。

arthas服务端代码分析

前置准备

看服务端启动命令可以知道 从 arthas-core.jar开始启动,arthas-core的pom.xml文件里面指定了mainClass为com.taobao.arthas.core.Arthas,使得程序启动的时候从该类的main方法开始运行。Arthas源码如下:

 

  1. public class Arthas {
  2. private Arthas(String[] args) throws Exception {
  3. attachAgent(parse(args));
  4. }
  5. private Configure parse(String[] args) {
  6. // 省略非关键代码,解析启动参数作为配置,并填充到configure对象里面
  7. return configure;
  8. }
  9. private void attachAgent(Configure configure) throws Exception {
  10. // 省略非关键代码,attach到目标进程
  11. virtualMachine = VirtualMachine.attach("" + configure.getJavaPid());
  12. virtualMachine.loadAgent(configure.getArthasAgent(),
  13. configure.getArthasCore() + ";" + configure.toString());
  14. }
  15. public static void main(String[] args) {
  16. new Arthas(args);
  17. }
  18. }
  1. Arthas首先解析入参,生成com.taobao.arthas.core.config.Configure类,包含了相关配置信息
  2. 使用jdk-tools里面的VirtualMachine.loadAgent,其中第一个参数为agent路径, 第二个参数向jar包中的agentmain()方法传递参数(此处为agent-core.jar包路径和config序列化之后的字符串),加载arthas-agent.jar包,并运行
  3. arthas-agent.jar包,指定了Agent-Class为com.taobao.arthas.agent.AgentBootstrap,同时可以使用Premain的方式和目标进程同时启动

 

  1. <manifestEntries>
  2. <Premain-Class>com.taobao.arthas.agent.AgentBootstrap</Premain-Class>
  3. <Agent-Class>com.taobao.arthas.agent.AgentBootstrap</Agent-Class>
  4. </manifestEntries>

其中Premain-ClasspremainAgent-Classagentmain都调用main方法。
main方法主要做4件事情:

  1. 找到arthas-spy.jar路径,并调用Instrumentation#appendToBootstrapClassLoaderSearch方法,使用bootstrapClassLoader来加载arthas-spy.jar里的Spy类。
  2. arthas-agent路径传递给自定义的classloader(ArthasClassloader),用来隔离arthas本身的类和目标进程的类。
  3. 使用 ArthasClassloader#loadClass方法,加载com.taobao.arthas.core.advisor.AdviceWeaver类,并将里面的methodOnBeginmethodOnReturnEndmethodOnThrowingEnd等方法取出赋值给Spy类对应的方法。同时Spy类里面的方法又会通过ASM字节码增强的方式,编织到目标代码的方法里面。使得Spy 间谍类可以关联由AppClassLoader加载的目标进程的业务类和ArthasClassloader加载的arthas类,因此Spy类可以看做两者之间的桥梁。根据classloader双亲委派特性,子classloader可以访问父classloader加载的类。源码如下:

 

  1. private static ClassLoader getClassLoader(Instrumentation inst, File spyJarFile, File agentJarFile) throws Throwable {
  2. // 将Spy添加到BootstrapClassLoader
  3. inst.appendToBootstrapClassLoaderSearch(new JarFile(spyJarFile));
  4. // 构造自定义的类加载器ArthasClassloader,尽量减少Arthas对现有工程的侵蚀
  5. return loadOrDefineClassLoader(agentJarFile);
  6. }
  7. private static void initSpy(ClassLoader classLoader) throws ClassNotFoundException, NoSuchMethodException {
  8. // 该classLoader为ArthasClassloader
  9. Class<?> adviceWeaverClass = classLoader.loadClass(ADVICEWEAVER);
  10. Method onBefore = adviceWeaverClass.getMethod(ON_BEFORE, int.class, ClassLoader.class, String.class,
  11. String.class, String.class, Object.class, Object[].class);
  12. Method onReturn = adviceWeaverClass.getMethod(ON_RETURN, Object.class);
  13. Method onThrows = adviceWeaverClass.getMethod(ON_THROWS, Throwable.class);
  14. Method beforeInvoke = adviceWeaverClass.getMethod(BEFORE_INVOKE, int.class, String.class, String.class, String.class);
  15. Method afterInvoke = adviceWeaverClass.getMethod(AFTER_INVOKE, int.class, String.class, String.class, String.class);
  16. Method throwInvoke = adviceWeaverClass.getMethod(THROW_INVOKE, int.class, String.class, String.class, String.class);
  17. Method reset = AgentBootstrap.class.getMethod(RESET);
  18. Spy.initForAgentLauncher(classLoader, onBefore, onReturn, onThrows, beforeInvoke, afterInvoke, throwInvoke, reset);
  19. }

classloader关系如下:

 

  1. +-BootstrapClassLoader
  2. +-sun.misc.Launcher$ExtClassLoader@7bf2dede
  3. +-com.taobao.arthas.agent.ArthasClassloader@51a10fc8
  4. +-sun.misc.Launcher$AppClassLoader@18b4aac2
  1. 异步调用bind方法,该方法最终启动server监听线程,监听客户端的连接,包括telnet和websocket两种通信方式。源码如下:

 

  1. Thread bindingThread = new Thread() {
  2. @Override
  3. public void run() {
  4. try {
  5. bind(inst, agentLoader, agentArgs);
  6. } catch (Throwable throwable) {
  7. throwable.printStackTrace(ps);
  8. }
  9. }
  10. };
  11. private static void bind(Instrumentation inst, ClassLoader agentLoader, String args) throws Throwable {
  12. /**
  13. * <pre>
  14. * Configure configure = Configure.toConfigure(args);
  15. * int javaPid = configure.getJavaPid();
  16. * ArthasBootstrap bootstrap = ArthasBootstrap.getInstance(javaPid, inst);
  17. * </pre>
  18. */
  19. Class<?> classOfConfigure = agentLoader.loadClass(ARTHAS_CONFIGURE);
  20. Object configure = classOfConfigure.getMethod(TO_CONFIGURE, String.class).invoke(null, args);
  21. int javaPid = (Integer) classOfConfigure.getMethod(GET_JAVA_PID).invoke(configure);
  22. Class<?> bootstrapClass = agentLoader.loadClass(ARTHAS_BOOTSTRAP);
  23. Object bootstrap = bootstrapClass.getMethod(GET_INSTANCE, int.class, Instrumentation.class).invoke(null, javaPid, inst);
  24. boolean isBind = (Boolean) bootstrapClass.getMethod(IS_BIND).invoke(bootstrap);
  25. if (!isBind) {
  26. try {
  27. ps.println("Arthas start to bind...");
  28. bootstrapClass.getMethod(BIND, classOfConfigure).invoke(bootstrap, configure);
  29. ps.println("Arthas server bind success.");
  30. return;
  31. } catch (Exception e) {
  32. ps.println("Arthas server port binding failed! Please check $HOME/logs/arthas/arthas.log for more details.");
  33. throw e;
  34. }
  35. }
  36. ps.println("Arthas server already bind.");
  37. }

主要做两件事情:

  • 使用ArthasClassloader加载com.taobao.arthas.core.config.Configure类(位于arthas-core.jar),并将传递过来的序列化之后的config,反序列化成对应的Configure对象。
  • 使用ArthasClassloader加载com.taobao.arthas.core.server.ArthasBootstrap类(位于arthas-core.jar),并调用bind方法。

启动服务器,并监听客户端请求

下面重点看下com.taobao.arthas.core.server.ArthasBootstrap#bind方法

 

  1. /**
  2. * Bootstrap arthas server
  3. *
  4. * @param configure 配置信息
  5. * @throws IOException 服务器启动失败
  6. */
  7. public void bind(Configure configure) throws Throwable {
  8. long start = System.currentTimeMillis();
  9. if (!isBindRef.compareAndSet(false, true)) {
  10. throw new IllegalStateException("already bind");
  11. }
  12. try {
  13. ShellServerOptions options = new ShellServerOptions()
  14. .setInstrumentation(instrumentation)
  15. .setPid(pid)
  16. .setSessionTimeout(configure.getSessionTimeout() * 1000);
  17. shellServer = new ShellServerImpl(options, this);
  18. BuiltinCommandPack builtinCommands = new BuiltinCommandPack();
  19. List<CommandResolver> resolvers = new ArrayList<CommandResolver>();
  20. resolvers.add(builtinCommands);
  21. // TODO: discover user provided command resolver
  22. if (configure.getTelnetPort() > 0) {
  23. // telnet方式的server
  24. shellServer.registerTermServer(new TelnetTermServer(configure.getIp(), configure.getTelnetPort(),
  25. options.getConnectionTimeout()));
  26. } else {
  27. logger.info("telnet port is {}, skip bind telnet server.", configure.getTelnetPort());
  28. }
  29. if (configure.getHttpPort() > 0) {
  30. // websocket方式的server
  31. shellServer.registerTermServer(new HttpTermServer(configure.getIp(), configure.getHttpPort(),
  32. options.getConnectionTimeout()));
  33. } else {
  34. logger.info("http port is {}, skip bind http server.", configure.getHttpPort());
  35. }
  36. for (CommandResolver resolver : resolvers) {
  37. shellServer.registerCommandResolver(resolver);
  38. }
  39. shellServer.listen(new BindHandler(isBindRef));
  40. logger.info("as-server listening on network={};telnet={};http={};timeout={};", configure.getIp(),
  41. configure.getTelnetPort(), configure.getHttpPort(), options.getConnectionTimeout());
  42. // 异步回报启动次数
  43. UserStatUtil.arthasStart();
  44. logger.info("as-server started in {} ms", System.currentTimeMillis() - start );
  45. } catch (Throwable e) {
  46. logger.error(null, "Error during bind to port " + configure.getTelnetPort(), e);
  47. if (shellServer != null) {
  48. shellServer.close();
  49. }
  50. throw e;
  51. }
  52. }

可以看到有两种类型的server,TelnetTermServerHttpTermServer。同时会在BuiltinCommandPack里添加所有的命令Command,添加命令的源码如下:

 

  1. public class BuiltinCommandPack implements CommandResolver {
  2. private static List<Command> commands = new ArrayList<Command>();
  3. static {
  4. initCommands();
  5. }
  6. @Override
  7. public List<Command> commands() {
  8. return commands;
  9. }
  10. private static void initCommands() {
  11. commands.add(Command.create(HelpCommand.class));
  12. commands.add(Command.create(KeymapCommand.class));
  13. commands.add(Command.create(SearchClassCommand.class));
  14. commands.add(Command.create(SearchMethodCommand.class));
  15. commands.add(Command.create(ClassLoaderCommand.class));
  16. commands.add(Command.create(JadCommand.class));
  17. commands.add(Command.create(GetStaticCommand.class));
  18. commands.add(Command.create(MonitorCommand.class));
  19. commands.add(Command.create(StackCommand.class));
  20. commands.add(Command.create(ThreadCommand.class));
  21. commands.add(Command.create(TraceCommand.class));
  22. commands.add(Command.create(WatchCommand.class));
  23. commands.add(Command.create(TimeTunnelCommand.class));
  24. commands.add(Command.create(JvmCommand.class));
  25. // commands.add(Command.create(GroovyScriptCommand.class));
  26. commands.add(Command.create(OgnlCommand.class));
  27. commands.add(Command.create(DashboardCommand.class));
  28. commands.add(Command.create(DumpClassCommand.class));
  29. commands.add(Command.create(JulyCommand.class));
  30. commands.add(Command.create(ThanksCommand.class));
  31. commands.add(Command.create(OptionsCommand.class));
  32. commands.add(Command.create(ClsCommand.class));
  33. commands.add(Command.create(ResetCommand.class));
  34. commands.add(Command.create(VersionCommand.class));
  35. commands.add(Command.create(ShutdownCommand.class));
  36. commands.add(Command.create(SessionCommand.class));
  37. commands.add(Command.create(SystemPropertyCommand.class));
  38. commands.add(Command.create(SystemEnvCommand.class));
  39. commands.add(Command.create(RedefineCommand.class));
  40. commands.add(Command.create(HistoryCommand.class));
  41. }
  42. }

调用shellServer.registerTermServershellServer.registerTermServershellServer.registerCommandResolve 注册到ShellServer里,ShellServer是整个服务端的门面类,调用listen方法启动ShellServer
ShellServer会使用一系列的类,细节比较复杂,可以见下面的类图。

 

Arthas-服务端类图.png


ShellServer#listen会调用所有注册的TermServer的listen方法,比如TelnetTermServer。然后TelnetTermServerlisten方法会注册一个回调类,该回调类在有新的客户端连接时会调用TermServerTermHandlerhandle方法处理。

 

 

  1. bootstrap = new NettyTelnetTtyBootstrap().setHost(hostIp).setPort(port);
  2. try {
  3. bootstrap.start(new Consumer<TtyConnection>() {
  4. @Override
  5. public void accept(final TtyConnection conn) {
  6. termHandler.handle(new TermImpl(Helper.loadKeymap(), conn));
  7. }
  8. }).get(connectionTimeout, TimeUnit.MILLISECONDS);
  9. listenHandler.handle(Future.<TermServer>succeededFuture());

该方法会接着调用ShellServerImplhandleTerm方法进行处理,ShellServerImplhandleTerm方法会调用ShellImplreadline方法。该方法会注册ShellLineHandler作为回调类,服务端接收到客户端发送的请求行之后,会回调ShellLineHandlerhandle方法处理请求。readline方法源码如下:

 

  1. public void readline(String prompt, Handler<String> lineHandler, Handler<Completion> completionHandler) {
  2. if (conn.getStdinHandler() != echoHandler) {
  3. throw new IllegalStateException();
  4. }
  5. if (inReadline) {
  6. throw new IllegalStateException();
  7. }
  8. inReadline = true;
  9. // 注册回调类RequestHandler,该类包装了ShellLineHandler,处理逻辑还是在ShellLineHandler类里面
  10. readline.readline(conn, prompt, new RequestHandler(this, lineHandler), new CompletionHandler(completionHandler, session));
  11. }

处理客户端请求

ShellLineHandlerhandle方法会根据不同的请求命令执行不同的逻辑:

  1. 如果是exit,logout,quit, jobs,fg,bg,kill等直接执行。
  2. 如果是其他的命令,则创建Job,并运行。创建Job的类图如下:

     

    服务端-创建job类图.png

     

    步骤比较多,就不一一细讲,总之:

  3. 创建Job时,会根据具体客户端传递的命令,找到对应的Command,并包装成Process, Process再被包装成Job。
  4. 运行Job时,反向先调用Process,再找到对应的Command,最终调用Commandprocess处理请求。

Command处理流程

Command主要分为两类:

  1. 不需要使用字节码增强的命令
    其中JVM相关的使用 java.lang.management 提供的管理接口,来查看具体的运行时数据。比较简单,就不介绍了。
  2. 需要使用字节码增强的命令
    字节码增强的命令,可以参考下图:

     

    arthas-command相关类图.png

字节码增加的命令统一继承EnhancerCommand类,process方法里面调用enhance方法进行增强。调用Enhancerenhance方法,该方法内部调用inst.addTransformer方法添加自定义的ClassFileTransformer,这边是Enhancer类。

Enhancer类使用AdviceWeaver(继承ClassVisitor),用来修改类的字节码。重写了visitMethod方法,在该方法里面修改类指定的方法。visitMethod方法里面使用了AdviceAdapter(继承了MethodVisitor类),在onMethodEnter方法, onMethodExit方法中,把Spy类对应的方法(ON_BEFORE_METHODON_RETURN_METHODON_THROWS_METHOD等)编织到目标类的方法对应的位置。

在前面Spy初始化的时候可以看到,这几个方法其实指向的是AdviceWeaver类的methodOnBeginmethodOnReturnEnd等。在这些方法里面都会根据adviceId查找对应的AdviceListener,并调用AdviceListener的对应的方法,比如before,afterReturning, afterThrowing

通过这种方式,可以实现不同的Command使用不同的AdviceListener,从而实现不同的处理逻辑。下面找几个常用的AdviceListener介绍下:

  1. StackAdviceListener
    在方法执行前,记录堆栈和方法的耗时。
  2. WatchAdviceListener
    满足条件时打印打印参数或者结果,条件表达式使用Ognl语法。
  3. TraceAdviceListener
    在每个方法前后都记录,并维护一个调用树结构。

arthas客户端代码分析

客户端代码在arthas-client模块里面,入口类是com.taobao.arthas.client.TelnetConsole。主要使用apache commons-net jar进行telnet连接,关键的代码有下面几步:

  1. 构造TelnetClient对象,并初始化
  2. 构造ConsoleReader对象,并初始化
  3. 调用IOUtil.readWrite(telnet.getInputStream(), telnet.getOutputStream(), System.in, consoleReader.getOutput())处理各个流,一共有四个流:
  • telnet.getInputStream()
  • telnet.getOutputStream()
  • System.in
  • consoleReader.getOutput()

请求时:从本地System.in读取,发送到 telnet.getOutputStream(),即发送给远程服务端。
响应时:从telnet.getInputStream()读取远程服务端发送过来的响应,并传递给 consoleReader.getOutput(),即在本地控制台输出。

 

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/IT小白/article/detail/450367
推荐阅读
相关标签
  

闽ICP备14008679号