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

2018-10-10 12:55 阅读:1282次 作者: 来源: 公众账号

image

上个月,阿里开源了 **监控与诊断 **工具 「 **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 则集成了这一功能。

大致的步骤如下:

  1. 通过指定class名称的内容,先进行类的查找

  2. 根据选项,判断是否进行Inner Class之类的查找

  3. 进行反编译

我们来看 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 是个好同志! :)

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容