Java依賴衝突高效解決之道

簡介:由於阿里媽媽聯盟團隊負責業務的特殊性,系統有龐大的對外依賴,依賴集團六七十個團隊服務及N多工具組件,通過此文和大家分享一下我們積累的一些複雜依賴有效治理的經驗,除了簡單技術技巧的總結外,也會探討一些關於這方面架構的思考,希望此文能系統徹底的解決java依賴衝突對大家的困擾。

image.png

作者 | 澄江
來源 | 阿里技術公衆號

一 概述

由於阿里媽媽聯盟團隊負責業務的特殊性,系統有龐大的對外依賴,依賴集團六七十個團隊服務及N多工具組件,通過此文和大家分享一下我們積累的一些複雜依賴有效治理的經驗,除了簡單技術技巧的總結外,也會探討一些關於這方面架構的思考,希望此文能系統徹底的解決java依賴衝突對大家的困擾。

二 依賴衝突產生的本質原因

要解決依賴衝突,首先要理解一下java依賴衝突產生的本質原因。

image.png

圖1

以上圖爲例,目前阿里大部分java工程都是maven工程,此類工程從開發到上線要經歷以下兩個重要步驟:

1 編譯打包

平時我們編寫的應用代碼,用maven編譯應用代碼時,maven只依賴第一級jar包(A.jar,B.jar,*.jar)既完成應用代碼的編譯,至於傳遞依賴的jar包(Y.jar,Z.jar)maven首先會對同名不同version的jar包進行依賴仲裁,然後依據仲裁結果下載對應的jar放到指定目錄下(例如上圖中Y.jar最終只會仲裁1.0或2.0一個版本,此處假定仲裁到2.0版本,Z.jar即便內容與Y.jar一致,但名稱不一樣所以不屬於maven仲裁範疇)。

有一點需注意不同maven版本可能會有差異,這會導致有時本地環境和日常、預發打包不一致造成應用邏輯表現不一致的情況(說明一下這種情況還有其他一些原因會導致,不是說一定是maven版本不一致仲裁結果不一致導致的)。

2 發佈上線

先明確一個概念,在JVM中,一個類型實例是通過它的全類名和加載它的類加載器(ClassLoader)實例來唯一確定的。所以所謂的“類隔離”,實際就是通過不同的類加載器實例去加載需要隔離的類來實現的,這樣即便兩個全類名完全相同但內容不同的類,只要他們的類加載器實例不同,就能在一個容器進程中共存,並且各自運行互不干擾。

發佈啓動容器時,不管是tomcat、taobao-tomcat還是PandoraBoot,還是其他容器, 首先都是用特定的類加載器實例先加載容器本身依賴的jar包,容器一般都會有多個類加載器實例,容器自身所依賴的jar包一般由專門的類加載器實例加載實現與應用包的絕對隔離,像Pandroa還有專門的類加載器實例加載淘系中間件避免中間件與應用類衝突,如下圖所示:

image.png

容器內部依賴jar加載完成後,才輪到必然的一步:由某個應用ClassLoader實例(一般與容器類加載器實例不是一個)來加載編譯打包階段打出來的應用jar包及應用.class程序,這樣容器才能運行業務,同時確保應用不會干擾容器的運行。

例如圖1中,最終打出的應用包中Y.jar-2.0,Z.jar都有com.taobao.Cc.class類,但一個應用ClassLoader實例僅能加載V3或V2中一個版本的com.taobao.Cc.class類。

那到底會加載哪個版本的com.taobao.Cc.class類呢?答案是不一定,這個取決於容器應用類加載實現策略, 從以往遇到的情況看,tomcat,taobao-tomcat、Pandora的做法都是直接裝載應用lib包下所有.jar包文件列表(上例是A.jar,B.jar,*.jar,Y.jar,Z.jar。除tomcat外都沒看源碼覈實過,有錯歡迎糾正)。但Java 在裝載一個目錄下所有jar包時, 它加載的順序完全取決於操作系統!而Linux的順序完全取決於INode的順序,INode的順序不完全能一致,所以筆者之前就遇到類似的問題,上線20臺機器,用同一個鏡像,有2臺就是起不來的情況。遇到這種情況目前就只能乖乖按以下章節中的手段去解決了。理論上最正確的做法應該是容器裝載應用 jar包時,按指定順序加載。

基於以上分析,我們可以得出結論,基本所有的類衝突產生的本質原因:要麼是因爲maven依賴仲裁jar包不滿足運行時需要,要麼是容器類加載過程中加載的類不滿足運行時需要導致的。

關於容器類加載隔離策略,網上ATA上有很多資料介紹,本文重點向大家講解遇到衝突的各種解決之道,解決衝突大家只需要知道以上重點原理就夠了。

理解了依賴衝突產生的本質原因,那麼發生依賴衝突如何高效定位具體是哪些jar包引起的衝突呢?請繼續看下一章節。

三 依賴衝突問題高效定位技巧

發生依賴衝突主要表現爲系統啓動或運行中會發生異常,99%表現爲三種NoClassDefFoundError、ClassNotFoundException、NoSuchMethodError。下面逐一講解一下定位技巧。

1 NoClassDefFoundError、ClassNotFoundException排查定位步驟

STEP1、發生NoClassDefFoundError首先要看完整異常棧,確認是否是靜態代碼塊發生異常,靜態代碼塊發生異常堆棧與jar包衝突有很明顯的區別,出現"Could not initialize"、"Caused by: ..."關鍵字一般是靜態代碼塊發生異常導致類加載失敗:

image.png

因爲靜態代碼塊發生異常導致NoClassDefFoundError,修改靜態代碼塊避免拋出異常即可。如果不是靜態代碼塊發生異常導致的問題,繼續下一步。

STEP2、如果不是靜態代碼塊發生異常導致加載失敗,異常message關鍵字中會明確顯示缺失的類名稱,例如:

image.png

STEP3、在IDEA中(快捷鍵Ctrl+N)查找異常棧中提示缺失的類在哪些版本的jar包中有,如上例中的org.apache.commons.lang.CharUtils

image.png

STEP4、查看應用部署機器上應用lib包目錄下(一般是/home/admin/union-uc/target/${projectName}/lib或union-pub/target/${projectName}.war/WEB-INF/lib)是否存在上一步驟中查出對應版本的jar包,以上情況一般是因爲此時應用依賴的是低版本jar包,而jar包中又沒有衝突的類,絕大部分情況下NoClassDefFoundError、ClassNotFoundException定位確認都是因爲maven依賴仲裁最終採納的jar包版本與運行時需要的不一致導致。

2 NoSuchMethodError排查到位步驟

STEP1、發生NoSuchMethodError,異常堆棧日誌核心片段(異常棧中處於棧底的片段,見過很多同學發生異常亂翻一通,那樣毫無意義,要有目的的翻關鍵地方,不要亂翻)會明確顯示具體是哪個類,缺失了哪個方法,異常堆棧核心片段示例如下:

Caused by: java.lang.NoSuchMethodError: org.springframework.beans.factory.support.DefaultListableBeanFactory.getDependencyComparator()Ljava/util/Comparator;
    at org.springframework.context.annotation.AnnotationConfigUtils.registerAnnotationConfigProcessors(AnnotationConfigUtils.java:190)
    at org.springframework.context.annotation.ComponentScanBeanDefinitionParser.registerComponents(ComponentScanBeanDefinitionParser.java:150)
    at org.springframework.context.annotation.ComponentScanBeanDefinitionParser.parse(ComponentScanBeanDefinitionParser.java:86)
    at org.springframework.beans.factory.xml.NamespaceHandlerSupport.parse(NamespaceHandlerSupport.java:73)

首先需確認JVM中當前加載的缺失方法類,如上"org.springframework.beans.factory.support.DefaultListableBeanFactory"類到底來自哪個jar包,目前最高效的辦法:

外部環境容器下,或者某些容器版本過低不支持Arthas在線診斷的情況下,可以通過在JVM啓動參數中增加" -XX:+TraceClassLoading",然後重新啓動系統,在系統工程日誌中即可看到JVM加載類的信息。從中即可找到JVM是從哪個jar包中加載的。

STEP2、在IDEA中(快捷鍵Ctrl+N)查找異常棧中提示缺失的類在哪些版本的jar包中有,如下圖所示:

image.png

然後依次查看各版本jar包中衝突類的源碼,工程中部分jar打包時附帶了源碼包可直接看到源碼,不帶源碼的需要用IDEA插件(推薦jad)反編譯一下。然後依次搜尋各個jar包中的衝突類,搜尋第一步是點擊上圖中某個版本類,在IDEA中查找類級次關係(快捷鍵Ctrl+H),如下圖所示:

image.png

然後在衝突類及所有衝突類的父類源碼中找到NoSuchMethodError異常信息中描述缺失的方法,以上例子中就是"getDependencyComparator()Ljava/util/Comparator"。

上例中通過搜尋可以發現spring-beans-3.2.1.RELEASE.jar,spring-2.5.6.SEC03.jar兩個版本DefaultListableBeanFactory類及父類中沒有"getDependencyComparator()Ljava/util/Comparator"方法,spring-beans-4.2.4.RELEASE.jar,spring-beans-4.3.5.RELEASE.jar兩個版本DefaultListableBeanFactory類中有缺失的"getDependencyComparator()Ljava/util/Comparator"方法。

STEP3、查看應用部署機器上應用lib包目錄下(一般是/home/admin/union-uc/target/${projectName}/lib或union-pub/target/${projectName}.war/WEB-INF/lib)下,找到相關jar包的版本,如上例中:

 

image.png

致此定位問題根本原因是應用啓動時加

載"org.springframework.beans.factory.support.DefaultListableBeanFactory"類未加載到運行時預期所需的spring-beans-4.3.5.RELEASE.jar版本,而是加載了spring-2.5.6.SEC03.jar導致。

按照以上流程步驟,基本99%的依賴衝突都可以定位到根本原因。定位到原因後如何解決衝突呢?事實上有些時候解決衝突遠沒有內網上很多帖子描述的"mvn dependency:tree"一下,排排jar那麼簡單。具體細節請繼續看下一章節。

四 通過maven調整依賴jar解決依賴衝突

1 升降級jar包解決依賴衝突

上一章節中的第一個例子中,最簡單的情況,如果發生衝突的jar包高版本是完全兼容低版本功能的情況下,只需在pom中簡單升級jar包版本即可。

但如果衝突 jar包高版本不兼容低版本,且應用依賴不是很複雜的情況下,可以分析升級衝突jar包後會對哪些業務有影響,具體做法推薦通過IDEA Maven Helper 插件查找衝突jar包有哪些業務依賴(此處不推薦"mvn dependency:tree",目前本人見過的大部分Maven工程都有多個Module,比如-dal,-Service,*-Controller,這類工程結構如果module未單獨打包上傳Maven倉庫,"mvn dependency:tree"是不能完整分析依賴關係的),記錄下來。如下圖所示:

image.png

然後升級衝突包,通過迴歸測試受到影響的二方庫對應的業務點。

如果應用依賴非常複雜(例如衝突包有幾十個二方庫依賴,或者依賴衝突包的二方庫是個基礎包,業務系統中無法清晰枚舉出使用受影響二方庫的業務點),這種情況下,如果要通過升級jar包解決依賴衝突,必須完整迴歸整個應用功能。筆者有幾次因爲迴歸不全面引發故障的慘痛經歷,希望大家不要重蹈覆轍。通過這幾次事例,筆者深刻理解到我們這個時代最偉大的計算機科學家Dijkstra大神“簡單是可靠的先決條件”這句至理名言,深深的體會到如果一個系統複雜到你完全無法理清楚他錯綜複雜的依賴關係的時候,那說明你該重構你的系統了,否則系統維護將會逐步變成噩夢。

當然不是所有情況都可以通過升降級jar解決衝突,舉個例子:

image.png

如上圖假設應用系統同時依賴A.jar,B.jar,而A.jar,B.jar都依賴protobuf-java,系統運行時都會分別用到A.jar,B.jar中protobuf部分的功能,而且A.jar,B.jar依賴的protobuf版本無法通過升高降低版本調整到一致。由於protobuf-java3.0版本序列化協議,類內容各方面都不兼容protobuf-java2.0版本。這種情況無論如何調整依賴都無法解決衝突的問題,要解決這種衝突,請繼續往下看,第五第六章內容。

2 排除jar包解決依賴衝突

上一章節中第二個例子,主要原因是容器啓動時加載到的類不是預期spring-beans-4.3.5.RELEASE.jar中的類,而是spring-2.5.6.SEC03.jar包中的類,如果spring-2.5.6.SEC03.jar排除對業務無影響,可以通過排除spring-2.5.6.SEC03.jar來解決衝突。與上一節例子類似,可以通過IDEA Maven Helper 插件確定spring-2.5.6.SEC03.jar是由哪個jar間接依賴進來的,判斷業務的影響範圍,此處不在贅述。與上一節一樣,類似的情況不一定都可以用排除jar解決。

五 通過pandora自定義插件解決依賴衝突

第四章中有講到,如果一個應用中要同時運行兩個不兼容版本的jar包,是無法通過Maven調整依賴關係解決的。第二章講解依賴衝突原理時有提到,Pandora通過類隔離機制實現了集團各個中間件之間的隔離,Pandroa同時也支持業務方按規範創建一個可以運行在Pandora容器中的插件,容器幫業務方實現加載隔離。

聯盟一淘團隊就將類似IC、卡券這種核武器級存在的二方包根據自己業務的需要進行裁剪包裝後,製作成Pandora插件來避免依賴衝突,取得了很好的效果。

用Pandora插件確實能在不對應用做很大調整,不影響性能的情況下完美解決依賴衝突問題。

但也有一些問題就不太適合用局部方法解決了,比如:

當維護的應用依賴過於複雜,每個應用依賴外部三四十個二方庫時。這種重量級應用就會嚴重影響生產效率。

image.png

如上圖所示,早期本人負責聯盟用戶平臺時,就遇到兩個巨無霸應用,adv(6w+代碼)、pub(12w+代碼)。

一方面因爲依賴多,基本每週都會遇到集團各種升級,安全問題,各種小修小補,不斷的上線。一方面業務發佈需求也較多。

導致需要頻繁發佈,比如有一年個人就發佈了566次。此時龐大的依賴導致部署效率,影響評估迴歸都會很難,此時就不應該從局部解決衝突這種視角去看,應該考慮優化應用架構,進行依賴治理,儘量避免衝突。

六 通過依賴架構治理解決依賴衝突

1 複雜依賴標準化、簡化治理

首先,依賴本身就是一種複雜的業務。大部分依賴背後都有較深的業務領域知識 或者 技術領域知識。

比如我們查詢搜索。

業務領域知識方面,光銷量就有交易成交筆數,成交件數,搜索銷量【有些訂單不計入搜索銷量】等。

技術領域知識方面,主搜索,聯盟廣告搜索引擎有時是配合使用的,比如商家未入駐廣告前給商家展示貨品信息就需要查主搜索,而入駐後投放下行時則需要用廣告引擎。不同引擎的調用方法,結果都不一樣。

如下圖所示,如果我們每個業務應用都各自實現,那麼各應用開發同學就要消化大量搜索客戶端相關的業務、技術領域知識。成本是很高的。

image.png

面對這種情況,如果我們將這類複雜的依賴,由專人owner進行統一包裝標準化【專人幹專事】,會大大提升組織協同效率。如下圖所示。

image.png

我們通過對主搜索,聯盟引擎的統一封裝。對檢索條件,返回結果的標準化封裝。大大降低了同學們的接入成本,以往要熟悉一個引擎的接入大概要2天,用標準化封裝後的wrapper,在專人,規範文檔的指導下僅0.5天就可以,大大提升效率。

2 重量級依賴代理服務化

第五節中有講到,應用依賴的jar包過多會導致應用啓動很慢,因此如果一個依賴引入jar包超過30個以上時,務必要警惕,這種依賴引入幾個,就會逐步導致你工作效率大大下降。比如IC,TP,優惠中心的二方包就是典型的例子。

目前我們針對這類依賴,是直接封裝一個標準代理服務,避免應用被這種巨無霸二方包拖慢。

image.png

經過以上綜合治理手段,取得了很好的效果。目前聯盟很少再需要大家去解決衝突問題。

image.png

原文鏈接

本文爲阿里雲原創內容,未經允許不得轉載。 

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