2018-10-10 12:55 阅读:1282次 作者: 来源: 公众账号
上个月,阿里开源了 **监控与诊断 **工具 「 **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=/pre>\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<> 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来进行的,通过Instrument的loadAllClass方法将所有的JVM加载的class按名字进行匹配,一致的会进行返回。
Instrumentation 是个好同志! :)