天天看點

去哪兒一站式 Java 應用診斷解決方案 - Bistoury

作者:閃念基因

一、背景

線上應用的診斷一直是日常維護中的難點和痛點,2018年下半年,Alibaba 開源了 java 應用診斷工具 arthas ,讓 java 應用的診斷能力上了一個台階。作為基礎架構團隊,我們自然也對它非常感興趣。研究後發現,arthas 确實是一個非常優秀的 java 診斷工具,但是也存在一些不足。

  • 一是 arthas 更像是一個工具,而不像一個産品。如果要使用它,首先要登入相關機器,然後在機器上下載下傳 arthas,再執行一些指令來運作。這整個流程裡,下載下傳可能出現問題,運作 arthas 也需要具有目标程序相應的權限,還需要先看看對應程序id等等...這些确實隻是一些小問題,但也可以選擇讓這些問題不存在,讓整個使用過程更加流暢。
  • 二是 arthas 缺少 web 界面。指令行界面用起來确實很酷,但不可否認在相當一部分情況下 web 界面更直覺更友好,很多需要查文檔的情況在 web 界面下都可以直接操作,降低了使用門檻。
  • 三是 arthas 所有功能都針對單台機器,實際上很多時候我們需要考慮和觀察整個應用的運作情況,需要一個應用級的視角。
  • 四是 arthas 是一個獨立的工具。對于一個開源工具,這不是一個缺點,但如果能和公司内部的應用中心、釋出系統等做一些适配的話,在使用上會更友善,也能夠做出一些獨立工具做不到的功能。

基于以上的原因,我們決定做一個更強大、更好用的一站式 java 應用診斷解決方案 - Bistoury。Bistoury 內建了 arthas,對上述的幾點不足進行了針對性改進,還提供了線上 debug、線程級 cpu 監控等 killer feature,目前也已經在 github 開源。

二、設計與實作

這一節内容首先會對 bistoury 整體設計進行介紹,讓讀者對 bistoury 有個大體的認識,接下來再對一些設計和功能點進行說明。

2.1 整體設計

Bistoury 涉及到的必要元件有使用者系統、agent、proxy、ui、注冊中心、負載均衡器、應用中心,其中 agent、proxy 和 ui 直接歸屬于 bistoury,其它都屬于外部系統。

  • 使用者系統就是待診斷的正在運作的應用。
  • agent 和待診斷系統部署在同一台機器上,接收來自 proxy 的指令,并根據其類型直接執行一部分指令,還要負責另一部分指令與使用者系統之間的互動。
  • proxy 負責維護與 agent 之間的長期 netty 連接配接,并以 websocket 的方式維護與 ui 在指令執行期間的連接配接,它将 ui 傳來的指令發送給具體的一個或多個 agent,同時對相應的連接配接進行管理。
  • ui 則提供圖形化和指令行界面,接收使用者請求并發送給 proxy,并将最終結果展示給使用者。
  • 注冊中心負責 proxy 的注冊,ui 通過注冊中心擷取 proxy 位址。
  • 負載均衡器負責 agent 到 proxy 的負載均衡,agent 在每次啟動時通過負載均衡器擷取單個 proxy 位址,并與之建立長期連接配接。
  • 應用中心提供應用和機器的相關資訊給 bistoury,用于簡化操作,并提供一些特殊功能需要的資訊。
  • 其它:bistoury 還會通路公司内代碼倉庫等系統,但這些都是具體功能所需要,并不在 bistoury 整體的系統設計上。
去哪兒一站式 Java 應用診斷解決方案 - Bistoury

一次請求

去哪兒一站式 Java 應用診斷解決方案 - Bistoury

一次響應

2.2 位元組碼插樁

Bistoury 的大量功能都使用了位元組碼插樁,同時因為 bistoury agent 需要對應用透明,與應用系統作為不同的程序分開運作,使用了 agentmain 方式動态 attach 到正在運作的使用者系統上。

不過位元組碼插樁和運作時 instrumentation 在網上有大量文章,這裡不再進行具體說明。

2.3 ClassLoader設計

ClassLoader 的設計是 bistoury 系統中非常重要的一個部分,也是 bistoury 能夠透明更新,各項功能能夠正常運作的基礎。Bistoury 內建了 arthas,ClassLoader 的設計也和 arthas 存在一些一緻的地方,但隻說明差異反而無法說清楚整個設計,這裡會一起說明。

下圖所示的是 bistoury agent 動态 attach 後,應用内部的 ClassLoader 結構圖,其中 BistouryClassLoader 是一個 bistoury 專有的 ClassLoader。

去哪兒一站式 Java 應用診斷解決方案 - Bistoury

classloader 結構

2.3.1 BistouryClassLoader

可以從上面圖中看到,BistouryClassLoader 加載了 attach jar,這個 jar 包含了 Bistoury 各個功能具體實作以及依賴 jar 包。

為什麼要使用一個專有的 BistouryClassLoader 呢,這是因為 attach jar 中包含的各個 jar 包在使用者系統中也可能存在,如果版本不一緻很可能會出現問題;bistoury agent 會進行更新,它的功能實作代碼、依賴的jar包都可能變化,需要對它們的影響範圍做一個限制;使用者系統可能非常穩定,甚至一年都沒有重新開機過,而 agent 可能在這一年中更新了幾十個版本,每個版本都需要在使用者系統裡面加載一大堆類,這些 jar 包和類都需要進行解除安裝。

配合從 agent 加載到 BootstrapClassLoader 中的 instrument jar,每次 agent 更新或解除安裝時,做完清理工作後将 instrument jar 中的 ClassLoader 引用重置,就可以将整個 BistouryClassLoader 和裡面的 attach jar 回收。

2.3.2 MagicClassLoader

下面是完整的 bistoury 的 ClassLoader 結構圖,可以看到比前面的 classloader 結構圖多出來一個 MagicClassLoader,這是 bistoury 中的一個特殊的 ClassLoader,用來解決同名類加載優先級問題。

首先來說一說這個問題的場景。

在 bistoury 的開發過程中,為了滿足需求,發現需要對 arthas 和 jackson 的源碼的進行少量修改。可以選擇的解決方案有自己 fork 一個分支,針對 jackson 也可以選擇不使用序列化架構自己寫一個。但要修改的代碼比較少,筆者不想大動幹戈也不想長期維護 fork 分支,隻想要簡單依賴 jar 包就好,于是就有了 MagicClassLoader 的出現。

MagicClassLoader 作用是 bistoury 可以指定一些類,把這些類委托給MagicClassLoader 加載,MagicClassLoader 會優先加載 Bistoury-magic-classes.jar 中的類檔案。這樣的話,隻需要把需要修改源碼的少量幾個類放入 Bistoury-magic-classes.jar,就可以達到修改 jar 包中源代碼的目的。

去哪兒一站式 Java 應用診斷解決方案 - Bistoury

完整 classloader 結構

2.4 線上 debug

一直以來,調試都是線上應用的痛點。

曾經在微網誌上流傳着這麼一個程式員才懂的笑話,NASA 要發射一個新型火箭,火箭發射升空後發現不行,NASA 把火箭拖回來加了兩行 log,再次發射,發現又不行,又加了兩行 log 發射,發現又不行...

當然這隻是一個笑話,但這樣的場景在我們的實際開發中卻屢見不鮮,多少次我們解決故障的時間就在不斷地加 log,釋出,加 log,釋出的過程中溜走。

Arthas 的 watch 指令讓我們可以觀察函數的入參、傳回值、異常等等,然而似乎每次 watch 都需要看看文檔裡參數該如何設定,面對函數中的本地變量也是無能為力,特别是行數較多的方法,方法内部的情況還是難以明了,想象一下面對上百行的方法,你需要腦補出其中各個本地變量值的情形,這個時候,我們需要的是 ide 的 debug 功能。

Bistoury 的線上 debug 功能正是針對這個場景而生,它模拟了 ide 的調試體驗,在功能上和遠端調試,或者說你在 ide 上 debug 本地代碼幾乎一緻。你在代碼某一行打一個斷點或條件斷點,斷點觸發就能看到本地變量、成員變量、靜态變量以及調用棧;與 idea 遠端 debug 不同的是,它不需要在系統啟動就帶上調試相關參數,對應用完全透明,同時在斷點觸發時不會暫停整個系統,而是隻列印斷點處快照資訊,列印後繼續執行代碼邏輯,完美符合我們對線上應用的 debug 需求。

2.4.1 對比

這裡用一個簡單例子在對 arthas 的 watch 和線上 debug 進行對比。假如我們要 debug 如下一段簡單的代碼。

protected ModelAndView detailView(String viewName, String code) {
    Application app = applicationManager.getAppByCode(code, From.master);
    applicationManager.checkOwner(app);
    return createView(viewName).addObject("app", app);
}           

下圖是使用 arthas 進行 watch 的結果:

去哪兒一站式 Java 應用診斷解決方案 - Bistoury

arthas watch

從圖裡可以看到,arthas 能夠擷取到傳入方法的兩個參數也就是 viewName 和 code 的資訊,但如果我們想檢視這裡局部變量 app 的屬性呢?也許傳回值裡能找到它,但局部變量可不一定都會出現在傳回值裡。

如果是使用 bistoury,我們可以在下圖第71行源代碼處打上斷點,擷取執行資訊:

去哪兒一站式 Java 應用診斷解決方案 - Bistoury

bistoury 線上 debug

斷點觸發後,在右邊的局部變量一欄,bistoury 會将 app、code、viewName 三個局部變量都展示出來,加上調用棧等資訊,可以起到類似于本地 ide debug 的效果。

2.4.2 原理

下面兩段代碼表現了線上 debug 打斷點前後的差異。

userSystem.preDo(); 
userSystem.do();
userSystem.afterDo();           
userSystem.preDo();
if (hitBreakPoint()) {
    captureSnapshot();
}
userSystem.do;
userSystem.afterDo();`           

當使用者添加斷點後,bistoury 會在斷點處添加位元組碼,判斷是否需要觸發斷點,捕捉斷點處上下文資訊。

函數的入參、傳回值、異常、靜态變量等資訊我們通過 arthas 也可以擷取,bistoury 更進一步的是擷取到了本地變量的資訊。

這裡涉及到兩個問題:斷點設定在源碼處,如何對應位元組碼裡的位置;這個位置有哪些本地變量,它們的名字和值如何擷取。

通過查閱 java 虛拟機規範,我們可以發現,java 類位元組碼裡用來表示方法的 method_info 結構有一個 code 屬性,code 屬性的屬性表裡有一個叫做 LineNumberTable,這裡用 java 代碼來近似描述 LineNumberTable 的一部分結構:

class LineNumberTable {
    LineNumber[] lineNumbers;
}
class LineNumber {
    short start_pc; // 方法body位元組碼數組的索引
    short line_number; // 源檔案的行号
}           

根據 java 虛拟機規範裡的說明,每一次源檔案行号發生變化都會在 lineNumbers 裡添加一條記錄。那麼我們就可以對位元組碼檔案進行掃描,根據掃描結果可以得出每一個源檔案行号所對應的位元組碼索引範圍,也就知道了位元組碼應該添加在哪裡。

同樣是在 code 屬性的屬性表中,我們還可以找到一個名為 LocalVariableTable 的屬性,還是用 java 代碼來對其中一部分結構進行描述:

class LocalVariableTable {
    Varible varible;
}
class Varible {
    short start_pc;         // 變量在body位元組碼數組的起始索引
    short length;           // 變量在body位元組碼數組存在的長度
    short name_index;       // 變量名的索引
    short descriptor_index; // 變量類型的索引
    short index;            // 變量在局部變量表的索引
}           

可以看到,變量的範圍就是[start_pc, start_pc + length),而通過 index 字段我們可以擷取到變量的值。

結合前面的 LineNumberTable 資訊,就可以擷取斷點處有哪些本地變量,達到擷取斷點處本地變量資訊的目的。

2.5 線程級cpu監控

在系統的日常運維中,有時會碰到 cpu 使用率突然飙高的情況。這個時候我們會登入機器,top 檢視程序 id,top -h 檢視消耗 cpu 的線程 id,然後 jstack 看看對應的線程是哪一個,最後再進行具體分析。 暫且不考慮這一系列操作需要的時間,我們收到報警的時候可能正在公司外吃飯,或是正在睡覺,而等我們做好準備登入上機器時問題已經結束了,現場沒了,我們還能做的就隻是看着機器的 cpu 監控圖一臉茫然...

可以看到,傳統的機器 cpu 使用率監控給出的資訊量太少,它能夠幫助發現問題,但在解決問題時作用不大。

Bistoury 的線程級 cpu 監控正是為解決各種 cpu 問題而生,讓此類問題的解決難以置信的簡單。

去哪兒一站式 Java 應用診斷解決方案 - Bistoury
去哪兒一站式 Java 應用診斷解決方案 - Bistoury

線程級 cpu 監控效果

上面兩個圖是對 bistoury 線程級 cpu 監控效果展示,可以看到,我們可以擷取到每一分鐘應用 cpu 和線程的完整資訊,從使用率到線程棧,從線程名到線程狀态,所有資訊一目了然。

2.5.1 原理

Cpu 使用率可以在機器 /proc 檔案夾下面擷取,通過每隔一分鐘進行一次檢測,我們可以擷取到線程從上一分鐘到目前的 cpu 使用時間,與 cpu 使用時間做對比,就可以擷取到每分鐘實際的 cpu 使用率;而瞬時 cpu 使用率可以通過間隔幾秒鐘進行檢測擷取。

Linux 線程 id 和 jvm 線程的關聯與我們日常運維的操作類似,都是通過 jstack 擷取線程号來進行關聯,同時在 jstack 中,我們還可以擷取到具體的線程棧和鎖等各種資訊。

最後将每分鐘的資訊持久化,通過界面将資料展示出來,這也就是 bistoury 的線程級 cpu 監控。

2.6 堆對象統計

有時候我們會發現應用記憶體的增長不太正常,但使用 jmap dump 整個應用記憶體分析又感覺有點大費周章,想要簡單一點解決問題,這時候可以使用 bistoury 的堆對象統計功能。

打開堆對象統計開關後,bistoury 每分鐘使用類似 jmap - histo 的指令,擷取整個 jvm 堆裡不同類型對象的統計資訊。通過與存儲系統結合,我們就能夠将不同時間段的統計資訊做對比,找出可能有問題的地方(比如一段時間内增長最快的對象)進行分析處理。

去哪兒一站式 Java 應用診斷解決方案 - Bistoury

堆對象統計

三、結尾

本文對 bistoury 的開發背景和整體設計做了簡單介紹,并對一些設計和功能實作進行了具體描述,希望能給讀者在 java 應用的診斷方面帶來一定的啟發。

作者介紹

·聶振宇:2013年加入去哪兒網,一直從事中間件相關的開發。

·謝磊:2018年加入去哪兒網,從事 bistoury、qtrace 等中間件開發,對 java 位元組碼插樁、問題排查工具、服務可觀測性有豐富的實踐經驗。

來源:微信公衆号:Qunar技術沙龍

出處:https://mp.weixin.qq.com/s/W7q0qgVsTSOwAuleEE-WRw

繼續閱讀