阿里监控诊断工具Arthas源码原理分析

编程

此前,阿里开源了 监控与诊断 工具 「 Arthas 」,一款可用于线上问题分析的利器,短期之内收获了大量关注,在 Twitter 上连 Java 官方的 Twitter 也转发了,真的很赞。

GitHub 上是这样自述的:

Arthas 是一款线上监控诊断产品,通过全局视角实时查看应用 load、内存、gc、线程的状态信息,并能在不修改应用代码的情况下,对业务问题进行诊断,包括查看方法调用的出入参、异常,监测方法执行耗时,类加载信息等,大大提升线上问题排查效率。

我一般看到感兴趣的开源工具,会找几个最感兴趣的功能点切入,从源码了解设计与实现原理。对于一些自己了解的实现思路,再从源码中验证一下是否是采用相同的实现思路。如果实现和自己想的一样,可能你会想,啊哈,想到一块了。如果源码中是另一种实现,你就会想 Cool, 还可以这样玩。 仿佛如同在和源码的作者对话一样 。

这次趁着国庆假期看了一些「 Arthas 」的源码,大致总结下。

从源码的包结构上,可以看到分为几个大的 模块:

  • Agent – VM 加载的自定义 Agent
  • Client – Telnet 客户端实现
  • Core – Arthas 核心实现,包含连接 VM, 解析各类命令等
  • Site – Arthas 的帮助手册站点内容

我主要看了以下几个功能:

  • 连接进程
  • 反编译class,获取源码
  • 查询指定加载的 class

连接进程

连接到指定的进程,是后续监控与诊断的 基础 。只有先 attach 到进程之上,才能获取 VM 对应的信息,查询 ClassLoader 加载的类等等。

怎样连接到进程呢?

用于类似诊断工具的读者可能都有印象,像 JProfile、 VisualVM 等工具,都会让你选择一个要连接到的进程。然后再在指定的 VM 上进行操作。比如查看对应的内存分区信息,内存垃圾收集信息,执行 BTrace脚本等等。

咱们先来想想,这些可供连接的进程列表,是怎么列出来的呢?

一般可能会是类似 ps aux | grep java 这种,或者是使用 Java 提供的工具 jps -lv 都可以列出包含进程id的内容。我在很早之前的文章里写过一点 jps 的内容( 你可能不知道的几个java小工具 ),其背后实现,是会将本地启动的所有 Java 进程,以 pid 做为文件名存放在Java 的临时目录中。这个列表,遍历这些文件即可得出来。

Arthas 是怎么做的呢?

在启动脚本 as.sh 中,有关于进程列表的代码如下,实现也是通过 jps 然后把Jps自己排除掉:

# check pid

if [ -z ${TARGET_PID} ] && [ ${BATCH_MODE} = false ]; then

local IFS_backup=$IFS

IFS=$"

"

CANDIDATES=($(${JAVA_HOME}/bin/jps -l | grep -v sun.tools.jps.Jps | awk "{print $0}"))

if [ ${#CANDIDATES[@]} -eq 0 ]; then

echo "Error: no available java process to attach."

# recover IFS

IFS=$IFS_backup

return 1

fi

echo "Found existing java process, please choose one and hit RETURN."

index=0

suggest=1

# auto select tomcat/pandora-boot process

for process in "${CANDIDATES[@]}"; do

index=$(($index+1))

if [ $(echo ${process} | grep -c org.apache.catalina.startup.Bootstrap) -eq 1 ]

|| [ $(echo ${process} | grep -c com.taobao.pandora.boot.loader.SarLauncher) -eq 1 ]

then

suggest=${index}

break

fi

done

  •  

选择好进程之后,就是连接到指定进程了。连接部分在 attach 这里

# attach arthas to target jvm

# $1 : arthas_local_version

attach_jvm()

{

local arthas_version=$1

local arthas_lib_dir=${ARTHAS_LIB_DIR}/${arthas_version}/arthas

echo "Attaching to ${TARGET_PID} using version ${1}..."

if [ ${TARGET_IP} = ${DEFAULT_TARGET_IP} ]; then

${JAVA_HOME}/bin/java

${ARTHAS_OPTS} ${BOOT_CLASSPATH} ${JVM_OPTS}

-jar ${arthas_lib_dir}/arthas-core.jar

-pid ${TARGET_PID}

-target-ip ${TARGET_IP}

-telnet-port ${TELNET_PORT}

-http-port ${HTTP_PORT}

-core "${arthas_lib_dir}/arthas-core.jar"

-agent "${arthas_lib_dir}/arthas-agent.jar"

fi

}

  •  

对于 JVM 内部的 attach 实现,是通过 tools.jar 这个包中的 com.sun.tools.attach.VirtualMachine 以及 VirtualMachine.attach(pid) 这种方式来实现的。

底层则是通过 JVMTI 。之前的文章简单分析过 JVMTI 这种技术( 当我们谈Debug时,我们在谈什么(Debug实现原理) ),在运行前或者运行时,将自定义的 Agent加载并和 VM 进行 通信 。

上面具体执行的内容在 arthas-core.jar 的主类中,我们来看具体的内容:

private void attachAgent(Configure configure) throws Exception {

VirtualMachineDescriptor virtualMachineDescriptor = null;

for (VirtualMachineDescriptor descriptor : VirtualMachine.list()) {

String pid = descriptor.id();

if (pid.equals(Integer.toString(configure.getJavaPid()))) {

virtualMachineDescriptor = descriptor;

}

}

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 = targetSystemProperties.getProperty("java.specification.version");

String currentJavaVersion = System.getProperty("java.specification.version");

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 {}, try to set the same JAVA_HOME.",

targetSystemProperties.getProperty("java.home"));

}

}

virtualMachine.loadAgent(configure.getArthasAgent(),

configure.getArthasCore() + ";" + configure.toString());

} finally {

if (null != virtualMachine) {

virtualMachine.detach();

}

}

}

  •  

通过 VirtualMachine , 可以attach到当前指定的pid上,或者是通过 VirtualMachineDescriptor 实现指定进程的attach,最核心的就是这一句:

virtualMachine.loadAgent(configure.getArthasAgent(),configure.getArthasCore() + ";" + configure.toString());

  • 1

这样,就和指定进程的 VM建立了连接,此时就可以进行通信啦。

类的反编译实现

我们在问题诊断中,有些时候需要了解当前加载的 class 对应的内容,方便确认加载的类是否正确等,一般通过 javap 只能显示类似摘要的内容,并不直观。 在桌面端我们可以通过 jd-gui 之类的工具,在命令行里一般可选的不多。Arthas 则集成了这一功能。

大致的步骤如下:

  • 通过指定class名称的内容,先进行类的查找
  • 根据选项,判断是否进行Inner Class之类的查找
  • 进行反编译

我们来看 Arthas 的实现。
对于 VM 中指定名称的 class 的查找,我们看下面这几行代码:

    public void process(CommandProcess process) {

RowAffect affect = new RowAffect();

Instrumentation inst = process.session().getInstrumentation();

Set<Class> matchedClasses = SearchUtils.searchClassOnly(inst, classPattern, isRegEx, code);

try {

if (matchedClasses == null || matchedClasses.isEmpty()) {

processNoMatch(process);

} else if (matchedClasses.size() > 1) {

processMatches(process, matchedClasses);

} else {

Set<Class> withInnerClasses = SearchUtils.searchClassOnly(inst, classPattern + "(?!.*\$\$Lambda\$).*", true, code);

processExactMatch(process, affect, inst, matchedClasses, withInnerClasses);

}

  •  

关键的查找内容,做了封装,在 SearchUtils 里,这里有一个核心的参数: Instrumentation ,都是这个哥们给实现的。

    /**

* 根据类名匹配,搜已经被JVM加载的类

*

* @param inst inst

* @param classNameMatcher 类名匹配

* @return 匹配的类集合

*/

public static Set> searchClass(Instrumentation inst, Matcher classNameMatcher, int limit) {

for (Class clazz : inst.getAllLoadedClasses()) {

if (classNameMatcher.matching(clazz.getName())) {

matches.add(clazz);

}

}

return matches;

}

  •  

inst.getAllLoadedClasses() ,它才是背后的大玩家。

查找到了 Class 之后,怎么反编译的呢?

 private String decompileWithCFR(String classPath, Class clazz, String methodName) {

List<String> options = new ArrayList<String>();

options.add(classPath);

// options.add(clazz.getName());

if (methodName != null) {

options.add(methodName);

}

options.add(OUTPUTOPTION);

options.add(DecompilePath);

options.add(COMMENTS);

options.add("false");

String args[] = new String[options.size()];

options.toArray(args);

Main.main(args);

String outputFilePath = DecompilePath + File.separator + Type.getInternalName(clazz) + ".java";

File outputFile = new File(outputFilePath);

if (outputFile.exists()) {

try {

return FileUtils.readFileToString(outputFile, Charset.defaultCharset());

} catch (IOException e) {

logger.error(null, "error read decompile result in: " + outputFilePath, e);

}

}

return null;

}

  • 通过这样一个方法: decompileWithCFR ,所以我们大概了解到反编译是通过第三方工具「 CFR 」来实现的。上面的代码也是拼 Option然后传给 CFR的 Main方法实现,再保存下来。感兴趣的朋友可以查询 benf cfr 了解具体用法。

查询加载类的实现

看过上面反编译 class 的内容之后,我们知道封装了一个 SearchUtil 的类,后面许多地方都会用到,而且上面反编译也是在查询到类的之后再进行的。查询的过程,也是在Instrument的基础之上,再加上各种匹配规则过滤,所以更多的具体内容不再赘述。

我们发现上面几个功能的实现中,有两个关键的东西:

  • VirtualMachine
  • Instrumentation

Arthas 的整体逻辑也是在 Java 的 Instrumentation基础上来实现,所有在加载的类会通过Agent的加载, 通过addTransformer之后,进行增强,然后将对应的Advice织入进去,对于类的查找,方法的查找,都是通过SearchUtil来进行的,通过InstrumentloadAllClass方法将所有的JVM加载的class按名字进行匹配,一致的会进行返回。

Instrumentation 是个好同志! ?

以上是 阿里监控诊断工具Arthas源码原理分析 的全部内容, 来源链接: utcz.com/z/510578.html

回到顶部