本文分享自華為雲社群《服務執行時動態掛載JavaAgent和外掛——Sermant熱插拔能力解析》,作者:華為雲高階軟體工程師 欒文飛
一、概述
Sermant是基於Java位元組碼增強技術的無代理服務網格,其利用Java位元組碼增強技術,為宿主應用程式提供服務治理功能,以解決大規模微服務場景中的服務治理問題,透過Java位元組碼增強技術,可以非侵入的提供服務治理能力。在以往版本中,Sermant透過配置-javaagent指令在微服務啟動時接入服務治理能力,當需要接入及解除安裝Sermant時都需要透過重新啟動微服務來完成。但從1.2.0版本開始,Sermant實現了在服務不停機狀態下進行安裝和解除安裝的能力,為服務治理能力帶來全新接入體驗。本文將會對這種動態接入的機制,從技術基礎到Sermant設計進行一次深入分析。
二、JavaAgent載入方式
首先介紹一下JavaAgent的不同接入方式,這是Sermant實現動態接入能力的技術基礎。Java 中Instrumentation API 提供了一種修改位元組碼的機制,利用該API,可以透過修改位元組碼的方式來改變程式的行為,而不用觸及程式的原始碼。JavaAgent為Instrumentation API的客戶端,透過JavaAgent可以呼叫API進行位元組碼的操作,其提供了兩種載入方式給開發者過載:
- 靜態載入:利用premain,在應用程式啟動時載入 JavaAgent稱為靜態載入,靜態載入會在啟動時在執行任何程式碼之前修改位元組碼。
靜態載入時,位元組碼增強是在類載入時發生的,當Java程式啟動時,類載入過程中所有被載入的類都會經過JavaAgent所定義的類檔案轉換器的處理。
- 動態載入:利用agentmain透過Java Attach API將JavaAgent載入到已執行的JVM中,動態載入可以透過位元組碼重轉換的方式在執行時修改位元組碼。
動態載入時,和靜態載入不同的是,此時JVM已在執行,目標類已被載入,就不能像靜態載入時一樣觸發位元組碼增強過程,在使用動態載入的過程中,往.往會透過Instrumentation API來觸發目標類(當然也可以指定所有已被載入的類)的重轉換過程,在重轉換過程中就會觸發到Agent構建的類檔案轉換器,從而完成位元組碼增強過程。
動態載入方式為JavaAgent提供了在JVM執行時接入的能力,但透過類重轉換來觸發位元組碼增強相對於在類載入時增強有一定的侷限性,例如不能在增強時修改類的繼承關係,不能為類新增靜態程式碼塊,不能增強記憶體中和資原始檔中位元組碼不一致的類等,這些也是在使用動態載入和多JavaAgent場景中常見的問題,綜上,兩種載入方式各有利弊,可以在使用時按照業務場景選擇。
三、Sermant熱插拔能力關鍵問題剖析
在瞭解技術基礎後,我們能輕易的想到,理論上基於JavaAgent的動態載入方式,只需要在使用Sermant時,將透過premain方式啟動改為透過agentmain方式啟動,就可以將微服務治理能力動態的接入到微服務中,做到微服務零侵入、微服務不停機的狀態下接入服務治理能力,但通往前方的路上總是充滿了障礙:
3.1 如何保證動態安裝過程中重轉換可順利執行?
這個問題的出現,根源在於JavaAgent透過agentmain方式載入到已執行的JVM中時,不同於靜態載入,會在類初次被載入時完成位元組碼的轉換,動態載入時一些需要被位元組碼增強類已經完成了類載入過程,這時候需要使用Instrumentation提供的類重轉換(retransform classes)能力來修改位元組碼,在Instrumentation的Javadoc中關於這個能力有這樣一段描述:
“The retransformation must not add, remove or rename fields or methods, change the signatures of methods, or change inheritance.(重轉換過程中,我們不能新增、刪除或者重新命名欄位和方法,不能更改方法的簽名,不能更改類的繼承。)”
從中可以看出,在引入動態載入能力前,優先要保證位元組碼增強時,不可以有上述內容中所描述的限制操作。
不過Sermant不太需要擔心這個問題,因為這種限制不僅僅在動態載入時會觸發,在多個JavaAgent同時使用時也可能會觸發,可以參考Sermant團隊的另一篇文章:《記一次多個JavaAgent同時使用的類增強衝突問題及分析》。為了保證在多Agent場景下的相容性,Sermant的位元組碼增強模板嚴格遵循Instrumentation API的限制,因此Sermant在相容性上的不斷改進過程中無心插柳,幫助動態載入能力鋪平了路。
3.2 如何保證在服務治理外掛安裝和解除安裝時不互相影響?
Sermant的設計中,透過位元組碼增強引入的服務治理能力,是透過在目標方法上新增服務治理功能切面來完成的,每一個服務治理外掛,透過一系列切面的配合來達成最終的服務治理效果。不同的服務治理功能,可能會對同一個目標方法進行處理。但並不會對同一個方法進行多次位元組碼增強,而是透過一次位元組碼增強織入排程切面(onMethodEnter、onMethodExit等),透過該切面對相關的服務治理能力(透過攔截器實現,每一個切面會對應一個攔截器的列表)進行排程:
對於服務治理能力的排程邏輯我們在另一篇文章《開發者能力機制解析,玩轉Sermant開發》有講過,本篇不再贅述。
基於框架的基本設計,就需要考慮兩個問題,當外掛在動態安裝時,如何保證不重複位元組碼增強?當外掛解除安裝時,如何保證不會導致有相同目標方法的外掛失效。
- 安裝時如何保證不重複執行位元組碼增強?
在位元組碼增強開發過程中,類檔案轉換器(ClassFileTransformer)是一定會接觸到的概念,開發者需要基於該轉換器來進行位元組碼的處理。在大多數的位元組碼增強框架中,都會對其進行封裝,用於降低位元組碼處理的難度。Sermant基於ByteBuddy提供的類檔案轉換器實現了一種可重入的類轉換器,在外掛動態安裝時,雖然目標方法已經被已安裝的外掛增強過了,但此時還是會觸發類檔案轉換(因為動態安裝外掛的過程是獨立的),當觸發類檔案轉換時,所有相關的類檔案轉換器都會被喚醒,再次觸發類檔案轉換過程。每次可重入類轉換器被喚醒時,將發生以下行為:
在Sermant中維護了一個針對目標方法的位元組碼增強鎖(AdviceKey鎖),即針對每一個目標方法,維護了1個訊號量當做鎖,用於讓各類檔案轉換器來檢查目標方法的位元組碼增強狀態,當目標方法對應的類被類轉換時,就會觸發Sermant所提供的類檔案轉換器,此時類檔案轉換器將嘗試獲取針對目標方法的訊號量,如果能獲取訊號量,則執行對目標方法的位元組碼增強,如果不能獲取,則不執行位元組碼增強。
基於位元組碼增強鎖,在轉換器觸發時,主要有兩條路徑可以走,類檔案轉換器會透過目標方法的AdviceKey(類名+方法hash+類載入器組成的一個唯一表示,用於表示位元組碼增強的目標)來檢查其所關聯的鎖,判斷當前目標方法是否已被Sermant進行過位元組碼增強(織入攔截器排程的切面):
- 能獲取鎖,說明未被增強:則當前檔案轉換器獲取當前AdviceKey所關聯的鎖,將其獲取的鎖透過其對應的外掛來維護,並且執行位元組碼增強,將服務治理所需的攔截器放入該AdviceKey所對應的攔截器列表;
- 不能獲取鎖,說明已被增強:則只將攔截器放入該AdviceKey對應的攔截器列表中,不執行位元組碼增強。
透過上述機制,就可以保證Sermant在安裝不同服務治理外掛時,不會進行重複的位元組碼增強,避免無端的效能和資源損耗。
- 解除安裝時如何保證不會導致其他外掛失效?
當外掛需要解除安裝時,會再次觸發相關目標類的重轉換,與安裝時不同的是,這次需要被解除安裝的外掛釋放自身已經持有的AdviceKey鎖。釋放鎖後,觸發目標類重轉換時,目標類所對應的各個外掛的類檔案轉換器將會再次觸發和安裝時相同的流程:
在這個過程中,未被解除安裝的外掛所提供的對目標類的類檔案轉換器,會在目標類重轉換時,再次觸發,並且只會經歷獲取鎖和位元組碼增強的過程。這樣就保證,如果還有外掛需要對該目標方法進行位元組碼增強時,可以獲得目標方法所對應的鎖,不會因為目標方法的交集而導致其他外掛能力失效。
四、總結
本篇文章對Sermant的熱插拔能力的核心機制進行了解析,希望可以為開發者及使用者在開發或使用相關能力時帶來更多的靈感和便利。更多的熱插拔能力介紹可以參考官網相關文件,Sermant Agent使用手冊,後續我們也會針對熱插拔適用的場景進行進一步分享,敬請期待。
Sermant作為專注於服務治理領域的位元組碼增強框架,致力於提供高效能、可擴充套件、易接入、功能豐富的服務治理體驗,並會在每個版本中做好效能、功能、體驗的看護,廣泛歡迎大家的加入。
- Sermant 官網:https://sermant.io
- GitHub 倉庫地址:https://github.com/huaweicloud/Sermant
點選關注,第一時間瞭解華為雲新鮮技術~