0%

Arthas 启动流程分析

Arthas 是 Alibaba 开源的 Java 诊断工具,开发者无需修改代码或重启 JVM 就可以排查生产环境问题,本质上使用 agent 机制,动态的修改字节码,从而实现插桩。

本文会对 Arthas 启动流程分析,从启动流程来理解 Arthas 工作原理。

Arthas 启动方式

根据 Arthas github README 所示,Arthas 提供了两种启动方式:

  1. Use arthas-boot(Recommended)

  2. 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) { // 使用 attach(String pid) 这种方式
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();
//convert jar path to unicode string
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
// com.taobao.arthas.agent334.AgentBootstrap#bind
private static void bind(Instrumentation inst, ClassLoader agentLoader, String args) throws Throwable {
/**
* <pre>
* ArthasBootstrap bootstrap = ArthasBootstrap.getInstance(inst);
* </pre>
*/
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 的构造方法,该方法主要做了如下的操作:

  1. 初始化 spy
  2. 初始化 arthas 环境变量
  3. 初始化 Logger
  4. 增强 ClassLoader
  5. 初始化 bean
  6. 启动 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
// com.taobao.arthas.core.server.ArthasBootstrap#ArthasBootstrap
private ArthasBootstrap(Instrumentation instrumentation, Map<String, String> args) throws Throwable {
this.instrumentation = instrumentation;

initFastjson();

// 1. initSpy()
initSpy();
// 2. ArthasEnvironment
initArthasEnvironment(args);

String outputPathStr = configure.getOutputPath();
if (outputPathStr == null) {
outputPathStr = ArthasConstants.ARTHAS_OUTPUT;
}
outputPath = new File(outputPathStr);
outputPath.mkdirs();

// 3. init logger
loggerContext = LogUtil.initLogger(arthasEnvironment);

// 4. 增强ClassLoader
enhanceClassLoader();
// 5. init beans
initBeans();

// 6. start agent server
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 实现原理。

参考文档

  1. Arthas Github