Java依賴沖突高效解決之道
一、概述
由于阿里媽媽聯(lián)盟團(tuán)隊(duì)負(fù)責(zé)業(yè)務(wù)的特殊性,系統(tǒng)有龐大的對(duì)外依賴,依賴集團(tuán)六七十個(gè)團(tuán)隊(duì)服務(wù)及N多工具組件,通過(guò)此文和大家分享一下我們積累的一些復(fù)雜依賴有效治理的經(jīng)驗(yàn),除了簡(jiǎn)單技術(shù)技巧的總結(jié)外,也會(huì)探討一些關(guān)于這方面架構(gòu)的思考,希望此文能系統(tǒng)徹底的解決java依賴沖突對(duì)大家的困擾。
二、依賴沖突產(chǎn)生的本質(zhì)原因
要解決依賴沖突,首先要理解一下java依賴沖突產(chǎn)生的本質(zhì)原因。
圖1
以上圖為例,目前阿里大部分Java工程都是maven工程,此類工程從開(kāi)發(fā)到上線要經(jīng)歷以下兩個(gè)重要步驟:
1.編譯打包
平時(shí)我們編寫(xiě)的應(yīng)用代碼,用maven編譯應(yīng)用代碼時(shí),maven只依賴第一級(jí)jar包(A.jar,B.jar,*.jar)既完成應(yīng)用代碼的編譯,至于傳遞依賴的jar包(Y.jar,Z.jar)maven首先會(huì)對(duì)同名不同version的jar包進(jìn)行依賴仲裁,然后依據(jù)仲裁結(jié)果下載對(duì)應(yīng)的jar放到指定目錄下(例如上圖中Y.jar最終只會(huì)仲裁1.0或2.0一個(gè)版本,此處假定仲裁到2.0版本,Z.jar即便內(nèi)容與Y.jar一致,但名稱不一樣所以不屬于maven仲裁范疇)。
有一點(diǎn)需注意不同maven版本可能會(huì)有差異,這會(huì)導(dǎo)致有時(shí)本地環(huán)境和日常、預(yù)發(fā)打包不一致造成應(yīng)用邏輯表現(xiàn)不一致的情況(說(shuō)明一下這種情況還有其他一些原因會(huì)導(dǎo)致,不是說(shuō)一定是maven版本不一致仲裁結(jié)果不一致導(dǎo)致的)。
2.發(fā)布上線
先明確一個(gè)概念,在JVM中,一個(gè)類型實(shí)例是通過(guò)它的全類名和加載它的類加載器(ClassLoader)實(shí)例來(lái)唯一確定的。所以所謂的“類隔離”,實(shí)際就是通過(guò)不同的類加載器實(shí)例去加載需要隔離的類來(lái)實(shí)現(xiàn)的,這樣即便兩個(gè)全類名完全相同但內(nèi)容不同的類,只要他們的類加載器實(shí)例不同,就能在一個(gè)容器進(jìn)程中共存,并且各自運(yùn)行互不干擾。
發(fā)布啟動(dòng)容器時(shí),不管是tomcat、taobao-tomcat還是PandoraBoot,還是其他容器, 首先都是用特定的類加載器實(shí)例先加載容器本身依賴的jar包,容器一般都會(huì)有多個(gè)類加載器實(shí)例,容器自身所依賴的jar包一般由專門(mén)的類加載器實(shí)例加載實(shí)現(xiàn)與應(yīng)用包的絕對(duì)隔離,像Pandroa還有專門(mén)的類加載器實(shí)例加載淘系中間件避免中間件與應(yīng)用類沖突,如下圖所示:
容器內(nèi)部依賴jar加載完成后,才輪到必然的一步:由某個(gè)應(yīng)用ClassLoader實(shí)例(一般與容器類加載器實(shí)例不是一個(gè))來(lái)加載編譯打包階段打出來(lái)的應(yīng)用jar包及應(yīng)用.class程序,這樣容器才能運(yùn)行業(yè)務(wù),同時(shí)確保應(yīng)用不會(huì)干擾容器的運(yùn)行。
例如圖1中,最終打出的應(yīng)用包中Y.jar-2.0,Z.jar都有com.taobao.Cc.class類,但一個(gè)應(yīng)用ClassLoader實(shí)例僅能加載V3或V2中一個(gè)版本的com.taobao.Cc.class類。
那到底會(huì)加載哪個(gè)版本的com.taobao.Cc.class類呢?答案是不一定,這個(gè)取決于容器應(yīng)用類加載實(shí)現(xiàn)策略, 從以往遇到的情況看,tomcat,taobao-tomcat、Pandora的做法都是直接裝載應(yīng)用lib包下所有.jar包文件列表(上例是A.jar,B.jar,*.jar,Y.jar,Z.jar。除tomcat外都沒(méi)看源碼核實(shí)過(guò),有錯(cuò)歡迎糾正)。但Java 在裝載一個(gè)目錄下所有jar包時(shí), 它加載的順序完全取決于操作系統(tǒng)!而Linux的順序完全取決于INode的順序,INode的順序不完全能一致,所以筆者之前就遇到類似的問(wèn)題,上線20臺(tái)機(jī)器,用同一個(gè)鏡像,有2臺(tái)就是起不來(lái)的情況。遇到這種情況目前就只能乖乖按以下章節(jié)中的手段去解決了。理論上最正確的做法應(yīng)該是容器裝載應(yīng)用 jar包時(shí),按指定順序加載。
基于以上分析,我們可以得出結(jié)論,基本所有的類沖突產(chǎn)生的本質(zhì)原因:要么是因?yàn)閙aven依賴仲裁jar包不滿足運(yùn)行時(shí)需要,要么是容器類加載過(guò)程中加載的類不滿足運(yùn)行時(shí)需要導(dǎo)致的。
關(guān)于容器類加載隔離策略,網(wǎng)上ATA上有很多資料介紹,本文重點(diǎn)向大家講解遇到?jīng)_突的各種解決之道,解決沖突大家只需要知道以上重點(diǎn)原理就夠了。
理解了依賴沖突產(chǎn)生的本質(zhì)原因,那么發(fā)生依賴沖突如何高效定位具體是哪些jar包引起的沖突呢?請(qǐng)繼續(xù)看下一章節(jié)。
三、依賴沖突問(wèn)題高效定位技巧
發(fā)生依賴沖突主要表現(xiàn)為系統(tǒng)啟動(dòng)或運(yùn)行中會(huì)發(fā)生異常,99%表現(xiàn)為三種NoClassDefFoundError、ClassNotFoundException、NoSuchMethodError。下面逐一講解一下定位技巧。
1.NoClassDefFoundError、ClassNotFoundException排查定位步驟
STEP1、發(fā)生NoClassDefFoundError首先要看完整異常棧,確認(rèn)是否是靜態(tài)代碼塊發(fā)生異常,靜態(tài)代碼塊發(fā)生異常堆棧與jar包沖突有很明顯的區(qū)別,出現(xiàn)"Could not initialize"、"Caused by: ..."關(guān)鍵字一般是靜態(tài)代碼塊發(fā)生異常導(dǎo)致類加載失敗:
- java.lang.NoClassDefFoundError: Could not initialize class testing.User
- at testing.Test.main(Test.java:23)
- Caused by: java.lang.RuntimeException: UserId Not found
- at testing.User.getUserId(Test.java:41)
- at testing.User.<clinit>(Test.java:35)
- ... 1 more
因?yàn)殪o態(tài)代碼塊發(fā)生異常導(dǎo)致NoClassDefFoundError,修改靜態(tài)代碼塊避免拋出異常即可。如果不是靜態(tài)代碼塊發(fā)生異常導(dǎo)致的問(wèn)題,繼續(xù)下一步。
STEP2、如果不是靜態(tài)代碼塊發(fā)生異常導(dǎo)致加載失敗,異常message關(guān)鍵字中會(huì)明確顯示缺失的類名稱,例如:
- java.lang.NoClassDefFoundError: org/apache/commons/lang/CharUtils
- at testing.Test.main(Test.java:19)
STEP3、在IDEA中(快捷鍵Ctrl+N)查找異常棧中提示缺失的類在哪些版本的jar包中有,如上例中的org.apache.commons.lang.CharUtils
STEP4、查看應(yīng)用部署機(jī)器上應(yīng)用lib包目錄下(一般是/home/admin/union-uc/target/${projectName}/lib或union-pub/target/${projectName}.war/WEB-INF/lib)是否存在上一步驟中查出對(duì)應(yīng)版本的jar包,以上情況一般是因?yàn)榇藭r(shí)應(yīng)用依賴的是低版本jar包,而jar包中又沒(méi)有沖突的類,絕大部分情況下NoClassDefFoundError、ClassNotFoundException定位確認(rèn)都是因?yàn)閙aven依賴仲裁最終采納的jar包版本與運(yùn)行時(shí)需要的不一致導(dǎo)致。
2.NoSuchMethodError排查到位步驟
STEP1、發(fā)生NoSuchMethodError,異常堆棧日志核心片段(異常棧中處于棧底的片段,見(jiàn)過(guò)很多同學(xué)發(fā)生異常亂翻一通,那樣毫無(wú)意義,要有目的的翻關(guān)鍵地方,不要亂翻)會(huì)明確顯示具體是哪個(gè)類,缺失了哪個(gè)方法,異常堆棧核心片段示例如下:
- 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)
首先需確認(rèn)JVM中當(dāng)前加載的缺失方法類,如上"org.springframework.beans.factory.support.DefaultListableBeanFactory"類到底來(lái)自哪個(gè)jar包,目前最高效的辦法:
外部環(huán)境容器下,或者某些容器版本過(guò)低不支持Arthas在線診斷的情況下,可以通過(guò)在JVM啟動(dòng)參數(shù)中增加" -XX:+TraceClassLoading",然后重新啟動(dòng)系統(tǒng),在系統(tǒng)工程日志中即可看到JVM加載類的信息。從中即可找到JVM是從哪個(gè)jar包中加載的。
STEP2、在IDEA中(快捷鍵Ctrl+N)查找異常棧中提示缺失的類在哪些版本的jar包中有,如下圖所示:
然后依次查看各版本jar包中沖突類的源碼,工程中部分jar打包時(shí)附帶了源碼包可直接看到源碼,不帶源碼的需要用IDEA插件(推薦jad)反編譯一下。然后依次搜尋各個(gè)jar包中的沖突類,搜尋第一步是點(diǎn)擊上圖中某個(gè)版本類,在IDEA中查找類級(jí)次關(guān)系(快捷鍵Ctrl+H),如下圖所示:
然后在沖突類及所有沖突類的父類源碼中找到NoSuchMethodError異常信息中描述缺失的方法,以上例子中就是"getDependencyComparator()Ljava/util/Comparator"。
上例中通過(guò)搜尋可以發(fā)現(xiàn)spring-beans-3.2.1.RELEASE.jar,spring-2.5.6.SEC03.jar兩個(gè)版本DefaultListableBeanFactory類及父類中沒(méi)有"getDependencyComparator()Ljava/util/Comparator"方法,spring-beans-4.2.4.RELEASE.jar,spring-beans-4.3.5.RELEASE.jar兩個(gè)版本DefaultListableBeanFactory類中有缺失的"getDependencyComparator()Ljava/util/Comparator"方法。
STEP3、查看應(yīng)用部署機(jī)器上應(yīng)用lib包目錄下(一般是/home/admin/union-uc/target/${projectName}/lib或union-pub/target/${projectName}.war/WEB-INF/lib)下,找到相關(guān)jar包的版本,如上例中:
致此定位問(wèn)題根本原因是應(yīng)用啟動(dòng)時(shí)加載"org.springframework.beans.factory.support.DefaultListableBeanFactory"類未加載到運(yùn)行時(shí)預(yù)期所需的spring-beans-4.3.5.RELEASE.jar版本,而是加載了spring-2.5.6.SEC03.jar導(dǎo)致。
按照以上流程步驟,基本99%的依賴沖突都可以定位到根本原因。定位到原因后如何解決沖突呢?事實(shí)上有些時(shí)候解決沖突遠(yuǎn)沒(méi)有內(nèi)網(wǎng)上很多帖子描述的"mvn dependency:tree"一下,排排jar那么簡(jiǎn)單。具體細(xì)節(jié)請(qǐng)繼續(xù)看下一章節(jié)。
四、通過(guò)maven調(diào)整依賴jar解決依賴沖突
1.升降級(jí)jar包解決依賴沖突
上一章節(jié)中的第一個(gè)例子中,最簡(jiǎn)單的情況,如果發(fā)生沖突的jar包高版本是完全兼容低版本功能的情況下,只需在pom中簡(jiǎn)單升級(jí)jar包版本即可。
但如果沖突 jar包高版本不兼容低版本,且應(yīng)用依賴不是很復(fù)雜的情況下,可以分析升級(jí)沖突jar包后會(huì)對(duì)哪些業(yè)務(wù)有影響,具體做法推薦通過(guò)IDEA Maven Helper 插件查找沖突jar包有哪些業(yè)務(wù)依賴(此處不推薦"mvn dependency:tree",目前本人見(jiàn)過(guò)的大部分Maven工程都有多個(gè)Module,比如*-dal,*-Service,*-Controller,這類工程結(jié)構(gòu)如果module未單獨(dú)打包上傳Maven倉(cāng)庫(kù),"mvn dependency:tree"是不能完整分析依賴關(guān)系的),記錄下來(lái)。如下圖所示:
然后升級(jí)沖突包,通過(guò)回歸測(cè)試受到影響的二方庫(kù)對(duì)應(yīng)的業(yè)務(wù)點(diǎn)。
如果應(yīng)用依賴非常復(fù)雜(例如沖突包有幾十個(gè)二方庫(kù)依賴,或者依賴沖突包的二方庫(kù)是個(gè)基礎(chǔ)包,業(yè)務(wù)系統(tǒng)中無(wú)法清晰枚舉出使用受影響二方庫(kù)的業(yè)務(wù)點(diǎn)),這種情況下,如果要通過(guò)升級(jí)jar包解決依賴沖突,必須完整回歸整個(gè)應(yīng)用功能。筆者有幾次因?yàn)榛貧w不全面引發(fā)故障的慘痛經(jīng)歷,希望大家不要重蹈覆轍。通過(guò)這幾次事例,筆者深刻理解到我們這個(gè)時(shí)代最偉大的計(jì)算機(jī)科學(xué)家Dijkstra大神“簡(jiǎn)單是可靠的先決條件”這句至理名言,深深的體會(huì)到如果一個(gè)系統(tǒng)復(fù)雜到你完全無(wú)法理清楚他錯(cuò)綜復(fù)雜的依賴關(guān)系的時(shí)候,那說(shuō)明你該重構(gòu)你的系統(tǒng)了,否則系統(tǒng)維護(hù)將會(huì)逐步變成噩夢(mèng)。
當(dāng)然不是所有情況都可以通過(guò)升降級(jí)jar解決沖突,舉個(gè)例子:
如上圖假設(shè)應(yīng)用系統(tǒng)同時(shí)依賴A.jar,B.jar,而A.jar,B.jar都依賴protobuf-java,系統(tǒng)運(yùn)行時(shí)都會(huì)分別用到A.jar,B.jar中protobuf部分的功能,而且A.jar,B.jar依賴的protobuf版本無(wú)法通過(guò)升高降低版本調(diào)整到一致。由于protobuf-java3.0版本序列化協(xié)議,類內(nèi)容各方面都不兼容protobuf-java2.0版本。這種情況無(wú)論如何調(diào)整依賴都無(wú)法解決沖突的問(wèn)題,要解決這種沖突,請(qǐng)繼續(xù)往下看,第五第六章內(nèi)容。
2.排除jar包解決依賴沖突
上一章節(jié)中第二個(gè)例子,主要原因是容器啟動(dòng)時(shí)加載到的類不是預(yù)期spring-beans-4.3.5.RELEASE.jar中的類,而是spring-2.5.6.SEC03.jar包中的類,如果spring-2.5.6.SEC03.jar排除對(duì)業(yè)務(wù)無(wú)影響,可以通過(guò)排除spring-2.5.6.SEC03.jar來(lái)解決沖突。與上一節(jié)例子類似,可以通過(guò)IDEA Maven Helper 插件確定spring-2.5.6.SEC03.jar是由哪個(gè)jar間接依賴進(jìn)來(lái)的,判斷業(yè)務(wù)的影響范圍,此處不在贅述。與上一節(jié)一樣,類似的情況不一定都可以用排除jar解決。
五、通過(guò)pandora自定義插件解決依賴沖突
第四章中有講到,如果一個(gè)應(yīng)用中要同時(shí)運(yùn)行兩個(gè)不兼容版本的jar包,是無(wú)法通過(guò)Maven調(diào)整依賴關(guān)系解決的。第二章講解依賴沖突原理時(shí)有提到,Pandora通過(guò)類隔離機(jī)制實(shí)現(xiàn)了集團(tuán)各個(gè)中間件之間的隔離,Pandroa同時(shí)也支持業(yè)務(wù)方按規(guī)范創(chuàng)建一個(gè)可以運(yùn)行在Pandora容器中的插件,容器幫業(yè)務(wù)方實(shí)現(xiàn)加載隔離。
聯(lián)盟一淘團(tuán)隊(duì)就將類似IC、卡券這種核武器級(jí)存在的二方包根據(jù)自己業(yè)務(wù)的需要進(jìn)行裁剪包裝后,制作成Pandora插件來(lái)避免依賴沖突,取得了很好的效果。
用Pandora插件確實(shí)能在不對(duì)應(yīng)用做很大調(diào)整,不影響性能的情況下完美解決依賴沖突問(wèn)題。
但也有一些問(wèn)題就不太適合用局部方法解決了,比如:
當(dāng)維護(hù)的應(yīng)用依賴過(guò)于復(fù)雜,每個(gè)應(yīng)用依賴外部三四十個(gè)二方庫(kù)時(shí)。這種重量級(jí)應(yīng)用就會(huì)嚴(yán)重影響生產(chǎn)效率。
如上圖所示,早期本人負(fù)責(zé)聯(lián)盟用戶平臺(tái)時(shí),就遇到兩個(gè)巨無(wú)霸應(yīng)用,adv(6w+代碼)、pub(12w+代碼)。
一方面因?yàn)橐蕾嚩?,基本每周都?huì)遇到集團(tuán)各種升級(jí),安全問(wèn)題,各種小修小補(bǔ),不斷的上線。一方面業(yè)務(wù)發(fā)布需求也較多。
導(dǎo)致需要頻繁發(fā)布,比如有一年個(gè)人就發(fā)布了566次。此時(shí)龐大的依賴導(dǎo)致部署效率,影響評(píng)估回歸都會(huì)很難,此時(shí)就不應(yīng)該從局部解決沖突這種視角去看,應(yīng)該考慮優(yōu)化應(yīng)用架構(gòu),進(jìn)行依賴治理,盡量避免沖突。
六、通過(guò)依賴架構(gòu)治理解決依賴沖突
1.復(fù)雜依賴標(biāo)準(zhǔn)化、簡(jiǎn)化治理
首先,依賴本身就是一種復(fù)雜的業(yè)務(wù)。大部分依賴背后都有較深的業(yè)務(wù)領(lǐng)域知識(shí) 或者 技術(shù)領(lǐng)域知識(shí)。
比如我們查詢搜索。
業(yè)務(wù)領(lǐng)域知識(shí)方面,光銷(xiāo)量就有交易成交筆數(shù),成交件數(shù),搜索銷(xiāo)量【有些訂單不計(jì)入搜索銷(xiāo)量】等。
技術(shù)領(lǐng)域知識(shí)方面,主搜索,聯(lián)盟廣告搜索引擎有時(shí)是配合使用的,比如商家未入駐廣告前給商家展示貨品信息就需要查主搜索,而入駐后投放下行時(shí)則需要用廣告引擎。不同引擎的調(diào)用方法,結(jié)果都不一樣。
如下圖所示,如果我們每個(gè)業(yè)務(wù)應(yīng)用都各自實(shí)現(xiàn),那么各應(yīng)用開(kāi)發(fā)同學(xué)就要消化大量搜索客戶端相關(guān)的業(yè)務(wù)、技術(shù)領(lǐng)域知識(shí)。成本是很高的。
面對(duì)這種情況,如果我們將這類復(fù)雜的依賴,由專人owner進(jìn)行統(tǒng)一包裝標(biāo)準(zhǔn)化【專人干專事】,會(huì)大大提升組織協(xié)同效率。如下圖所示。
我們通過(guò)對(duì)主搜索,聯(lián)盟引擎的統(tǒng)一封裝。對(duì)檢索條件,返回結(jié)果的標(biāo)準(zhǔn)化封裝。大大降低了同學(xué)們的接入成本,以往要熟悉一個(gè)引擎的接入大概要2天,用標(biāo)準(zhǔn)化封裝后的wrapper,在專人,規(guī)范文檔的指導(dǎo)下僅0.5天就可以,大大提升效率。
2.重量級(jí)依賴代理服務(wù)化
第五節(jié)中有講到,應(yīng)用依賴的jar包過(guò)多會(huì)導(dǎo)致應(yīng)用啟動(dòng)很慢,因此如果一個(gè)依賴引入jar包超過(guò)30個(gè)以上時(shí),務(wù)必要警惕,這種依賴引入幾個(gè),就會(huì)逐步導(dǎo)致你工作效率大大下降。比如IC,TP,優(yōu)惠中心的二方包就是典型的例子。
目前我們針對(duì)這類依賴,是直接封裝一個(gè)標(biāo)準(zhǔn)代理服務(wù),避免應(yīng)用被這種巨無(wú)霸二方包拖慢。
經(jīng)過(guò)以上綜合治理手段,取得了很好的效果。目前聯(lián)盟很少再需要大家去解決沖突問(wèn)題。
參考鏈接
INode講解:http://www.cnblogs.com/itech/archive/2012/05/15/2502284.html