赞
踩
arthas 是Alibaba开源的Java诊断工具,基于jvm Agent
方式,使用Instrumentation
方式修改字节码方式以及使用java.lang.management
包提供的管理接口的方式进行java应用诊断。详细的介绍可以参考官方文档。
官方文档地址:https://alibaba.github.io/arthas/
GitHub地址:https://github.com/alibaba/arthas/
本文主要分析arthas源码,主要分成下面几个部分:
arthas有多个模块组成,如下图所示:
arthas模块图.png
- ${JAVA_HOME}"/bin/java \
- ${opts} \
- -jar "${arthas_lib_dir}/arthas-core.jar" \
- -pid ${TARGET_PID} \ 要注入的进程id
- -target-ip ${TARGET_IP} \ 服务器ip地址
- -telnet-port ${TELNET_PORT} \ 服务器telnet服务端口号
- -http-port ${HTTP_PORT} \ websocket服务端口号
- -core "${arthas_lib_dir}/arthas-core.jar" \ arthas-core目录
- -agent "${arthas_lib_dir}/arthas-agent.jar" arthas-agent目录
virtualMachine#attach
到目标进程,并加载arthas-agent.jar作为agent jar包。ArthasClassLoader
)加载arthas-core.jar里面的com.taobao.arthas.core.config.Configure
类以及com.taobao.arthas.core.server.ArthasBootstrap
。 同时程序运行的时候会使用arthas-spy.jar。BootstrapClassLoader
来加载,从而使目标进程的java应用可以访问Spy类。通过ASM修改字节码,可以将Spy类的方法ON_BEFORE_METHOD
, ON_RETURN_METHOD
等编织到目标类里面。Spy类你可以简单理解为类似spring aop的Advice,有前置方法,后置方法等。看服务端启动命令可以知道 从 arthas-core.jar开始启动,arthas-core的pom.xml文件里面指定了mainClass为com.taobao.arthas.core.Arthas
,使得程序启动的时候从该类的main方法开始运行。Arthas源码如下:
- public class Arthas {
-
- private Arthas(String[] args) throws Exception {
- attachAgent(parse(args));
- }
-
- private Configure parse(String[] args) {
- // 省略非关键代码,解析启动参数作为配置,并填充到configure对象里面
- return configure;
- }
-
- private void attachAgent(Configure configure) throws Exception {
- // 省略非关键代码,attach到目标进程
- virtualMachine = VirtualMachine.attach("" + configure.getJavaPid());
- virtualMachine.loadAgent(configure.getArthasAgent(),
- configure.getArthasCore() + ";" + configure.toString());
- }
-
-
- public static void main(String[] args) {
- new Arthas(args);
- }
- }
com.taobao.arthas.core.config.Configure
类,包含了相关配置信息VirtualMachine.loadAgent
,其中第一个参数为agent路径, 第二个参数向jar包中的agentmain()方法传递参数(此处为agent-core.jar包路径和config序列化之后的字符串),加载arthas-agent.jar包,并运行com.taobao.arthas.agent.AgentBootstrap
,同时可以使用Premain的方式和目标进程同时启动
- <manifestEntries>
- <Premain-Class>com.taobao.arthas.agent.AgentBootstrap</Premain-Class>
- <Agent-Class>com.taobao.arthas.agent.AgentBootstrap</Agent-Class>
- </manifestEntries>
其中Premain-Class
的premain
和Agent-Class
的agentmain
都调用main方法。
main方法主要做4件事情:
Instrumentation#appendToBootstrapClassLoaderSearch
方法,使用bootstrapClassLoader
来加载arthas-spy.jar里的Spy类。ArthasClassloader
),用来隔离arthas本身的类和目标进程的类。ArthasClassloader#loadClass
方法,加载com.taobao.arthas.core.advisor.AdviceWeaver
类,并将里面的methodOnBegin
、methodOnReturnEnd
、methodOnThrowingEnd
等方法取出赋值给Spy类对应的方法。同时Spy类里面的方法又会通过ASM字节码增强的方式,编织到目标代码的方法里面。使得Spy 间谍类可以关联由AppClassLoader
加载的目标进程的业务类和ArthasClassloader
加载的arthas类,因此Spy类可以看做两者之间的桥梁。根据classloader双亲委派特性,子classloader可以访问父classloader加载的类。源码如下:
- private static ClassLoader getClassLoader(Instrumentation inst, File spyJarFile, File agentJarFile) throws Throwable {
- // 将Spy添加到BootstrapClassLoader
- inst.appendToBootstrapClassLoaderSearch(new JarFile(spyJarFile));
-
- // 构造自定义的类加载器ArthasClassloader,尽量减少Arthas对现有工程的侵蚀
- return loadOrDefineClassLoader(agentJarFile);
- }
-
- private static void initSpy(ClassLoader classLoader) throws ClassNotFoundException, NoSuchMethodException {
- // 该classLoader为ArthasClassloader
- Class<?> adviceWeaverClass = classLoader.loadClass(ADVICEWEAVER);
- Method onBefore = adviceWeaverClass.getMethod(ON_BEFORE, int.class, ClassLoader.class, String.class,
- String.class, String.class, Object.class, Object[].class);
- Method onReturn = adviceWeaverClass.getMethod(ON_RETURN, Object.class);
- Method onThrows = adviceWeaverClass.getMethod(ON_THROWS, Throwable.class);
- Method beforeInvoke = adviceWeaverClass.getMethod(BEFORE_INVOKE, int.class, String.class, String.class, String.class);
- Method afterInvoke = adviceWeaverClass.getMethod(AFTER_INVOKE, int.class, String.class, String.class, String.class);
- Method throwInvoke = adviceWeaverClass.getMethod(THROW_INVOKE, int.class, String.class, String.class, String.class);
- Method reset = AgentBootstrap.class.getMethod(RESET);
- Spy.initForAgentLauncher(classLoader, onBefore, onReturn, onThrows, beforeInvoke, afterInvoke, throwInvoke, reset);
- }
-
-
classloader关系如下:
- +-BootstrapClassLoader
- +-sun.misc.Launcher$ExtClassLoader@7bf2dede
- +-com.taobao.arthas.agent.ArthasClassloader@51a10fc8
- +-sun.misc.Launcher$AppClassLoader@18b4aac2
- Thread bindingThread = new Thread() {
- @Override
- public void run() {
- try {
- bind(inst, agentLoader, agentArgs);
- } catch (Throwable throwable) {
- throwable.printStackTrace(ps);
- }
- }
- };
-
- private static void bind(Instrumentation inst, ClassLoader agentLoader, String args) throws Throwable {
- /**
- * <pre>
- * Configure configure = Configure.toConfigure(args);
- * int javaPid = configure.getJavaPid();
- * ArthasBootstrap bootstrap = ArthasBootstrap.getInstance(javaPid, inst);
- * </pre>
- */
- Class<?> classOfConfigure = agentLoader.loadClass(ARTHAS_CONFIGURE);
- Object configure = classOfConfigure.getMethod(TO_CONFIGURE, String.class).invoke(null, args);
- int javaPid = (Integer) classOfConfigure.getMethod(GET_JAVA_PID).invoke(configure);
- Class<?> bootstrapClass = agentLoader.loadClass(ARTHAS_BOOTSTRAP);
- Object bootstrap = bootstrapClass.getMethod(GET_INSTANCE, int.class, Instrumentation.class).invoke(null, javaPid, inst);
- boolean isBind = (Boolean) bootstrapClass.getMethod(IS_BIND).invoke(bootstrap);
- if (!isBind) {
- try {
- ps.println("Arthas start to bind...");
- bootstrapClass.getMethod(BIND, classOfConfigure).invoke(bootstrap, configure);
- ps.println("Arthas server bind success.");
- return;
- } catch (Exception e) {
- ps.println("Arthas server port binding failed! Please check $HOME/logs/arthas/arthas.log for more details.");
- throw e;
- }
- }
- ps.println("Arthas server already bind.");
- }
主要做两件事情:
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
方法
- /**
- * Bootstrap arthas server
- *
- * @param configure 配置信息
- * @throws IOException 服务器启动失败
- */
- public void bind(Configure configure) throws Throwable {
-
- long start = System.currentTimeMillis();
-
- if (!isBindRef.compareAndSet(false, true)) {
- throw new IllegalStateException("already bind");
- }
-
- try {
- ShellServerOptions options = new ShellServerOptions()
- .setInstrumentation(instrumentation)
- .setPid(pid)
- .setSessionTimeout(configure.getSessionTimeout() * 1000);
- shellServer = new ShellServerImpl(options, this);
- BuiltinCommandPack builtinCommands = new BuiltinCommandPack();
- List<CommandResolver> resolvers = new ArrayList<CommandResolver>();
- resolvers.add(builtinCommands);
- // TODO: discover user provided command resolver
- if (configure.getTelnetPort() > 0) {
- // telnet方式的server
- shellServer.registerTermServer(new TelnetTermServer(configure.getIp(), configure.getTelnetPort(),
- options.getConnectionTimeout()));
- } else {
- logger.info("telnet port is {}, skip bind telnet server.", configure.getTelnetPort());
- }
- if (configure.getHttpPort() > 0) {
- // websocket方式的server
- shellServer.registerTermServer(new HttpTermServer(configure.getIp(), configure.getHttpPort(),
- options.getConnectionTimeout()));
- } else {
- logger.info("http port is {}, skip bind http server.", configure.getHttpPort());
- }
-
- for (CommandResolver resolver : resolvers) {
- shellServer.registerCommandResolver(resolver);
- }
-
- shellServer.listen(new BindHandler(isBindRef));
-
- logger.info("as-server listening on network={};telnet={};http={};timeout={};", configure.getIp(),
- configure.getTelnetPort(), configure.getHttpPort(), options.getConnectionTimeout());
- // 异步回报启动次数
- UserStatUtil.arthasStart();
-
- logger.info("as-server started in {} ms", System.currentTimeMillis() - start );
- } catch (Throwable e) {
- logger.error(null, "Error during bind to port " + configure.getTelnetPort(), e);
- if (shellServer != null) {
- shellServer.close();
- }
- throw e;
- }
- }
可以看到有两种类型的server,TelnetTermServer
和HttpTermServer
。同时会在BuiltinCommandPack里添加所有的命令Command,添加命令的源码如下:
- public class BuiltinCommandPack implements CommandResolver {
-
- private static List<Command> commands = new ArrayList<Command>();
-
- static {
- initCommands();
- }
-
- @Override
- public List<Command> commands() {
- return commands;
- }
-
- private static void initCommands() {
- commands.add(Command.create(HelpCommand.class));
- commands.add(Command.create(KeymapCommand.class));
- commands.add(Command.create(SearchClassCommand.class));
- commands.add(Command.create(SearchMethodCommand.class));
- commands.add(Command.create(ClassLoaderCommand.class));
- commands.add(Command.create(JadCommand.class));
- commands.add(Command.create(GetStaticCommand.class));
- commands.add(Command.create(MonitorCommand.class));
- commands.add(Command.create(StackCommand.class));
- commands.add(Command.create(ThreadCommand.class));
- commands.add(Command.create(TraceCommand.class));
- commands.add(Command.create(WatchCommand.class));
- commands.add(Command.create(TimeTunnelCommand.class));
- commands.add(Command.create(JvmCommand.class));
- // commands.add(Command.create(GroovyScriptCommand.class));
- commands.add(Command.create(OgnlCommand.class));
- commands.add(Command.create(DashboardCommand.class));
- commands.add(Command.create(DumpClassCommand.class));
- commands.add(Command.create(JulyCommand.class));
- commands.add(Command.create(ThanksCommand.class));
- commands.add(Command.create(OptionsCommand.class));
- commands.add(Command.create(ClsCommand.class));
- commands.add(Command.create(ResetCommand.class));
- commands.add(Command.create(VersionCommand.class));
- commands.add(Command.create(ShutdownCommand.class));
- commands.add(Command.create(SessionCommand.class));
- commands.add(Command.create(SystemPropertyCommand.class));
- commands.add(Command.create(SystemEnvCommand.class));
- commands.add(Command.create(RedefineCommand.class));
- commands.add(Command.create(HistoryCommand.class));
- }
- }
调用shellServer.registerTermServer
,shellServer.registerTermServer
,shellServer.registerCommandResolve
注册到ShellServer
里,ShellServer
是整个服务端的门面类,调用listen
方法启动ShellServer
。ShellServer
会使用一系列的类,细节比较复杂,可以见下面的类图。
Arthas-服务端类图.png
ShellServer#listen
会调用所有注册的TermServer的listen
方法,比如TelnetTermServer
。然后TelnetTermServer
的listen
方法会注册一个回调类,该回调类在有新的客户端连接时会调用TermServerTermHandler
的handle
方法处理。
- bootstrap = new NettyTelnetTtyBootstrap().setHost(hostIp).setPort(port);
- try {
- bootstrap.start(new Consumer<TtyConnection>() {
- @Override
- public void accept(final TtyConnection conn) {
- termHandler.handle(new TermImpl(Helper.loadKeymap(), conn));
- }
- }).get(connectionTimeout, TimeUnit.MILLISECONDS);
- listenHandler.handle(Future.<TermServer>succeededFuture());
该方法会接着调用ShellServerImpl
的handleTerm
方法进行处理,ShellServerImpl
的handleTerm
方法会调用ShellImpl
的readline
方法。该方法会注册ShellLineHandler
作为回调类,服务端接收到客户端发送的请求行之后,会回调ShellLineHandler
的handle
方法处理请求。readline
方法源码如下:
- public void readline(String prompt, Handler<String> lineHandler, Handler<Completion> completionHandler) {
- if (conn.getStdinHandler() != echoHandler) {
- throw new IllegalStateException();
- }
- if (inReadline) {
- throw new IllegalStateException();
- }
- inReadline = true;
- // 注册回调类RequestHandler,该类包装了ShellLineHandler,处理逻辑还是在ShellLineHandler类里面
- readline.readline(conn, prompt, new RequestHandler(this, lineHandler), new CompletionHandler(completionHandler, session));
- }
ShellLineHandler
的handle
方法会根据不同的请求命令执行不同的逻辑:
如果是其他的命令,则创建Job,并运行。创建Job的类图如下:
服务端-创建job类图.png
步骤比较多,就不一一细讲,总之:
Job
时,会根据具体客户端传递的命令,找到对应的Command
,并包装成Process
, Process
再被包装成Job。Job
时,反向先调用Process
,再找到对应的Command
,最终调用Command
的process
处理请求。Command
主要分为两类:
java.lang.management
提供的管理接口,来查看具体的运行时数据。比较简单,就不介绍了。需要使用字节码增强的命令
字节码增强的命令,可以参考下图:
arthas-command相关类图.png
字节码增加的命令统一继承EnhancerCommand
类,process
方法里面调用enhance
方法进行增强。调用Enhancer
类enhance
方法,该方法内部调用inst.addTransformer
方法添加自定义的ClassFileTransformer
,这边是Enhancer
类。
Enhancer
类使用AdviceWeaver
(继承ClassVisitor
),用来修改类的字节码。重写了visitMethod
方法,在该方法里面修改类指定的方法。visitMethod
方法里面使用了AdviceAdapter
(继承了MethodVisitor
类),在onMethodEnter
方法, onMethodExit
方法中,把Spy
类对应的方法(ON_BEFORE_METHOD
, ON_RETURN_METHOD
, ON_THROWS_METHOD
等)编织到目标类的方法对应的位置。
在前面Spy
初始化的时候可以看到,这几个方法其实指向的是AdviceWeaver
类的methodOnBegin
, methodOnReturnEnd
等。在这些方法里面都会根据adviceId
查找对应的AdviceListener
,并调用AdviceListener
的对应的方法,比如before
,afterReturning
, afterThrowing
。
通过这种方式,可以实现不同的Command
使用不同的AdviceListener
,从而实现不同的处理逻辑。下面找几个常用的AdviceListener
介绍下:
StackAdviceListener
WatchAdviceListener
TraceAdviceListener
客户端代码在arthas-client模块里面,入口类是com.taobao.arthas.client.TelnetConsole
。主要使用apache commons-net jar进行telnet连接,关键的代码有下面几步:
TelnetClient
对象,并初始化ConsoleReader
对象,并初始化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()
,即在本地控制台输出。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。