阿里監控診斷工具 Arthas 源碼原理分析

此前,阿里開源了 監控與診斷 工具 「 Arthas 」,一款可用於線上問題分析的利器,短時間以內收穫了大量關注,在 Twitter 上連 Java 官方的 Twitter 也轉發了,真的很贊。java

GitHub 上是這樣自述的:apache

Arthas 是一款線上監控診斷產品,經過全局視角實時查看應用 load、內存、gc、線程的狀態信息,並能在不修改應用代碼的狀況下,對業務問題進行診斷,包括查看方法調用的出入參、異常,監測方法執行耗時,類加載信息等,大大提高線上問題排查效率。tomcat

我通常看到感興趣的開源工具,會找幾個最感興趣的功能點切入,從源碼瞭解設計與實現原理。對於一些本身瞭解的實現思路,再從源碼中驗證一下是不是採用相同的實現思路。若是實現和本身想的同樣,可能你會想,啊哈,想到一塊了。若是源碼中是另外一種實現,你就會想 Cool, 還能夠這樣玩。 彷彿如同在和源碼的做者對話同樣 。session

此次趁着國慶假期看了一些「 Arthas 」的源碼,大體總結下。jvm

從源碼的包結構上,能夠看到分爲幾個大的 模塊:工具

  • Agent – VM 加載的自定義 Agent
  • Client – Telnet 客戶端實現
  • Core – Arthas 核心實現,包含鏈接 VM, 解析各種命令等
  • Site – Arthas 的幫助手冊站點內容

我主要看了如下幾個功能:ui

  • 鏈接進程
  • 反編譯class,獲取源碼
  • 查詢指定加載的 class

鏈接進程

鏈接到指定的進程,是後續監控與診斷的 基礎 。只有先 attach 到進程之上,才能獲取 VM 對應的信息,查詢 ClassLoader 加載的類等等。spa

怎樣鏈接到進程呢?

用於相似診斷工具的讀者可能都有印象,像 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());
  • 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 是個好同志! ?

相關文章
相關標籤/搜索