轉(zhuǎn):阿里監(jiān)控診斷工具 Arthas 源碼原理分析

2018-10-10 12:55 閱讀:1282次 作者: 來源: 公眾賬號(hào)

image

上個(gè)月屁置,阿里開源了 **監(jiān)控與診斷 **工具 「 **Arthas **」做裙,一款可用于線上問題分析的利器,短期之內(nèi)收獲了大量關(guān)注棍苹,在 Twitter 上連 Java 官方的 Twitter 也轉(zhuǎn)發(fā)了涧衙,真的很贊哺窄。

GitHub 上是這樣自述的:

Arthas 是一款線上監(jiān)控診斷產(chǎn)品,通過全局視角實(shí)時(shí)查看應(yīng)用 load到腥、內(nèi)存朵逝、gc、線程的狀態(tài)信息乡范,并能在不修改應(yīng)用代碼的情況下配名,對(duì)業(yè)務(wù)問題進(jìn)行診斷,包括查看方法調(diào)用的出入?yún)⒔尽惓G觯O(jiān)測(cè)方法執(zhí)行耗時(shí),類加載信息等栈拖,大大提升線上問題排查效率连舍。

我一般看到感興趣的開源工具,會(huì)找?guī)讉€(gè)最感興趣的功能點(diǎn)切入涩哟,從源碼了解設(shè)計(jì)與實(shí)現(xiàn)原理索赏。對(duì)于一些自己了解的實(shí)現(xiàn)思路,再?gòu)脑创a中驗(yàn)證一下是否是采用相同的實(shí)現(xiàn)思路贴彼。如果實(shí)現(xiàn)和自己想的一樣潜腻,可能你會(huì)想,啊哈器仗,想到一塊了融涣。如果源碼中是另一種實(shí)現(xiàn)童番,你就會(huì)想 Cool, 還可以這樣玩。 **仿佛如同在和源碼的作者對(duì)話一樣 **威鹿。

這次趁著國(guó)慶假期看了一些「 ##Arthas」的源碼剃斧,大致總結(jié)下。

從源碼的包結(jié)構(gòu)上忽你,可以看到分為幾個(gè)大的 模塊:

  • Agent -- VM 加載的自定義 Agent

  • Client -- Telnet 客戶端實(shí)現(xiàn)

  • Core -- Arthas 核心實(shí)現(xiàn)幼东,包含連接 VM, 解析各類命令等

  • Site -- Arthas 的幫助手冊(cè)站點(diǎn)內(nèi)容

我主要看了以下幾個(gè)功能:

  • 連接進(jìn)程

  • 反編譯class,獲取源碼

  • 查詢指定加載的 class

連接進(jìn)程

連接到指定的進(jìn)程科雳,是后續(xù)監(jiān)控與診斷的 **基礎(chǔ) **根蟹。只有先 attach 到進(jìn)程之上,才能獲取 VM 對(duì)應(yīng)的信息糟秘,查詢 ClassLoader 加載的類等等简逮。

怎樣連接到進(jìn)程呢?

用于類似診斷工具的讀者可能都有印象尿赚,像 JProfile散庶、 VisualVM 等工具,都會(huì)讓你選擇一個(gè)要連接到的進(jìn)程凌净。然后再在指定的 VM 上進(jìn)行操作督赤。比如查看對(duì)應(yīng)的內(nèi)存分區(qū)信息,內(nèi)存垃圾收集信息泻蚊,執(zhí)行 BTrace腳本等等躲舌。

咱們先來想想,這些可供連接的進(jìn)程列表性雄,是怎么列出來的呢没卸?

一般可能會(huì)是類似 ps aux | grep java這種,或者是使用 Java 提供的工具 jps -lv都可以列出包含進(jìn)程id的內(nèi)容秒旋。我在很早之前的文章里寫過一點(diǎn) jps 的內(nèi)容( 你可能不知道的幾個(gè)java小工具 )约计,其背后實(shí)現(xiàn),是會(huì)將本地啟動(dòng)的所有 Java 進(jìn)程迁筛,以 pid做為文件名存放在Java 的臨時(shí)目錄中煤蚌。這個(gè)列表,遍歷這些文件即可得出來细卧。

Arthas 是怎么做的呢尉桩?

在啟動(dòng)腳本 as.sh中,有關(guān)于進(jìn)程列表的代碼如下贪庙,實(shí)現(xiàn)也是通過 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

選擇好進(jìn)程之后蜘犁,就是連接到指定進(jìn)程了。連接部分在 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}

對(duì)于 JVM 內(nèi)部的 attach 實(shí)現(xiàn)止邮,

是通過 tools.jar這個(gè)包中的 com.sun.tools.attach.VirtualMachine以及 VirtualMachine.attach(pid)這種方式來實(shí)現(xiàn)的这橙。

底層則是通過 JVMTI奏窑。之前的文章簡(jiǎn)單分析過 JVMTI這種技術(shù)( 當(dāng)我們談Debug時(shí),我們?cè)谡勈裁?Debug實(shí)現(xiàn)原理) )屈扎,在運(yùn)行前或者運(yùn)行時(shí)埃唯,將自定義的 Agent加載并和 VM 進(jìn)行 **通信 **。

上面具體執(zhí)行的內(nèi)容在 arthas-core.jar的主類中鹰晨,我們來看具體的內(nèi)容:

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到當(dāng)前指定的pid上筑凫,或者是通過 VirtualMachineDescriptor實(shí)現(xiàn)指定進(jìn)程的attach,最核心的就是這一句:

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

這樣,就和指定進(jìn)程的 VM建立了連接并村,此時(shí)就可以進(jìn)行通信啦。

類的反編譯實(shí)現(xiàn)

我們?cè)趩栴}診斷中滓技,有些時(shí)候需要了解當(dāng)前加載的 class 對(duì)應(yīng)的內(nèi)容哩牍,方便確認(rèn)加載的類是否正確等,一般通過 javap只能顯示類似摘要的內(nèi)容令漂,并不直觀膝昆。 在桌面端我們可以通過 jd-gui之類的工具,在命令行里一般可選的不多叠必。

Arthas 則集成了這一功能荚孵。

大致的步驟如下:

  1. 通過指定class名稱的內(nèi)容,先進(jìn)行類的查找

  2. 根據(jù)選項(xiàng)纬朝,判斷是否進(jìn)行Inner Class之類的查找

  3. 進(jìn)行反編譯

我們來看 Arthas 的實(shí)現(xiàn)收叶。

對(duì)于 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);
}

關(guān)鍵的查找內(nèi)容共苛,做了封裝判没,在 SearchUtils里,這里有一個(gè)核心的參數(shù): Instrumentation隅茎,都是這個(gè)哥們給實(shí)現(xiàn)的澄峰。

  /**
 * 根據(jù)類名匹配,搜已經(jīng)被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;
}

通過這樣一個(gè)方法: decompileWithCFR魂毁,所以我們大概了解到反編譯是通過第三方工具「 **CFR **」來實(shí)現(xiàn)的。上面的代碼也是拼 Option然后傳給 CFR的 Main方法實(shí)現(xiàn)出嘹,再保存下來漱牵。感興趣的朋友可以查詢 benf cfr了解具體用法。

查詢加載類的實(shí)現(xiàn)

看過上面反編譯 class 的內(nèi)容之后疚漆,我們知道封裝了一個(gè) SearchUtil的類酣胀,后面許多地方都會(huì)用到刁赦,而且上面反編譯也是在查詢到類的之后再進(jìn)行的。查詢的過程闻镶,也是在Instrument的基礎(chǔ)之上甚脉,再加上各種匹配規(guī)則過濾,所以更多的具體內(nèi)容不再贅述铆农。

我們發(fā)現(xiàn)上面幾個(gè)功能的實(shí)現(xiàn)中牺氨,有兩個(gè)關(guān)鍵的東西:

  • VirtualMachine

  • Instrumentation

Arthas 的整體邏輯也是在 Java 的 Instrumentation基礎(chǔ)上來實(shí)現(xiàn),所有在加載的類會(huì)通過Agent的加載墩剖, 通過addTransformer之后猴凹,進(jìn)行增強(qiáng),然后將對(duì)應(yīng)的Advice織入進(jìn)去岭皂,對(duì)于類的查找郊霎,方法的查找,都是通過SearchUtil來進(jìn)行的爷绘,通過Instrument的loadAllClass方法將所有的JVM加載的class按名字進(jìn)行匹配书劝,一致的會(huì)進(jìn)行返回。

Instrumentation 是個(gè)好同志土至! :)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末购对,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子陶因,更是在濱河造成了極大的恐慌骡苞,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,482評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件楷扬,死亡現(xiàn)場(chǎng)離奇詭異烙如,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)毅否,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,377評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門亚铁,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人螟加,你說我怎么就攤上這事徘溢。” “怎么了捆探?”我有些...
    開封第一講書人閱讀 152,762評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵然爆,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我黍图,道長(zhǎng)曾雕,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,273評(píng)論 1 279
  • 正文 為了忘掉前任助被,我火速辦了婚禮剖张,結(jié)果婚禮上切诀,老公的妹妹穿的比我還像新娘。我一直安慰自己搔弄,他們只是感情好幅虑,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,289評(píng)論 5 373
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著顾犹,像睡著了一般倒庵。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上炫刷,一...
    開封第一講書人閱讀 49,046評(píng)論 1 285
  • 那天擎宝,我揣著相機(jī)與錄音,去河邊找鬼浑玛。 笑死绍申,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的锄奢。 我是一名探鬼主播,決...
    沈念sama閱讀 38,351評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼剧腻,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼拘央!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起书在,我...
    開封第一講書人閱讀 36,988評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤灰伟,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后儒旬,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體栏账,經(jīng)...
    沈念sama閱讀 43,476評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,948評(píng)論 2 324
  • 正文 我和宋清朗相戀三年栈源,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了挡爵。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,064評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡甚垦,死狀恐怖茶鹃,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情艰亮,我是刑警寧澤闭翩,帶...
    沈念sama閱讀 33,712評(píng)論 4 323
  • 正文 年R本政府宣布,位于F島的核電站迄埃,受9級(jí)特大地震影響疗韵,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜侄非,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,261評(píng)論 3 307
  • 文/蒙蒙 一蕉汪、第九天 我趴在偏房一處隱蔽的房頂上張望流译。 院中可真熱鬧,春花似錦肤无、人聲如沸先蒋。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,264評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)竞漾。三九已至,卻和暖如春窥翩,著一層夾襖步出監(jiān)牢的瞬間业岁,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,486評(píng)論 1 262
  • 我被黑心中介騙來泰國(guó)打工寇蚊, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留笔时,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,511評(píng)論 2 354
  • 正文 我出身青樓仗岸,卻偏偏與公主長(zhǎng)得像允耿,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子扒怖,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,802評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容