Sermant是基於Java位元組碼增強技術的無代理服務網格,其利用Java位元組碼增強技術為宿主應用程式提供服務治理功能。 ...
本文分享自華為雲社區《Sermant類隔離架構解析——解決JavaAgent場景類衝突的實踐》,作者:華為雲開源。
Sermant是基於Java位元組碼增強技術的無代理服務網格,其利用Java位元組碼增強技術為宿主應用程式提供服務治理功能。因深知JavaAgent場景中類衝突問題會造成的影響,Sermant在設計之初便為此規划了全面的類隔離架構。經歷多次迭代,如今Sermant的類隔離架構已可以輕鬆的應對各種複雜的類載入環境。
一、JavaAgent場景為什麼要註意類衝突問題?
類衝突問題並非僅存在於JavaAgent場景中,在Java場景中一直都存在,該問題通常會導致運行時觸發NoClassDefFoundError、ClassNotFoundException、NoSuchMethodError等異常。
從使用場景來看,基於JavaAgent技術所實現的工具,往往用於監控、治理等場景,並非企業核心業務程式。如果在使用時引入類衝突問題,可能會造成核心業務程式故障,得不償失,所以避免向核心業務程式引入類衝突是一個JavaAgent工具的基本要求。
還有一個重要原因是在Java應用中可以於開發態採用依賴的升降級、統一依賴架構治理等手段解決該問題。但基於JavaAgent技術實現的工具作用於運行態,無法在開發態就和需要被增強的Java應用進行統一的依賴管理,所以引入類衝突問題的可能性更大。
二、JavaAgent場景如何解決該問題?
無論是在Java應用中,還是在JavaAgent場景,修複類衝突的邏輯都是一致的,就是避免引入會衝突的類。不同點在於基於JavaAgent技術實現的各式各樣的工具,往往都具有業務無關性,在設計和實現之初,並不會為特定的Java應用類型而定製。對於JavaAgent程式而言,需要被位元組碼增強的應用即是黑盒,所以無法像Java應用那樣去梳理依賴結構,排除、升級依賴項,統一進行依賴架構治理。並且JavaAgent往往在運行時使用,所以只能通過保障依賴絕對隔離的方式來避免引入衝突。
為何會產生類衝突,本文重點不在此,簡單講是因為我們在Java中因為重覆引入傳遞依賴、類的載入順序無法控制等問題,導致引入了相同【類載入器和全限定類名(Fully Qualified Class Name)都相同】但又表現不同【因為類版本不同而導致的類邏輯不一致】的類。所以為了避免衝突,我們就需要避免在運行時引入相同的類,如何讓JavaAgent引入的類和宿主完全不相同,從全限定類名和類載入器下手才是根本:
- 基於maven shade plugin進行類隔離
該插件是Maven提供用於構建打包的插件,通過maven-shade-plugin的‘Relocating Classes’能力,來修改某些類的全限定類名。
此方法的原理便是通過改變全限定類名來讓JavaAgent引入的類和Java程式的類完全不可能出現相同的情況,從根本上避免類衝突。但是我們在使用一種框架,或者使用一種產品時,往往約定要優於配置,基於maven-shade-plugin通過配置去改變全限定類名並不是一個簡單的辦法,在使用時就需要針對JavaAgent所涉及依賴進行梳理,在maven-shade-plugin中進行配置,並且需要在每次依賴變更後重新篩查。對持續迭代極不友好。
採用上述方法也對Debug造成阻礙,在Debug過程中被重定向的類的斷點將不可達,嚴重降低調試效率。
- 基於類載入機制進行類隔離
基於maven-shade-plugin修改全限定類名往往用來解決單點的類衝突問題,雖然也能做到將JavaAgent所引入類完全隔離,但並不是一個好的解決方案。
基於類衝突原理,我們還可以通過限制兩個相同全限定類名的類的載入器來讓其不同,如Tomcat那樣,通過自定義類載入器破壞Java的雙親委派原則,來隔離JavaAgent引入的類。這樣既避免了繁重的配置,也避免了依賴變更而帶來的影響。但也有其弊端,在JavaAgent場景中往往會利用到Java應用程式的類,所以基於類載入器的隔離機制,往往就讓開發者只能通過反射等操作完成此類邏輯,這會對性能和開發效率產生不良影響。
三、Sermant如何做?
Sermant是基於Java位元組碼增強技術的無代理服務網格,不僅是一個開箱即用的服務治理工具,也同樣是一個易用的服務治理能力開發框架。
“把簡單留給別人,把麻煩留給自己!”
Sermant從設計之初就遵循上述重要原則,並規划了全方面的類隔離架構,利用Java的類載入機制對自身各模塊做了充分類隔離,讓使用者和開發者無需考慮因使用JavaAgent而導致類衝突問題,並且也針對開發者的使用場景做了優化,可以在開發中無縫使用被增強Java程式的類,避免因反射等行為帶來的不利影響。Sermant是如何實現的呢,下文將對Sermant類隔離架構進行詳細解析。
1) Sermant的類隔離架構解析
如上文所說,Sermant不僅是個開箱即用的服務網格,也同樣是一個易用的服務治理能力開發框架,服務治理能力是多樣的包括但不限於流量治理、可用性治理、安全治理等,所以Sermant採用插件化的架構來讓用戶能更靈活的接入和開發服務治理能力。
在Sermant的整體架構下,我們不僅需要保證不向宿主服務引入類衝突問題,避免在開箱即用時對宿主服務造成負面影響,同時也需要保障框架與插件、插件與插件之間不會引入類衝突問題,避免插件開發者因為和其他服務治理插件產生類衝突問題而苦惱,所以Sermant設計瞭如下類隔離結構:
- SermantClassLoader,破壞雙親委派,用於載入Sermant框架核心邏輯,併在AppClassLoader下隔離出Sermant的類載入模型。避免受到宿主服務自身複雜類載入結構的影響,減少應對不同類載入結構服務的適配需求。
- FrameworkClassLoader,破壞雙親委派,主要作用是隔離Sermant核心能力所引入的三方依賴,避免向宿主服務及服務治理插件引入類衝突問題。目前的主要場景 ①用於隔離Sermant的日誌系統,避免對宿主服務的日誌系統產生影響 ②隔離Sermant框架的核心服務(心跳、動態配置、統一消息網關)所需三方依賴。
- PluginClassLoader,遵循雙親委派,主要用於隔離Sermant各服務治理插件,避免不同服務治理插件之間產生類衝突問題。
- ServiceClassLoader,破壞雙親委派,主要用於隔離插件中的依賴,通過該類載入器載入插件服務的相關lib(插件服務會在插件載入時被Sermant初始化),開發者可任意引入三方依賴,無需關心對插件主邏輯的影響。
其中的PluginClassloader和ServiceClassloader不僅在類隔離中起到至關重要的作用,更是一種長遠的考慮,為每個插件設計獨立的類載入器,使得Sermant可以平滑的進行插件動態安裝&卸載以及插件熱更新。
2) 插件隔離的特殊之處
在上文中所述類隔離架構中,可以看到一處特別的邏輯(紅框處),這也是Sermant中PluginClassLoader(插件類載入器)的特殊之處,在實際使用過程中,每個插件類載入器會在其中為每個線程維護一個局部的類載入器(localLoader)。
PluginClassLoader遵循雙親委派,在類載入過程中先委派SermantClassLoader載入Sermant的核心類,再通過自身載入插件類,當需要使用宿主服務的類時,則會委托局部類載入器(其Parent可以是任何類載入器,不局限於圖中所指示)進行載入。用於讓位元組碼增強的切麵邏輯(Sermant攔截器)可以獲取到宿主服務所使用的類,這有利於服務治理場景,其邏輯如下圖所示:
通過重寫類載入器loadClass邏輯,在執行Sermant攔截器時,配置一個局部的類載入環境,讓Sermant攔截器中的邏輯可以順利的使用宿主服務載入的類,這樣開發服務治理插件時無需通過反射獲取宿主服務的類,從而提升服務治理能力的開發效率和最終運行時的性能,同時還避免了宿主服務和服務治理插件的類衝突。
(代碼實現可以在開源倉庫進行查看:)
3) 實戰效果如何
因接入JavaAgent而導致的依賴衝突、類衝突問題乃是業界通病,但如果有Sermant的類載入機制加持,該問題則可從根源避免,不再讓廣大JavaAgent的使用者和開發者深受其害!
《拜托,別在 agent 中依賴 fastjson 了》所述案例,是一個因JavaAgent而產生的依賴衝突問題的典型場景,其應用通過AppClassLoader載入到了Agent中fastjson的類FastJsonHttpMessageConverter, 該類依賴spring-web.jar的類GenericHttpMessageConverter,但由於AppClassLoader的搜索路徑中並沒有spring-web.jar(fastjson通過provide方式引入),最終載入類失敗。
但如基於Sermant開發則不會產生該問題,基於Sermant開發JavaAgent和Spring應用一起運行時的類隔離架構如下:
在此類載入器的結構下,有兩個關鍵的不同:
- 由於Sermant改變了類載入的結構,通過Agent引入的fastjson已不在AppClassLoader的搜索路徑中,因此Agent中的FastJsonHttpMessageConverter類不再會被Spring應用通過AppClassLoader載入到,從根源上避免了文中所觸發的類衝突問題。
- 當運行時若Agent需要使用spring-web的類GenericHttpMessageConverter時,則可通過Sermant提供的局部類載入環境成功通過LaunchedUrlClassloader成功從Spring應用中獲取。
正是因為此兩點差異,讓基於Sermant開發的能力可以在和應用之間進行類隔離,避免通過JavaAgent引入類衝突問題,同時可以在運行時使用應用所引入的類。
四、總結
Sermant是基於Java位元組碼增強技術的無代理服務網格,其利用Java位元組碼增強技術為宿主應用程式提供服務治理功能。因深知JavaAgent場景中類衝突問題會造成的影響,Sermant在設計之初便為此規划了全面的類隔離架構。經歷多次迭代,如今Sermant的類隔離架構已可以輕鬆的應對各種複雜的類載入環境。
除了保證類隔離,Sermant作為服務網格需要重點關註自身的服務治理能力對宿主服務帶來的性能影響,所以也通過獨有設計避免因為過度隔離帶來的性能損耗。同時Sermant還在構建開放的服務治理插件開發生態,並提供高效的服務治理能力開發框架。在類隔離設計時也考慮到了易用性、開發效率提升等方面的問題。並未因為類隔離機制的存在,而降低開發的效率,增大學習曲線的陡峭程度。
Sermant 作為專註於服務治理領域的位元組碼增強框架,致力於提供高性能、可擴展、易接入、功能豐富的服務治理體驗,並會在每個版本中做好性能、功能、體驗的看護,廣泛歡迎大家的加入。
- Sermant 官網:https://sermant.io
- GitHub 倉庫地址:https://github.com/huaweicloud/Sermant