從0-1開發Java性能剖析工具

背景

在這個應用滿天飛的時代,作爲一代寵兒,我們每個人都肩負着使命:保證我們應用的服務質量。服務質量包括:應用的可用性、可靠性、響應指標等。本文的主題更多的是和系統的響應指標相關。

本文作者來自京東生態運營部-保險研發中心工作,從去年6.18開始,就開始連續負責保險系統的6.18、11.11大促的運籌準備工作,同時肩負着保證核心系統的服務質量的要責。

說到大促的準備工作,核心必然離不開整個系統的壓測。每次壓測都會耗費大量的時間和心血在系統調優上,以保證系統提供穩定、高速、可靠的服務。幾次調優下來,金融的各種監控起到了非常棒的助力作用,包括主機監控、SGM監控等。過程中,可以實時的看到應用所在服務器的CPU、內存等硬核指標,同時也可以通過SGM輕鬆的看到秒級的應用響應情況,包括99及999指標等。一旦發現了比較慢的響應時,可以通過SGM的鑽取功能輕鬆的定位到鏈路中真正耗時的部位。

但是有一點比較尷尬,SGM默認只支持比如JSF,HTTP等重要協議入口的方法性能監控,當然如果想監控其它方法,可以通過配置把想要監控的方法配置到SGM中意支持性能監控。但是問題來了,業務相當複雜,中間來回調用可能會涉及到成百上千的方法,這樣都配置上去需要耗費相當的人力。然後還需要在完成監控之後把所有的方法配置再去掉,太繁瑣了。

我們到底需要什麼?

1 能夠輕鬆的在生產或者預發環境引入類似JProfiler的CPU剖析能力

生在Java的世界,真的是幸運的,豐富完整的生態讓我們解決任何問題基本都能夠輕鬆應對。對於診斷工具也是非常豐富,比如說自帶的jstat、jmap,商業化的工具JProfiler、開源的工具VisualVM。進入分佈式時代更是有SkyWalking、Pinpoint等分佈式調用鏈路跟蹤引擎。

JProfiler、VisualVM都有比較強大的性能剖析能力。但很遺憾,商業化的並不方便引入系統中,同時並不能非常方便的引入到我們現有的生產或者預發,沒有能夠提供方便的接入能力,同時不太滿足我們的定製化需求,稍候會說到。

2 支持包配置,可以輕鬆的對所有屬於包內的類的方法調用都會自動進行監控

不同的業務線,肯定包名都是不一致的,不同的業務線使用可以根據自己的訴求進行包名的填加。以對不同的類的方法進行跟蹤監控。

3 支持指定監控時機

業務系統隨時在運行,我們不能隨時都進行監控,我們需要具備在需要時打開監控,不需要的時候關閉監控。

4 支持指定監控入口

剛纔說過,我們默認是支持直接指定包名的,這就可能帶來一個問題,包內的所有方法入口都會被跟蹤監控,這個在真正進行性能剖析的時候不是我們所期望的,這樣必然會有相當一部分不是我們的數據會影響我們的判斷,因爲分析的場景下一般都是針對性比較強的,需要能夠指定我們分析的入口,然後只有進入這個入口才會進行性能剖析數據的採樣,避免不必要的採樣對性能剖析的影響。

5 支持進行統計分析

數據落地後,對於源數據我們稱之爲Raw Data,對我們而言基本是沒有任何價值的。只有統計後的結果纔是對我們有意義的。比如,我們真正想看到的數據是這個樣子的:入口方法總共耗費了多少時間,這麼多時間,大部分的時間被耗費在被調用的哪個方法上了,這樣的數據對我們纔是有意義的。

技術儲備

俗話說:“難者不會,會者不難”,說難的人,說明對事物本身是不瞭解的,真正瞭解的人,必然不會覺得難。就像好多人現在逐漸的開始接觸到區塊鏈,都覺得這個東西有些晦澀,當逐漸的深入之後,會覺得真的沒有那麼難理解。

我相信大家對於任何的技術的學習,都會感同身受。“溫故而知新”,大家對於這句話再熟悉不過了,對的,它出自論語。對於技術,大家一定會有一樣的感覺,隨着我們資歷不斷的提升,我們對以往技術的理解都會不一樣,因爲我們在不斷的長大。

同樣的,想做一個這樣的工具,我們需要具備一定的知識儲備,並且需要有一定深度的認識,不能淺嘗則止。下面我們就具體的說說我們需要哪些技術儲備。

1 Java

貌似是句費話。因爲本身我們做的工具要和Java打交道,甚至是Java的底層字節碼,所以你不僅需要會用Java編程,還要懂一些Java字節碼的知識。

2 類加載機制

一個Java類文件,從源代碼的誕生到最終在JVM中執行,需要經歷好多的過程:

具體每個過程的具體職能,我們這裏就不詳細展開了,大家如果想要深入瞭解,建議看一下這裏:https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-5.html。我們這裏重點說一下類的加載過程。

在Java中,所有的類都是通過類加載器ClassLoader完成類的加載的,主要有三種類加載器:Bootstrap ClassLoader、ExtensionClassLoader、AppClassLoader:

圖中我們可以看到,當我們需要加載一個Class進入JVM的時候,會經過如下步驟:

  • JVM會判斷是否已經加載過這個Class,如果有則直接使用,如果沒有則需要通過ClassLoader機制來加載Class;
  • ClassLoader機制會把加載請求交給App ClassLoader,App會直接委託給Extension來處理,Extension會真委託給Bootstrap來處理;
  • Bootstrap會通過jre的classpath:jre\lib\rt.jar搜索需要加載的類,如果OK就使用這個類,如果不OK,就再委託給Extension來加載;
  • Extension會在jre\lib\ext下搜索對應的類,如果找到就用這個類,找不到就委託給App加載這個類,如果找到就用這個類,找不到就會拋出ClassNotFoundException的異常。

其實這個也就是大家熟知的雙親委派機制,即優先Parents加載,加載不到的,纔會由自己加載。

3 AOP

說起AOP,大家應該並不陌生。類似於我們說OOP,它本身也是一種編程範式,學名叫“面向切面編程”。面向對象的特點是:繼承、封裝、多態。本質上是希望我們把功能按不同的關注點進行很好的隔離,讓編程本身變的更加專注和簡單。

比如,如果大家做業務編程,必然少不了記錄操作日誌、權限管理等。大家一般會選擇怎樣處理。簡單的,大家會直接把相關的代碼放入業務代碼中。下面我們以權限爲例,寫些僞代碼。

我們發現業務代碼只有那麼一行。功能性代碼對業務切入太大了。不喜歡,我們需要調整一下:

是不是感覺好一點點啦,看的稍微乾淨一點了。但還是不太好,業務人員總是需要在業務代碼的邏輯中加入功能代碼,我們再調整一下:

這回啥感受,是不是感覺棒棒的,大家有沒有發現,其實我們整個過程就是一個面向切面的優化過程。只是切面的位置在逐漸的調整。以上這個場景大家發現有個特點,就是無論如何還是需要開發人員告知切面程序相關的業務信息,比如這裏的businessCode。其實,稍候我們說到的切面實現方式,由於本身功能和業務完全無關,所以開發人員可以完全無感知,不需要什麼註解。

4 Java Agent

到這裏開始有些小興奮,因爲馬上就進行核心支撐能力的說明。其實早在JDK1.5的時候,Java就提供了一套Instrumentation的API,這套API提供了一套修改字節碼的能力。這就意味着我們不需要相關類的源代碼,而直接對運行在JVM中的字節碼進行修改,這種能力的魅力自然不用多費口舌啊,必然會給應用能力帶來巨大的影響。

01 - 一個簡單的Java Agent

實現Java Agent需要如下幾個步驟:

  • 必須實現接下這個方法:publicstaticvoidpremain(StringagentArgs, Instrumentation inst)這個實際上是一個入口程序,類似於我們通常理解的Java中的main方法,JVM初始化之後,會調用premain方法,這個方法可以有多個,會按照指定的順序進行調用;
  • 還有關鍵的一步,需要在META-INF中加入MANIFIST.MF文件,裏面會包含一些Agent相關的元數據信息,比如:Premain-Class,Can-Redefine-Classes,Can-Retransform-Classes等信息;
  • 通過-javaagent參數指定我們自己做的agent.jar文件,啓動JVM,所有的transform就會生效。

Java1.6後,有了增強的黑科技,我們不一定非要通過-javaagent指定,Java提供了一個API,我們可以在JVM運行時Attach到JVM上,然後加載agent.jar,再對類進行操作。只不過這個時候我們需要實現的不再是premain方法,而是agentmain方法。性能剖析就是通過JVM的Attach API進行的agent載入和處理。

02 - 字節碼修改工具

常用的字節碼處理工具有:

  • ASM:它是一個直接操作字節碼的工具,分析類信息,修改類信息
  • Javassist:分析、編輯、創建Java字節碼的類庫,這個工具相比ASM提供了多了一些抽象,更容易理解和使用,同時在類的修改等方面也提供了一些便捷的方法
  • ByteBuddy:對字節碼的操作進行了高度抽象,提供了聲明式API,對字節碼的修改和操作同樣的依賴於ASM,不僅僅如此,ByteBuddy提供了大量的API用來降低agent、instrument的使用成本。我們接下來編寫的剖析工具正是基於這個工具。

編碼實錄

我會按照設計思路逐一分析。工具上主要依賴ByteBuddy完成相關功能。

1 述求

  • 能夠在應用啓動時完成Agent的安裝由於環境的限制,我們不可能在jdk級別加入javaagent,我們需要在應用內部啓動一個agent完成類的重新定義;
  • 支持指定對哪些包中的類的方法進行統計記錄我們不可能對所有的包都進行切入分析,成本太高了,而且對於java.lang、java.util層面的包,分析的意義也不大,我們也不可能去改這些類;
  • 可以對進行剖析的方法入口進行指定由於採樣數據落地成本比較高,我們不期望所有的方法都進行記錄。我們只對指定的入口進行剖析和分析。也就是說,只有從特定入口進入的數據,我們纔會統計落地;
  • 能夠指定採樣間隔如果在量比較大的時候,如果我們每次都進行方法耗時的統計、鏈路的跟蹤,勢必會對整體的性能數據產生較大的影響,尤其是方法的耗時級別在毫秒級的時候;
  • 能夠指定採樣閾值在接口被大量訪問是,有可能我們只是關心那些突起的方法調用情況,所以不在毛刺區間的數據我們是沒有必要保留的。只有那些大於指定閾值的數據我們纔會記錄落地;

2 應用內部安裝Agent

  • 依賴ByteBuddy相關Jar

  • 安裝Agent

3 修改字節碼,插入性能分析代碼

我們剖析的核心就在這裏,我們會通過ByteBuddy的Advise機制把我們的代碼切入到所有需要剖析的方法中:

4 把Advice切入到字節碼中

5 測試

接下來就可以測試了,調用包內類的相關方法的時候,就會看到你期望的結果了。

作者介紹

皮亮,京東數科架構師,擁有多個行業系統架構經驗,現負責保險系統的研發及架構相關工作。

本文轉載自公衆號京東數科技術說(ID:JDDTechTalk)。

原文鏈接

從0-1開發Java性能剖析工具

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章