上个月,阿里开源了 监控与诊断 工具 「 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=$'/n'
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());
这样,就和指定进程的 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<Class<?>> searchClass(Instrumentation inst, Matcher<String> 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来进行的,通过Instrument的loadAllClass方法将所有的JVM加载的class按名字进行匹配,一致的会进行返回。
Instrumentation 是个好同志! :)
相关阅读
读源码时,我们到底在读什么?
怎样阅读源代码?
一款功能强大的Tomcat 管理监控工具
Java七武器系列多情环 --多功能Profiling工具 JVisual VM
Java七武器系列长生剑 -- Java虚拟机的显微镜 Serviceability Agent
关注『 Tomcat那些事儿 』 ,发现更多精彩文章!了解各种常见问题背后的原理与答案。深入源码,分析细节,内容原创,欢迎关注。
转发是最大的支持 ,谢谢
更多精彩内容:
一台机器上安装多个Tomcat 的原理(回复001)
监控Tomcat中的各种数据 (回复002)
启动Tomcat的安全机制(回复003)
乱码问题的原理及解决方式(回复007)
Tomcat 日志工作原理及配置(回复011)
web.xml 解析实现(回复 012)
线程池的原理( 回复 014)
Tomcat 的集群搭建原理与实现 (回复 015)
类加载器的原理 (回复 016)
类找不到等问题 (回复 017)
代码的热替换实现(回复 018)
Tomcat 进程自动退出问题 (回复 019)
为什么总是返回404? (回复 020)
...
PS: 对于一些 Tomcat常见问 题 ,在公众号的【 常见问题 】菜单中,有需要的朋友欢迎关注查看。