《阿里云:Java應用提速(速度與激情)(2022)(55頁).pdf》由會員分享,可在線閱讀,更多相關《阿里云:Java應用提速(速度與激情)(2022)(55頁).pdf(55頁珍藏版)》請在三個皮匠報告上搜索。
1、封面頁(此頁面將由下圖全覆蓋,此為編輯稿中的示意,將在終稿 PDF 版中做更新)(待分享)卷首語 速度與效率與激情 什么是速度?速度就是快,快有很多種。有小李飛刀的快,也有閃電俠的快,當然還有周星星的快:(船家)我是出了名夠快。(周星星)“這船好像在下沉?”(船家)“是呀!沉得快嘛”。并不是任何事情越快越好,而是那些有價值有意義的事才越快越好。對于這些越快越好的事來說,快的表現是速度,而實質上是提效。今天我們要講的 Java 應用的研發效率,即如何加快我們的 Java 研發速度,提高我們的研發效率。提效的方式也有很多種,但可以分成二大類。我們使用一些工具與平臺進行應用研發與交付。這些工具與平臺
2、的用戶很多,而且一般對于大部分應用來說,效率不錯,否則也不會有這些工具與平臺的存在了。但對于一小部分應用來說,效率低的要命。所以當這小部分應用的用戶找工具與平臺負責人時,負責人建議提效的方案是:你看看其他應用都這么快,說明我們平臺沒問題??赡苁悄銈兊膽眉軜嫷膯栴},也可能是你們的應用中祖傳代碼太多了,要自己好好重構下。這是大家最常見的一類提效方式。而今天我們要講的是第二類,是從工具與平臺方面進行升級。即通過基礎研發設施與工具的微創新改進,實現研發提效,而用戶要做的可能就是換個工具的版本號。有了速度后一定會有激情嗎?我們先來看看電影中一個小插曲。有一次故事講到范老大來到了古巴,與當地的大哥賽車。
3、相比與大哥的豪車,范老大的車就是一輛拖拉機。但他用雙手好象對拖拉機的發動機施了一個“魔法”,然后在最后一公里時,發動機著火了,但車的速度卻達到火箭一樣的速度,沖向了終點。這里沒有哈利波特的人設,這世界沒有魔法。范老大也沒有。但這世界上幾乎沒有人能比范老大更懂發動機。所以也幾乎沒有人能比他更能改造發動機。而發動機就是一輛車的靈魂。買了一輛再好的車,帶來的只是速度。而自己不斷研究與改造發動機,讓車子越來越快,在帶來不斷突破的“速度”的同時還帶來了“激情”。因為這是一個不斷用自己雙手創造奇跡的過程。所以我們今天要講的不是買一輛好車,而是講如何改造“發動機”。在阿里集團,有上萬多個應用,大部分應用都是
4、 Java 應用,95%應用的構建編譯時間是 5 分鐘以上,鏡像構建時間是 2 分鐘以上,啟動時間是 8 分鐘以上,這樣意味著研發同學的一次改動,大部分需要等待 15 分鐘左右,才能進行業務驗證。而且隨著業務迭代和時間的推移,應用的整體編譯構建、啟動速度也越來越慢,發布、擴容、混部拉起等等一系列動作都被拖慢,極大的影響了研發和運維整體效能,應用提速刻不容緩。我們將闡述通過基礎設施與工具的改進,實現從構建到啟動全方面大幅提速的實踐和理論,相信能幫助大家。目錄 一、maven 構建提速.6 二、本地 IDEA 環境提速.20 三、docker 構建提速.25 四、JDK 提速.30 五、Class
5、Loader 提速.37 六、阿里中間件提速.42 七、其他提速.50 卷尾語:持續地.激情.56 作者:阿里巴巴 CTO 技術 聯合作者:道延 微波 沈陵 梁希 大熊 斷嶺 北緯 未宇 岱澤 浮圖 一、maven 構建提速 6 一、maven 構建提速 1.現狀 maven 其實并不是拖拉機。相對于 ant 時代來說,maven 是一輛大奔。但隨著業務越來越復雜,我們為業務提供服務的軟件也越來越復雜。雖然我們在提倡要降低軟件復雜度,但對于復雜的業務來說,降低了復雜度的軟件還是復雜的。在這些年,隨著業務競爭越來越激勵,業務越來越復雜,軟件也越來越復雜。而maven 卻還是幾年的版本。在 201
6、2 年推出 maven3.0.0 以來,直到現在的 2022年,正好十年,但 maven 最新版本還是 3 系列 3.8.6。所以在十年后的今天,站在復雜軟件面前,maven 變成了一輛拖拉機。編碼也是一種藝術,對于我們一線研發同學來說,每個人都期望變成一名藝術家,而不是一個碼農。但如一次構建大于 3 分鐘,會將我們從一名高雅的藝術家淪為一名焦慮的碼農,因為項目的 deadline 是放的那么地顯眼。我們可能有過這樣體驗,編碼幾分鐘,代碼提交后,CI/CD 中的構建卻要 10 多分鐘。特別是在項目聯調階段,代碼修改會越頻率,但一天解決不了幾個 BUG,因為時間都花在等待構建階段上了。我們曾經錯
7、過了多少個夏天的晚霞與秋天的朝霞,都是因為等待構建與編譯而工作到凌晨。2.解決方案 在這十年,雖然 maven 還是停留在主版本號是 3,但當今業界也不斷出現了優秀的構建工具,如 gradle,bazel。但因各工具的生態不同,同時工具間遷移有成本與風險,所以目前在 Java 服務端應用仍是以 maven 構建為主。所以我們在 apache-maven 的基礎上,參照 gradle,bazel 等其它工具的思路,進行了優化,并以“amaven”命名。一、maven 構建提速 7 因為 amaven 完全兼容 apache-maven,所支持的命令與參數都兼容,所以對我們研發同學來說,只要修改一
8、個 maven 的版本號。3.效果 從目前試驗來看,對于 mvn build 耗時在 3 分鐘以上的應用有效果。對于典型應用從 2325 秒降到 188 秒,提升了 10 倍多。我們再來看持續了一個時間段后的總體效果,典型應用使用 amaven 后,構建耗時p95 的時間有較明顯下降,對比使用前后二個月的構建耗時降了 50%左右。4.原理 如果說發動機是一輛車的靈魂,那依賴管理就是 maven 的靈魂。因為 maven 就是為了系統化的管理依賴而產生的工具。使用過 maven 的同學都清楚,我們將依賴寫在pom.xml中,而這依賴又定義了自己的依賴在自己的pom.xml。通過 pom 文件的層
9、次化來管理依賴的確讓我們方便很多。我們平常說的 maven 其實指二樣東西,一個是 maven 工具,一個是 maven 倉庫。maven 工具主要是 mvn 命令,我們在執行 mvn compile 等命令時,maven 會先不斷解析 pom 中的依賴,如某個依賴本地沒有則會從 maven 倉庫下載到本地,再遞歸解析與下載“依賴的依賴”,最后生成一個 dependencyGraph,然后再將graph 中的依賴的 jar 列表成 classPath 中的參數,進行 Javac,從而完成一次編譯。如再加上一些插件執行,則一次典型的 maven 構建過程,會是這樣:一、maven 構建提速 8
10、從上圖可以看出,maven 構建主要有二個階段,而第一階段是第二階段的基礎,基本上大部分的插件都會使用第一階段產生的依賴樹:解析應用的 pom 及依賴的 pom,生成依賴樹;在解析過程中,一般還會從maven 倉庫下載新增的依賴或更新了的 snapshot 包。執行各 maven 插件。我們也通過分析實際的構建日志,發現大于 3 分鐘的 maven 構建,瓶頸都在“生成依賴樹”階段。而“生成依賴樹”階段慢的根本原因是一個 module 配置的依賴太多太復雜,它表現為:依賴太多,則要從 maven 倉庫下載的可能性越大。依賴太復雜,則依賴樹解析過程中遞歸次數越多。既然說發動機是車子的靈魂,那要讓
11、車子跑的更快,最核心的就要不斷改造發動機的性能;既然說依賴管理是 maven 的靈魂,那要讓 maven 執行的快,最核心的就要不斷“改造”依賴分析的性能。在amaven中通過優化依賴分析算法,與提升下載依賴速度來提升依賴分析的性能。除此之外,性能優化的經典思想是緩存增量,與分布式并發,我們也遵循這個思想。既然生成依賴樹的代價大,那我們就將依賴樹緩存起來(直接緩存與復用肯定比重新解析一次快),因為在實際開發過程中,修改自己的 Java 代碼的概率遠大于修改 一、maven 構建提速 9 應用的 pom。同時,如一個應用,特別是大庫應用,當它的 module 可能有幾十個,或幾百個時,則要使用分
12、布式并發構建的方案,將互不依賴的的 module 啟多線程,甚至分配到不同的編譯機上去同時構建。因為 maven 自己也是 Java 程序,所以為了盡可能降低字節碼在運行時轉成機器碼的開銷,我們也考慮了 daemon 方案。所以總的加速思路如下:而當以上思路在不斷落地過程中,amaven 也不斷地 C/S 化了,即 amaven 不再是一個 client,而有了 server 端,同時將部分復雜的計算從 client 端移到了 server 端。而當 client 越做越薄,server 端的功能越來越強大時,server 的計算所需要的資源也會越來越多,將這些資源用彈性伸縮來解決,慢慢地 a
13、maven 云化了。從單個 client 到 C/S 化再到云化,這也是一個工具不斷進化的趨勢所在。1)依賴樹 a)依賴樹緩存 既然依賴樹生成慢,那我們就將這依賴樹緩存起來。緩存后,這依賴樹可以不用重復生成,而且可以不同人,不同的機器的編譯進行共享。使用依賴樹緩存后,一次典型的 mvn 構建的過程如下:一、maven 構建提速 10 從上圖中可以看到 amaven-server,它主要負責依賴樹緩存的讀寫性能,保障存儲可靠性,及保證緩存的正確性等。b)依賴樹生成算法優化 雖在日常研發過程中,修改 pom 文件的概率較修改應用 Java 低,但還是有一定概率;同時當 pom 中依賴了較多 SNA
14、PSHOT 且 SNAPSHOT 有更新時,依賴樹緩存會失效掉。所以還是會有不少的依賴樹重新生成的場景。所以還是有必要來優化依賴樹生成算法。在 maven2 及 maven3 版本中,包括最新的 maven3.8.5 中,maven 是以深度優先遍歷(DF)來生成依賴樹的。(在社區版本中,目前 master 上已經支持 BF,但還未發 release 版本:https:/ debug 與打日志發現有很多相同的 gav 或相同的 ga 會被重復分析很多次,甚至數萬次。一、maven 構建提速 11 樹的經典遍歷算法主要有二種:深度優先算法(DF)及廣度優先算法(BF),BF 與DF 的效率其實差
15、不多的。在有些場景,是 DF 更快,在有些場景,是 BF 更快。DF一般用 stack 數據結構,BF 一般用 queue 數據結構。樹的二種遍歷算法本沒有根本的孰好孰壞之分,但當結合 maven 的版本仲裁機制考慮會發現有些差異。我們再來看看 maven 的仲裁機制。無論是maven2還是maven3,最主要的仲裁原則就是depth。相同ga或相同gav,誰更 deeper,誰就 skip。當然仲裁的因素還有 scope、profile 等??紤]根據 depth來仲裁的機制,按層遍歷會更優,因為可以比 DF 更容易結合按 depth 仲裁。如下圖,如按層來遍歷,則紅色的二個 D1,D2 就會
16、 skip 掉,不會重復解析。(注意,實際場景是 C 的 D1 還是會被解析,因為它更左)。按層遍歷也就是 BF。所以小結下,對于樹的遍歷算法本身來說,DF 與 BF 效率是差不多的。但對于maven3.5.0 的依賴樹生成邏輯來說,是因為在 BF 中可以先加上按 depth 仲裁邏輯,才會比 DF 快。即算法優化的思路是:“提前修枝”。之前 maven3 的邏輯是先生成依賴樹再版本仲裁,而優化后是邊生成依賴樹邊仲裁。就好比一個樹苗,要邊生長邊修枝,而如果等它長成了參天大樹后則修枝要累死人。一、maven 構建提速 12 c)依賴下載優化 maven 在編譯過程中,會解析 pom,然后不斷下載
17、直接依賴與間接依賴到本地。一般本地目錄是.m2。對一線研發來說,本地的.m2 不太會去刪除,所以除非有大的重構,每次編譯只有少量的依賴會下載。但對于 CICD 平臺來說,因為編譯機一般不是獨占的,而是多應用間共享的,所以為了應用間不相互影響,每次編譯后可能會刪除掉.m2 目錄。這樣,在 CICD 平臺要考慮.m2 的隔離,及當.m2 清理后要下載大量依賴包的場景。而依賴包的下載,是需要經過網絡,所以當一次編譯,如要下載上千個依賴,那構建耗時大部分是在下載包,即瓶頸是下載。增大下載并發數 依賴包是從 maven 倉庫下載。maven3.5.0 在編譯時默認是啟了 5 個線程下載。我們可以通過 a
18、ether.connector.basic.threads 來設置更多的線程如 20 個來下載,但這要求 maven 倉庫要能撐得住翻倍的并發流量。所以我們對 maven 倉庫進行了架構升級,根據包不同的文件大小區間使用了本地硬盤緩存,redis 緩存等包文件多級存儲來加快包的下載。一、maven 構建提速 13 下表是對熱點應用 A 用不同的下載線程數來下載 5000 多個依賴得到的下載耗時結果比較:在 amaven 中我們加了對下載耗時的統計報告,包括下載多少個依賴,下載線程是多少,下載耗時是多少,方便大家進行性能分析。如下圖:同時為了減少網絡開銷,我們還采用了在編譯機本地建立了 mirr
19、or 機制。本地 mirror 在 CI/CD 平臺,構建時,為避免重復下載同一個依賴文件,架構可能是這樣的:(架構 1.0:共享.m2)這架構會有依賴包的準確性問題。一、maven 構建提速 14 在一個 node 上,會編譯很多應用,而每個應用編譯時指定的 maven 倉庫可能不一樣。如果 volume 同一個.m2 目錄,當應用 A 下載從倉庫 a 下載了 maven-compiler-plugin:3.8.1 后,應用 B 它指定了從倉庫 b 下載依賴,但當它編譯時發現.m2 目錄已經有 maven-compiler-plugin:3.8.1 了,就不會下載了。當倉庫 a 與倉庫 b
20、中maven-compiler-plugin:3.8.1 的文件的 checksum 不同時,就會讓應用 B 在構建或運行時出現問題。為保證依賴包的準確性,需要將.m2 隔離.即每個 pod 都有一個獨立的.m2 來volume。(架構 2.0:按 pod 隔離.m2)雖然這樣會浪費一些磁盤,但準確性就能得到保障。但產生了同一個倉庫的同一個依賴會在同一個 node 上重復下載的問題,即使 2.0 圖示的二個 pod 是同一個應用的并發構建,但在同一個 node 上面下載 maven-compiler-plugin:3.8.1 時,要下載二次。所以我們繼續架構演進,來按 app 來隔離。一、ma
21、ven 構建提速 15 (架構 3.0:按 app 隔離.m2)但還是會存在同一個 node 上重復下載同一個倉庫的同一個依賴文件,因為 appA與 appB,它們用的是同一個倉庫。因為在一個 node 上,是不會知道在其中運行的mvn build 的,是同一個應用?或不同應用但相同倉庫?或不同應用不同倉庫?所以我們得繼續演進,不按應用而按倉庫來隔離.m2。一、maven 構建提速 16 (架構 4.0:按 repo 隔離.m2)這架構看上去感覺還可以,但解決不了一個應用依賴多個倉庫的問題。有些應用有些復雜,它會在 maven 構建的倉庫配置文件 settings.xml(或 pom 文件)中
22、指定下載多個倉庫。因為這應用的要下載的依賴的確來自多個倉庫。當指定多個倉庫時,下載一個依賴包,會依次從這多個倉庫查找并下載。雖然 maven 的 settings.xml 語法支持多個倉庫,但 localRepository 卻只能指定一個。所以要看下 docker 是否支持將多個目錄 volume 到同一個容器中的目錄,即上圖中的紅線,但初步看了 docker 官網文檔,并不支持。那是否可以用 overlay 的方式?即使可行,但 overlay 時誰在 lower,誰在 upper,這個得根據 settings.xml 中指定的多個倉庫的順序來才行。于是得在啟 pod 構建前要解析下這應用
23、的 settings.xml,稍嫌復雜。一、maven 構建提速 17 為解決按倉庫隔離.m2,且應用依賴多個倉庫時的問題,我們現在通過對 amaven的優化來解決。(架構 5.0:repo_mirror)當 amaven 執行 mvn build 時,當一個依賴包不在本地.m2 目錄,而要下載時,會先到 repo_mirror 中對應的倉庫中找,如找到,則從 repo_mirror 中對應的倉庫中將包直接復制到.m2,否則就只能到遠程倉庫下載,下載到.m2 后,會同時將包復制到 repo_mirror 中對應的倉庫中。通過repo_mirror可以實現同一個node上只會下載一次下載一次同一
24、個倉庫的同一個文件。當然如果有合適的共享存儲,多個 node 共享一個存儲服務,那就能解決多個 node只下載一次同一個倉庫的同一個文件了。d)SNAPSHOT 版本號緩存 其實在 amavenServer 的緩存中,除了依賴樹,還緩存了 SNAPSHOT 的版本號。我們的應用會依賴一些 snapshot 包,同時當我們在 mvn 構建時加上-U 就會去檢測這些 SNAPSHOT 的更新,而在 apache-maven 中檢測 SNAPSHOT 需要多次請求maven 倉庫,會有一些網絡開銷?,F在我們結合 maven 倉庫作了優化,從而讓多次請求 maven 倉庫,換成了一次cache 服務直
25、接拿到 SNAPSHOT 的最新版本。一、maven 構建提速 18 2)增量 增量是與緩存息息相關的,增量的實現就是用緩存。maven 的開放性是通過插件機制實現的,每個插件實現具體的功能,是一個函數。當輸入不變,則輸出不變,即復用輸出,而將每次每個函數執行后的輸出緩存起來。上面講的依賴樹緩存,也是 maven 本身(非插件)的一種增量方式。要實現增量的關鍵是定義好一個函數的輸入與輸出,即要保證定義好的輸入不變時,定義好的輸出肯定不變。每個插件自己是清楚輸入與輸出是什么的,所以插件的增量不是由 amaven 統一實現,而是 amaven 提供了一個機制。如一個插件按約定定義好了輸入與輸出,則
26、 amaven 在執行前會檢測輸入是否變化,如沒變化,則直接跳過插件的執行,而從緩存中取到輸出結果。但其實一個插件的輸入與輸出要定義清楚并沒那么簡單,我們拿 maven-compiler-plugin 來說。maven-compiler-plugin 簡單地說,它的輸入是 src/java 目錄,輸出是 classes 目錄。但有些場景即使 src/java 沒變,classes 也不能復用。如一個 project有二個 module:A 與 B。B 中有一個 Java 文件引用了 A 的一個 Java 文件的常量。A 修改了一個 Java 中的一個常量,A 會重新 compile。但 B 沒
27、修改 Java 文件,但 B也要重新 compile。一、maven 構建提速 19 除了常量外,還有一些 ABI 兼容性的問題也要考慮。module B 依賴 A,B 調用了 A中的一個方法 foo(String args),當 A 將方法簽名修改成 foo(String.args),B 不用修改對應的調用 foo 方法的類,而需要重新編譯,否則運行時會報找不到方法。一個 module 執行一個插件,是否能用增量,不只是考慮自己 module 的變化情況,還要考慮其它 module 及直接依賴或間接依賴的變化情況,這會讓增量的實現有一定的挑戰性。需要不斷的校正輸入。但增量的效果是明顯的,如依
28、賴樹緩存與算法的優化能讓 maven 構建從 10 分鐘降到 2 分鐘,那增量則可以將構建耗時從分鐘級降到秒級。而 gradle 與 bazel 能達到修改一個 Java 文件,幾秒內完成編譯,就是增量效果的體現。當然它們也有定義清晰與準確輸入的挑戰性。3)daemon 與分布式 daemon 是為了進一步達到 10 秒內構建的實現途徑。maven 也是 Java 程序,運行時要將字節碼轉成機器碼,而這轉化有時間開銷。雖這開銷只有幾秒時間,但對一個 mvn構建只要15秒的應用來說,所占比例也有10%多。為降低這時間開銷,可以用 JIT 直接將 maven 程序編譯成機器碼,同時 mvn 在構建
29、完成后,不退出,常駐進程,當有新構建任務來時,直接調用 mvn 進程。一般,一個 maven 應用編譯不會超過 10 分鐘,所以,看上去沒必要將構建任務拆成子任務,再調度到不同的上執行分布式構建。因為分布式調度有時間開銷,這開銷可能比直接在本機上編譯耗時更大,即得不償失。所以分布式構建的使用場景是大庫。為了簡化版本管理,將二進制依賴轉成源碼依賴,將依賴較密切的源碼放在一個代碼倉庫中,就是大庫。當一個大庫有成千上萬個 module 時,則非用分布式構建不可了。使用分布式構建,可以將大庫幾個小時的構建降到幾分鐘級別。二、本地 IDEA 環境提速 20 二、本地 IDEA 環境提速 1.從盲俠說起
30、曾經有有一位盲人叫座頭市,他雙目失明,但卻是一位頂尖的劍客,江湖上沒人能接得了他三招,他行俠于江湖,江湖上稱他為“盲俠”。在我們的一線研發同學中,也有不少盲俠。這些同學在本地進行寫代碼時,是盲寫。他們寫的代碼盡管全都顯示紅色警示,寫的單測盡管在本地沒跑過,但還是照寫不誤。而且慢慢的練就了,本地寫了代碼后,不用管語法的錯誤提示,不用管單測是否能跑,代碼提交上去后,能一切編譯通過,部署正常。但這“練就”其實只是大家自己的期望,每次代碼提交后,返工的次數還是挺多的。而且這些同學也不是自己故意裝逼要當個“盲俠”,而是逼于無奈。因為他們要研發的應用的代碼在本地 IDEA 環境導入后,依賴解析不全,導致眾
31、多紅叉。我們一般的開發流程是,接到一個需求,從主干拉一個分支,再將本地的代碼切到這新分支,再刷新 IDEA。但有些分支在刷新后,盡管等了 30 分鐘,盡管自己的 mac的 CPU 沙沙直響,熱的冒泡,但 IDEA 的工作區還是有很多紅線。這些紅線逼我們不少同學走上了“盲俠”之路。一個 maven 工程的 Java 應用,IDEA 的導入也是使用了 maven 的依賴分析。而我們據分析與實際觀測,一個需求的開發,即在一個分支上的開發,在本地使用 maven的次數絕對比在 CICD 平臺上使用的次數多。所以本地的 maven 的性能更需要提升,更需要改造。因為它能帶來更大的人效。二、本地 IDEA
32、 環境提速 21 我們在“maven 構建提速”這一小節中講了 amaven 在 CICD 平臺上的解決方案,及它的效果與原理。在這,我們再講講 amaven 如何用在本地,特別是用在本地的IDEA 工具中。2.解決方案 amaven 要結合在本地的 IDEA 中使用也很方便。a)下載 amaven 最新版本 b)在本地解壓,如目錄/Users/userName/soft/amaven-3.5.0 c)設置 Maven home path:為了充分利用 mac 的內存資源,建議設置大些的內存:-Xms1538m-Xmx2048m-Xmn768m-XX:SurvivorRatio=10 d)在應
33、用目錄下新建 amaven.config,并寫入:二、本地 IDEA 環境提速 22 aether.collector.impl=bf amaven.write.log.to.file=true#amaven.log.dir=amaven.log.dir 如不設置,默認是用戶目錄。建議將這 amaven.config 提交到分支上,這樣同一應用的其他研發同學就不用重復設置了。amaven.config 在用戶目錄中,則它對所有應用生效,如在應用目錄中,則優先使用應用目錄的,且只能此應用生效。e)重啟 idea 后,點 import project 最后我們看看效果,對熱點應用進行 import
34、 project 測試,用 maven 要 20 分鐘左右,而用 amaven3.5.0 在 3 分鐘左右,在命中緩存情況下最佳能到 1 分鐘內。簡單五步后,我們就不用再當“盲俠”了,在本地可以流暢地編碼與跑單元測試。除了在 IDEA 中使用 amaven 的依賴分析能力外,在本地通過命令行來運行 mvn compile 或 dependency:tree,也完全兼容 apache-maven 的。二、本地 IDEA 環境提速 23 3.原理 IDEA 是如何調用 maven 的依賴分析方法的?在 IDEA 的源碼文件:https:/ 中 979 行,調用了 dependencyResolve
35、r.resolve(resolution)方法:dependencyResolver 就是通過 maven home path 指定的 maven 目錄中的DefaultProjectDependenciesResolver.java。而 DefaultProjectDependenciesResolver.resolve()方法就是依賴分析的入口。IDEA 主要用了 maven 的依賴分析的能力,在“maven 構建提速”這一小節中,我們已經講了一些 amaven 加速的原理,其中依賴算法從 DF 換到 BF,依賴下載優化,整個依賴樹緩存,SNAPSHOT 緩存這些特性都是與依賴分析過程相關
36、,所以都能用在 IDEA 提速上,而依賴倉庫 mirror 等因為在我們自己的本地一般不會刪除.m2,所以不會有所體現。二、本地 IDEA 環境提速 24 amaven 可以在本地結合 IDEA 使用,也可以在 CICD 平臺中使用,只是它們調用maven 的方法的方式不同或入口不同而已。但對于 maven 協議來說“靈魂”的還是依賴管理與依賴分析。三、docker 構建提速 25 三、docker 構建提速 1.背景 自從阿里巴巴集團容器化后,把構建鏡像做為發布構建的一步后開發人員經常被鏡像構建速度困擾,每天要發布很多次的應用體感尤其不好。為了讓應用的鏡像構建盡量的少,我們幾年前已經按最佳實
37、踐推薦每個應用要把鏡像拆分成兩部分,一部分是基礎鏡像,包含低頻修改的部分。另一部分是應用鏡像,包含高頻修改的部分,比如應用的代碼構建產物。但是很多應用按我們提供的最佳實踐修改后,高頻修改部分的構建速度依然不盡如人意?,F在 CICD 平臺和集團很多鏡像構建場景用的還是 pouch 的前身。它只支持順序構建,對多階段并發構建的支持也不完整,我們設想的很多優化方法也因為它的技術過于老舊而無法實施。它中很多對低版本內核和富容器的支持也讓一些鏡像包含了一些現在運行時用不到的文件。為了跟上主流技術的發展,我們計劃把CICD平臺的構建工具升級到moby-buildkit,docker 的最新版本也計劃把構建
38、切換到 moby-buildkit 了,這個也是業界的趨勢。同時在 buildkit 基礎上我們作了一些增強。2.增強 1)新語法 SYNC 我們先用增量的思想,相對于 COPY 增加了一個新語法 SYNC。我們分析 Java 應用高頻構建部分的鏡像構建場景,高頻情況下只會執行 Dockerfile中的一個指令:COPY appName.tgz/home/appName/target/appName.tgz 三、docker 構建提速 26 發現大多數情況下 Java 應用每次構建雖然會生成一個新的 app.war 目錄,但是里面的大部分 jar 文件都是從 maven 等倉庫下載的,它們的創
39、建和修改時間雖然會變化但是內容的都是沒有變化的。對于一個 1G 大小的 war,每次發布變化的文件平均也就三十多個,大小加起來 2-3 M,但是由于這個 appName.war 目錄是全新生成的,這個 copy 指令每次都需要全新執行,如果全部拷貝,對于稍微大點的應用這一層就占有 1G 大小的空間,鏡像的 copy push pull 都需要處理很多重復的內容,消耗無謂的時間和空間。如果我們能做到定制 dockerfile 中的 copy 指令,拷貝時像 Linux 上面的 rsync 一樣只做增量 copy 的話,構建速度、上傳速度、增量下載速度、鏡像的占用的磁盤空間都能得到很好的優化。因為
40、 moby-buildkit 的代碼架構分層比較好,我們基于dockerfile 前端定制了內部的 SYNC 指令。我們掃描到 SYNC 語法時,會在前端生成原生的兩個指令,一個是從基線鏡像中 link拷貝原來那個目錄(COPY),另一個是把兩個目錄做比較(DIFF),把有變化的文件和刪除的文件在新的一層上面生效,這樣在基線沒有變化的情況下,就做到了高頻構建每次只拷貝上傳下載幾十個文件僅幾兆內容的這一層。而用戶要修改的,只是將原來的 COPY 語法修改成 SYNC 就行了。如將:COPY appName.tgz/home/admin/appName/target/appName.tgz 修改為
41、:SYNC appName.dir/home/admin/appName/target/appName.war 我們再來看看 SYNC 的效果。集團最核心的熱點應用 A 切換到 moby-buildkit 以及我們的 sync 指令后 90 分位鏡像構建速度已經從 140 秒左右降低到 80 秒左右。三、docker 構建提速 27 2)none-gzip 實現 為了讓moby-buildkit能在CICD平臺上面用起來,首先要把none-gzip支持起來。這個需求在docker社區也有很多討論:https:/ gzip 會導致 90%的時間都花在壓縮和解壓縮上面,構建和下載時間會加倍,發布環
42、境拉鏡像的時候主機上一些 CPU 也會被 gzip解壓打滿,影響同主機其它容器的運行。雖然 none-gzip 后,CPU 不會高,但會讓上傳下載等傳輸過程變慢,因為文件不壓縮變大了。但相對于 CPU 資源來說,內網情況下帶寬資源不是瓶頸。只需要在上傳鏡像層時按配置跳過 gzip 邏輯去掉,并把鏡像層的 MediaType 從:application/vnd.docker.image.rootfs.diff.tar.gzip 改成:application/vnd.docker.image.rootfs.diff.tar 三、docker 構建提速 28 就可以在內網環境下充分提速了。3)單層內
43、并發下載 在 CICD 過程中,即使是同一個應用的構建,也可能會被調度到不同的編譯機上。即使構建調度有一定的親和性。為了讓新構建機,或應用換構建機后能快速拉取到基礎鏡像,由于我們以前的最佳實踐是要求用戶把鏡像分成兩個(基礎鏡像與應用鏡像)。換編譯機后需要在新的編譯機上面把基礎鏡像拉下來,而基礎鏡像一般單層就有超過 1G 大小的,多層并發拉取對于單層特別大的鏡像已經沒有效果。所以我們在層內并發拉取的基礎上,還增加了同一層鏡像的并發拉取,讓拉鏡像的速度提升了 4 倍左右。默認每 100M 一個分片,用戶也可以通過參數來設置分片大小。當然實現這層內并發下載是有前提的,即鏡像的存儲需要支持分段下載。因
44、為我們公司是用了阿里云的 OSS 來存儲 docker 鏡像,它有很好的分段下載或多線程下載的性能。4)無中心 P2P 下載 現在都是用 containerd 中的 content store 來存儲鏡像原始數據,也就是說每個節點本身就存儲了一個鏡像的所有原始數據 manifest 和 layers。所以如果多個相鄰的節點,都需要拉鏡像的話,可以先看到中心目錄服務器上查看鄰居節點上面是否已經有這個鏡像了。如果有的話就可以直接從鄰居節點拉這個鏡像,而不需要走鏡像倉庫去取鏡像 layer,manifest 數據還必須從倉庫獲取是為了防止鏡像名對應的數據已經發生了變化了,只要取到 manifest
45、后其它的 layer 數據都可以從相鄰的節點獲取,每個節點可以只在每一層下載后的五分鐘內(時間可配置)提供共享服務,這樣大概率還能用到本地 page cache,而不用真正讀磁盤。三、docker 構建提速 29 中心 OSS 服務總共只能提供最多 20G 的帶寬,從歷史拉鏡像數據能看到每個節點的下載速度都很難超過 30M,但是我們現在每個節點都是 50G 網絡,節點相互之間共享鏡像層數據可以充分利用到節點本地的 50G 網絡帶寬,當然為了不影響其它服務,我們把鏡像共享的帶寬控制在 200M 以下。5)鏡像 ONBUILD 支持 社區的 moby-buidkit 已經支持了新的 schema2
46、 格式的鏡像的 ONBUILD 了,但是集團內部還有很多應用 FROM 的基礎鏡像是 schema1 格式的基礎鏡像,這些基礎鏡像中很多都很巧妙的用了一些 ONBUILD 指令來減少 FROM 它的 Dockerfile 中的公共構建指令。如果不能解析 schema1 格式的鏡像,這部分應用的構建雖然會成功,但是其實很多應該執行的指令并沒有執行,對于這個能力缺失,我們在內部補上的同時也把這些修改回饋給了社區(https:/ 提速 30 四、JDK 提速 1.AppCDS 1)現狀 CDS(Class Data Sharing)在 Oracle JDK1.5 被首次引入,在 Oracle JDK
47、8u40 中引入了 AppCDS,支持 JDK 以外的類,但是作為商業特性提供。隨后 Oracle 將 AppCDS貢獻給了社區,在 JDK10 中 CDS 逐漸完善,也支持了用戶自定義類加載器(又稱AppCDS v2)。目前 CDS 在阿里的落地情況:熱點應用 A 使用 CDS 減少了 10 秒啟動時間。云產品 SAE 和 FC 在使用 Dragonwell11 時開啟 CDS、AOT 等特性加速啟動。經過十年的發展,CDS 已經發展為一項成熟的技術。但是很容易令人不解的是 CDS不管在阿里的業務還是業界(即便是 AWS Lambda)都沒能被大規模使用。關鍵原因有兩個。a)AppCDS 在
48、實踐中效果不明顯 jsa 中存儲的 InstanceKlass 是對 class 文件解析的產物。對于 boot classloader(加載 jre/lib/rt.jar 下面的類的類加載器)和 system(app)類加載器(加載-classpath下面的類的類加載器),CDS 有內部機制可以跳過對 class 文件的讀取,僅僅通過類名在 jsa 文件中匹配對應的數據結構。Java 語言還提供用戶自定義類加載器(custom class loader)的機制,用戶通過Override 自己的 Classloader.loadClass()查找類,AppCDS 在為 customer cla
49、ss loade 時加載類是需要經過如下步驟:調用用戶定義的 Classloader.loadClass(),拿到 class byte stream 計算 class byte stream 的 checksum,與 jsa 中的同類名結構的 checksum 比較 四、JDK 提速 31 如果匹配成功則返回 jsa 中的 InstanceKlass,否則繼續使用 slow path 解析class 文件 b)工程實踐不友好 使用 AppCDS 需要如下步驟:針對當前版本在生產環境啟動應用,收集 profiling 信息 基于 profiling 信息生成 jsa(java sahred a
50、rchive)dump 將 jsa 文件和應用本身打包在一起,發布到生產環境 由于這種 trace-replay 模式的復雜性,在 SAE 和 FC 云產品的落地都是通過發布流程的定制以及開發復雜的命令行工具來解決的。2)解決方案 針對上述的問題 1,在熱點應用 A 上 CDS 配合 JarIndex 或者使用編譯器團隊開發的EagerAppCDS 特性(原理見 5.1.3.1)都能讓 CDS 發揮最佳效果。經驗證,在熱點應用A已經使用JarIndex做優化的前提下進一步使用EagerAppCDS依然可以獲得 15 秒左右的啟動加速效果。3)原理 面向對象語言將對象(數據)和方法(對象上的操作
51、)綁定到了一起,來提供更強的封裝性和多態。這些特性都依賴對象頭中的類型信息來實現,Java、Python 語言都是如此。Java 對象在內存中的 layout 如下:+-+|mark|+-+|Klass*|+-+|fields|+-+四、JDK 提速 32 mark 表示了對象的狀態,包括是否被加鎖、GC 年齡等等。而 Klass*指向了描述對象類型的數據結構 InstanceKlass:/InstanceKlass layout:/C+vtbl pointer Klass/java mirror Klass/super Klass/access_flags Klass/name Klass/
52、methods /fields .基于這個結構,諸如 o instanceof String 這樣的表達式就可以有足夠的信息判斷了。要注意的是 InstanceKlass 結構比較復雜,包含了類的所有方法、field 等等,方法又包含了字節碼等信息。這個數據結構是通過運行時解析 class 文件獲得的,為了保證安全性,解析 class 時還需要校驗字節碼的合法性(非通過 javac 產生的方法字節碼很容易引起 jvm crash)。CDS 可以將這個解析、校驗產生的數據結構存儲(dump)到文件,在下一次運行時重復使用。這個 dump 產物叫做 Shared Archive,以 jsa 后綴(
53、java shared archive)。為了減少 CDS 讀取 jsa dump 的開銷,避免將數據反序列化到 InstanceKlass 的開銷,jsa 文件中的存儲 layout 和 InstanceKlass 對象完全一樣,這樣在使用 jsa 數據時,只需要將 jsa 文件映射到內存,并且讓對象頭中的類型指針指向這塊內存地址即可,十分高效。Object:+-+|mark|+-+-+|classes.jsa file|Klass*+-java_mirror|super|methods|+-+|java_mirror|super|methods|fields|java_mirror|sup
54、er|methods|+-+四、JDK 提速 33+-+a)Alibaba Dragonwell 對 AppCDS 的優化 上述 AppCDS for custom classloader 的加載流程更加復雜的原因是 JVM 通過(classloader,className)二元組來唯一確定一個類。對于 BootClassloader、AppClassloader 在每次運行都是唯一的,因此可以在多次運行之間確定唯一的身份。對于 customClassloader 除了類型,并沒有明顯的唯一標識。AppCDS 因此無法在加載類階段通過 classloader 對象和類型去 shared arc
55、hive 定位到需要的InstanceKlass 條目。Dragonwell提供的解決方法是讓用戶為customClassloader標識唯一的identifier,加載相同類的 classloader 在多次運行間保持唯一的 identifier。并且擴展了 shared archive,記錄用戶定義的 classloader identifier 字段,這樣 AppCDS 便可以在運行時通過(identifier,className)二元組來迅速定位到 shared archive 中的類條目。從而讓 custom classloader 下的類加載能和 buildin class 一樣快
56、。在常見的微服務 workload 下,我們可以看到 Dragonwell 優化后的 AppCDS 將基礎的 AppCDS 的加速效果從 10%提升到了 40%。2.啟動 profiling 工具 1)現狀 目前有很多 Java 性能剖析工具,但專門用于 Java 啟動過程分析的還沒有。不過有些現有的工具,可以間接用于啟動過程分析,由于不是專門的工具,每個都存在這樣那樣的不足。比如 async-profiler,其強項是適合診斷 CPU 熱點、墻鐘熱點、內存分配熱點、JVM內鎖爭搶等場景,展現形式是火焰圖??梢栽趹脛倓倖雍?,馬上開啟 aync-profiler,持續剖析直到應用啟動完成。a
57、sync-profiler 的 CPU 熱點和墻鐘熱點能力對于分析啟動過程有很大幫助,可以找到占用 CPU 較多的方法,進而指導啟動加速的優化。四、JDK 提速 34 async-profiler 有 2 個主要缺點:第 1 個是展現形式較單一,關聯分析能力較弱,比如無法選擇特定時間區間,也無法支持選中多線程場景下的火焰圖聚合等。第 2 個是采集的數據種類較少,看不到類加載、GC、文件 IO、SocketIO、編譯、VM Operation 等方面的數據,沒法做精細的分析。再比如 arthas,arthas 的火焰圖底層也是利用 async-profiler,所以 async-profiler
58、存在的問題也無法回避。最后我們自然會想到 OpenJDK 的 JDK Flight Recorder,簡稱 JFR。AJDK8.5.10+和AJDK11 支持 JFR。JFR 是 JVM 內置的診斷工具,類似飛機上的黑匣子,可以低開銷的記錄很多關鍵數據,存儲到特定格式的 JFR 文件中,用這些數據可以很方便的還原應用啟動過程,從而指導啟動優化。JFR 的缺點是有一定的使用門檻,需要對虛擬機有一定的理解,高級配置也較復雜,同時還需要搭配桌面軟件Java Mission Control才能解析和閱讀 JFR 文件。面對上述問題,JVM 工具團隊進行了深入的思考,并逐步迭代開發出了針對啟動過程分析的
59、技術產品。2)解決方案 我們選擇 JFR 作為應用啟動性能剖析的基礎工具。JFR 開銷低,內建在 JDK 中無第三方依賴,且數據豐富。JFR 會周期性記錄 Running 狀態的線程的棧,可以構建 CPU熱點火焰圖。JFR 也記錄了類加載、GC、文件 IO、SocketIO、編譯、VM Operation、Lock 等事件,可以回溯線程的關鍵活動。對于早期版本 JFR 可能存在性能問題的特性,我們也支持自動切換到 aync-profiler 以更低開銷實現相同功能。為了降低 JFR 的使用門檻,我們封裝了一個 javaagent,通過在啟動命令中增加javaagent 參數,即可快速使用 JF
60、R。我們在 javaagent 中內置了文件收集和上傳功能,打通數據收集、上傳、分析和交互等關鍵環節,實現開箱即用。我們開發了一個 Web 版本的分析器(或者平臺),它接收到 javaagent 收集上傳的數據后,便可以直接查看和分析。我們開發了功能更豐富和易用的火焰圖和線程活動圖。在類加載和資源文件加載方面我們也做了專門的分析,類似 URLClassLoader 四、JDK 提速 35 在大量 Jar 包場景下的 Class Loading 開銷大、Tomcat 的 WebAppClassLoader 在大量 jar 包場景下 getResource 開銷大、并發控制不合理導致鎖爭搶線程等待
61、等問題都變得顯而易見,未來還將提供評估開啟 CDS(Class Data Sharing)以及 JarIndex后可以節省時間的預估能力。3)原理 當 Oracle 在 OpenJDK11 上開源了 JDK Flight Recorder 之后,阿里巴巴也是作為主要的貢獻者,與社區包括 RedHat 等,一起將 JFR 移植到了 OpenJDK 8。JFR 是 OpenJDK 內置的低開銷的監控和性能剖析工具,它深度集成在了虛擬機各個角落。JFR 由兩個部分組成:第 1 個部分分布在虛擬機的各個關鍵路徑上,負責捕獲信息。第 2 個部分是虛擬機內的單獨模塊,負責接收和存儲第 1 個部分產生的數據
62、。這些數據通常也叫做事件。JFR 包含 160 種以上的事件。JFR 的事件包含了很多有用的上下文信息以及時間戳。比如文件訪問,特定 GC 階段的發生,或者特定 GC 階段的耗時,相關的關鍵信息都被記錄到事件中。盡管 JFR 事件在他們發生時被創建,但 JFR 并不會實時的把事件數據存到硬盤上,JFR 會將事件數據保存在線程變量緩存中,這些緩存中的數據隨后會被轉移到一個global ring buffer。當 global ring buffer 寫滿時,才會被一個周期性的線程持久化到磁盤。雖然 JFR 本身比較復雜,但它被設計為低 CPU 和內存占用,總體開銷非常低,大約1%甚至更低。所以
63、JFR 適合用于生產環境,這一點和很多其它工具不同,他們的開銷一般都比 JFR 大。JFR 不僅僅用于監控虛擬機自身,它也允許在應用層自定義事件,讓應用程序開發者可以方便的使用 JFR 的基礎能力。四、JDK 提速 36 有些類庫沒有預埋 JFR 事件,也不方便直接修改源代碼,我們則用 javaagent 機制,在類加載過程中,直接用 ASM 修改字節碼插入 JFR 事件記錄的能力。比如 Tomcat的 WebAppClassLoader,為了記錄 getResource 事件,我們就采用了這個方法。整個系統的結構如下:五、ClassLoader 提速 37 五、ClassLoader 提速
64、1.現狀 集團整套電商系統已經運行好多年了,機器上運行的 jar 包,不會因為最近大環境不好而減少,只會逐年遞增,而中臺的幾個核心應用,因為之前走的是“平臺集成業務”的模式,像個黑洞一樣,所有業務都在上面開發,膨脹得更加明顯,比如熱點應用 A 機器上運行的 jar 包就有上千個,jar 包中包含的資源文件數量更是達到了上萬級別,通過工具分析,發現熱點應用 A 啟動耗時中有 180 秒以上是花在classLoader 上,占總耗時的 1/3 以上,其中占比大頭的是 findResource 的耗時。不論是 loadClass 還是 getResource,最終都會調用到 findResource
65、,慢主要是慢在資源的檢索上?,F在 spring 框架幾乎是每個 Java 必備的,各種 annotation,各種掃包,雖然極大的方便開發者,但也給應用的啟動帶來不少的負擔。目前集團有上萬多個 Java 應用,classLoader 如果可以進行優化,將帶來非常非??捎^的收益。2.解決方案 優化的方案可以簡單的用一句話概括,就是給URLClassLoader的資源查找加索引。3.提速效果 目前中臺核心應用都已升級,基本都有 100 秒以上的啟動提速,占總耗時的 2035%,效果非常明顯!4.原理 1)原生 URLClassLoader 為什么會慢 Java 的 JIT(just in time
66、)即時編譯,想必大家都不陌生,JDK 里不僅僅是類的裝載過程按這個思想去設計的,類的查找過程也是一樣的。通過研讀 URLClassPath 的實現,你會發現以下幾個特性:URLClassPath 初始化的時候,所有的 URL 都沒有 open。五、ClassLoader 提速 38 findResources 會比 findResource 更快的返回,因為實際并沒有查找,而是在調用 Enumeration 的 next()的時候才會去遍歷查找,而 findResource 去找了第一個。URL 是在遍歷過程逐個 open 的,會轉成 Loader,放到 loaders 里(數組結構,決定了順
67、序)和 lmap 中(Map 結構,防止重復加載)。一個 URL 可以通過 Class-Path 引入新的 URL(所以,理論上是可能存在新 URL又引入新的 URL,無限循環的場景)。因 為URL和Loader是 會 在 遍 歷 過 程 中 動 態 新 增,所 以URLClassPath#getLoader(int index)里加了兩把鎖。這些特性就是為了按需加載(懶加載),遍歷的過程是 O(N)的復雜度,按順序從頭到尾的遍歷,而且遍歷過程可能會伴隨著 URL 的打開,和新 URL 的引入,所以,隨著 jar 包數量的增多,每次 loadClass 或者 findResources 的耗時
68、會線性增長,調用次數也會增長(加載的類也變多了),啟動就慢下去了。慢的另一個次要原因是,getLoader(int index)加了兩把鎖。五、ClassLoader 提速 39 2)JDK 為什么不給 URLClassLoader 加索引 跟數據庫查詢一樣,數量多了,加個索引,立桿見效,那為什么 URLClassLoader 里沒加索引。其實,在 JDK8 里的 URLClassPath 代碼里面,是可以看到索引的蹤影的,通過加“-Dsun.cds.enableSharedLookupCache=true”來打開,但是,我換各種姿勢嘗試了數次,發現都沒生效,lookupCacheEnable
69、d 始終是 false,通過 debug發現 JDK 啟動的過程會把這個變量從 System 的 properties 里移除掉。另外,最近都在升 JDK11,也看了一下它里面的實現,發現這塊代碼直接被刪除的干干凈凈,不見蹤影了。通過仔細閱讀 URLClassPath 的代碼,我能想到 JDK 沒支持索引的原因有以下 3 點:原因一:跟按需加載相矛盾,且 URL 的加載有不確定性 建索引就得提前將所有 URL 打開并遍歷一遍,這與原先的按需加載設計相矛盾。另外,URL 的加載有 2 個不確定性:一是可能是非本地文件,需要從網絡上下載 jar 包,下載可能快,可能慢,也可能會失敗。二是 URL
70、的加載可能會引入新的 URL,新的 URL 又可能會引入新的 URL。原因二:不是所有 URL 都支持遍歷 URL 的類型可以歸為 3 種:本地文件目錄,如 classes 目錄。本地或者遠程下載下來的 jar 包。其他 URL。前 2 種是最基本最常見的,可以進行遍歷的,而第 3 種是不一定支持遍歷,默認只有一個 get 接口,傳入確定性的 name,返回有或者沒有。原因三:URL 里的內容可能在運行時被修改 比如本地文件目錄(classes 目錄)的 URL,就可以在運行時往改目錄下動態添加文件和類,URLClassLoader 是能加載到的,而索引要支持動態更新,這個非常難。五、Clas
71、sLoader 提速 40 3)FastURLClassLoader 如何進行提速 首先必須承認,URLClassLoader 需要支持所有場景都能建索引,這是有點不太現實的,所以,FastURLClassLoader 設計之初只為滿足絕大部分使用場景能夠提速,我們設計了一個 enable 的開關,關閉則跟原生 URLClassLoader 是一樣的。另外,一個 java 進程里經常會存在非常多的 URLClassLoader 實例,不能將所有實例都開打fast模式,這也是沒有直接在AliJDK里修改原生URLClassLoader的實現,而是新寫了個類的原因。FastURLClassLoad
72、er 繼承了 URLClassLoader,核心是將 URLClassPath 的實現重寫了,在初始化過程,會將所有的 Loader 進行初始化,并遍歷一遍生成 index 索引,后續 findResources的時候,不是從 0 開始,而是從 index 里獲取需要遍歷的 Loader數組,這將原來的 O(N)復雜度優化到了 O(1),且查找過程是無鎖的。FastURLClassLoader 會有以下特征:特征一:初始化過程不是懶加載,會慢一些 索引是在構造函數里進行初始化的,如果 url 都是本地文件(目錄或 Jar 包),這個過程不會暫用過多的時間,3000+的 jar,建索引耗時在 0
73、.5 秒以內,內部會根據 jar包數量進行多線程并發建索引。這個耗時,懶加載方式只是將它打散了,實際并沒有少,而且集團大部分應用都使用了 spring 框架,spring 啟動過程有各種掃包,第一次掃包,所有 URL 就都打開了。特征二:目前只支持本地文件夾和 Jar 類型的 URL 如果包含其他類型的 URL,會直接拋異常。雖然如 ftp 協議的 URL 也是支持遍歷的,但得針對性的去開發,而且 ftp 有網絡開銷,可能懶加載更適合,后續有需要再支持。特征三:目前不支持通過 META-INF/INDEX.LIST 引入更多 URL 當前正式版本支持通過 Class-Path 引入更多的 UR
74、L,但還不支持通過 META-INF/INDEX.LIST 來引入,目前還沒碰用到這個的場景,但可以支持。通過 Class-Path 五、ClassLoader 提速 41 引入更多的 URL 比較常見,比如 idea 啟動,如果 jar 太多,會因為參數過長而無法啟動,轉而選擇使用“JAR manifest”模式啟動。特征四:索引是初始化過程創建的,除了主動調用 addURL 時會更新,其他場景不會更新 比如在 classes 目錄下,新增文件或者子目錄,將不會更新到索引里。為此,FastURLClassLoader 做了一個兜底保護,如果通過索引找不到,會降級逐一到本地目錄類型的 URL
75、里找一遍(大部分場景下,目錄類型的 URL 只有一個),Jar 包類型的 URL 一般不會動態修改,所以沒找。5.注意事項 1)索引對內存的開銷 索引的是 jar 包和它目錄和根目錄文件的關系,所以不是特別大,熱點應用 A 有3000+個 jar 包,INDEX.LIST 的大小是 3.2M。2)同名類的仲裁 tomcat 在沒有 INDEX.LIST 的情況下,同名類使用哪個 jar 包中的,存在一定不確性,添加索引后,仲裁優先級是 jar 包名稱按字母排序來的,保險起見,可以對啟動后應用加載的類進行對比驗證。六、阿里中間件提速 42 六、阿里中間件提速 在阿里集團的大部分應用都是依賴了各種
76、中間件的 Java 應用,通過對核心中間件的集中優化,提升了各 Java 應用的整體啟動時間,提速 8%。1.Dubbo3 啟動優化 1)現狀 Dubbo3 作為阿里巴巴使用最為廣泛的分布式服務框架,服務集團內數萬個應用,它的重要性自然不言而喻;但是隨著業務的發展,應用依賴的 Jar 包和 HSF 服務也變得越來越多,導致應用啟動速度變得越來越慢,接下來我們將看一下 Dubbo3 如何優化啟動速度。2)Dubbo3 為什么會慢 Dubbo3 作為一個優秀的 RPC 服務框架,當然能夠讓用戶能夠進行靈活擴展,因此Dubbo3 框架提供各種各樣的擴展點一共 200+個。Dubbo3 的擴展點機制有
77、點類似 Java 標準的 SPI 機制,但是 Dubbo3 設置了 3 個不同的加載路徑,具體的加載路徑如下:META-INF/dubbo/internal/META-INF/dubbo/META-INF/services/也就是說,一個 SPI 的加載,一個 classLoader 就需要掃描這個 classLoader 下所有的 Jar 包 3 次。以熱點應用 A 為例,總的業務 bundle classLoader 數達到 582 個左右,那么所有的 SPI 加載需要的次數為:200(spi)*3(路徑)*582(classloader)=349200 次。六、阿里中間件提速 43 可以
78、看到掃描次數接近 35 萬次!并且整個過程是串行掃描的,而我們知道java.lang.ClassLoader#getResources 是一個比較耗時的操作,因此整個 SPI 加載過程耗時是非常久的。3)SPI 加載慢的解決方法 由我們前面的分析可以知道,要想減少耗時,第一是需要減少 SPI 掃描的次數,第二是提升并發度,減少無效等待時間。第一個減少 SPI 掃描的次數,我們經過分析得知,在整個集團的業務應用中,使用到的 SPI 集中在不到 10 個 SPI,因此我們疏理出一個 SPI 列表,在這個 SPI 列表中,默認只從 Dubbo3 框架所在 classLoader 的限定目錄加載,這樣
79、大大下降了掃描次數,使熱點應用 A 總掃描計數下降到不到 2 萬次,占原來的次數 5%這樣。第二個提升了對多個 classLoader 掃描的效率,采用并發線程池的方式來減少等待的時間,具體代碼如下:CountDownLatch countDownLatch=new CountDownLatch(classLoaders.size();for(ClassLoader classLoader:classLoaders)GlobalResourcesRepository.getGlobalExecutorService().submit()-resources.put(classLoader,lo
80、adResources(fileName,classLoader);countDownLatch.countDown(););4)其他優化手段 去除啟動關鍵鏈路的非必要同步耗時動作,轉成異步后臺處理。緩存啟動過程中查詢第三方可緩存的結果,反復重復使用。5)優化結果 熱點應用 A 啟動時間從 603 秒下降到 220 秒,總體時間下降了 383 秒。六、阿里中間件提速 44 2.TairClient 啟動優化 背景介紹:tair:阿里巴巴內部的緩存服務,類似于公有云的 redis。diamond:阿里巴巴內部配置中心,目前已經升級成 MSE,和公有云一樣的中間件產品。1)現狀 目前中臺基礎服務使
81、用的 tair 集群均使用獨立集群,獨立集群中使用多個 NS(命名空間)來區分不同的業務域,同時部分小的業務也會和其他業務共享一個公共集群內單個 NS。早期 tair 的集群是通過 configID 進行初始化,后來為了容災及設計上的考慮,調整為使用 username 進行初始化訪問,但 username 內部還是會使用 configid 來確定需要鏈接的集群。整個 tair 初始化過程中讀取的 diamond 配置的流程如下:a)根據 userName 獲取配置信息,從配置信息中可以獲得 TairConfigId 信息,用于標識所在集群。Dataid:ocs.userinfo.usernam
82、e Group:DEFAULT_GROUP b)根據 ConfigId 信息,獲取當前 tair 的路由規則,規定某一個機房會訪問的集群信息。dataId:tairConfigId group:tairConfigId.TGROUP 六、阿里中間件提速 45 通過該配置可以確定當前機房會訪問的目標集群配置,以 na610 為例,對應的配置集群 tair.mdb.mc.uic.NA61。c)獲取對應集群的信息,確定 tair 集群的 cs 列表。Dataid:tairConfigId/tair.mdb.mc.uic Group:tairClusterConfig/tair.mdb.mc.uic.
83、NA61 從上面的分析來看,在每次初始化的過程中,都會訪問相同的 diamond 配置,在初始化多個同集群的 namespace 的時候,部分關鍵配置就會多次訪問。但實際這部分 diamond 配置的數據本身是完全一致。由于 diamond 本身為了保護自身的穩定性,在客戶端對訪問單個配置的頻率做了控制,超過一定的頻率會進入等待超時階段,這一部分導致了應用的啟動延遲。在一分鐘的時間窗口內,限制單個 diamond 配置的訪問次數低于-DlimitTime配置,默認配置為 5,對于超過限制的配置會進入等待狀態。六、阿里中間件提速 46 2)優化方案 tair 客戶端進行改造,啟動過程中,對 Di
84、amond 的配置數據做緩存,配置監聽器維護緩存的數據一致性,tair 客戶端啟動時,優先從緩存中獲取配置,當緩存獲取不到時,再重新配置 Diamond 配置監聽及獲取 Diamond 配置信息。3.SwitchCenter 啟動優化 背景介紹:SwitchCenter:阿里巴巴集團內部的開關平臺,對應阿里云 AHAS 云產品:https:/ 六、阿里中間件提速 47 1)現狀 All methods add synchronized made this class to be thread safe.switch op is not frequent,so dont care about p
85、erformance here.這是 switch 源碼里存放各個 switch bean 的 SwitchContainer 中的注釋,可見當時的作者認為 switch bean 只需初始化一次,本身對性能的影響不大。但沒有預料到隨著業務的增長,switch bean 的初始化可能會成為應用啟動的瓶頸。業務平臺的定位導致了平臺啟動期間有大量業務容器初始化,由于 switch 中間件的大部分方法全部被 synchronized 修飾,因此所有應用容器初始化到了加載開關配置時(入口為 com.taobao.csp.switchcenter.core.SwitchManager#init())就需
86、要串行執行,嚴重影響啟動速度。2)解決方案 去除了關鍵路徑上的所有鎖。3)原理 本次升級將存放配置的核心數據結構修改為了ConcurrentMap,并基于putIfAbsent等 j.u.c API 做了小重構。值得關注的是修改后原先串行的對 diamond 配置的獲取變成了并行,觸發了 diamond 服務端限流,在大量獲取相同開關配置的情況下有很大概率拋異常啟動失敗。(如上:去鎖后,配置獲取的總次數不變,但是請求速率變快)六、阿里中間件提速 48 為了避免上述問題:在本地緩存 switch 配置的獲取。diamond 監聽 switch 配置的變更,確保即使 switch 配置被更新,本地
87、的緩存依然是最新的。4.TDDL 啟動優化 背景介紹:TDDL:基于 Java 語言的分布式數據庫系統,核心能力包括:分庫分表、透明讀寫分離、數據存儲平滑擴容、成熟的管控系統。1)現狀 TDDL 在啟動過程,隨著分庫分表規則的增加,啟動耗時呈線性上漲趨勢,在國際化多站點的場景下,耗時增長會特別明顯,未優化前,我們一個核心應用 TDDL 啟動耗時為 120 秒+(6 個庫),單個庫啟動耗時 20 秒+,且通過多個庫并行啟動,無法有效降低耗時。2)解決方案 通過工具分析,發現將分庫分表規則轉成 groovy 腳本,并生成 groovy 的 class,這塊邏輯總耗時非常久,調用次數非常多,且 gr
88、oovy 在 parseClass 里頭有加鎖(所以并行無效果)。調用次數多,是因為生成 class 的個數,會剩以物理表的數量,比如配置里只有一個邏輯表+一個規則(不同表的規則也存在大量重復),分成 1024張物理表,實際啟動時會產生 1024 個規則類,存在大量的重復,不僅啟動慢,還浪費了很多 metaspace。優化方案是新增一個全局的 GuavaCache,將規則和生成的規則類實例存放進去,避免相同的規則去創建不同的類和實例。六、阿里中間件提速 49 七、其他提速 50 七、其他提速 除了前面幾篇文章提到的優化點(classLoader 優化、中間件優化等)以外,我們還對中臺核心應用做
89、了很多啟動優化的工作,因為這些優化點可能覆蓋面沒那么廣,有些可能只是中臺在使用,所以在這里統一進行簡單介紹,大家可以按需讀取。另外,文章中提到的優化效果是單項的,組合在一起的效果不是各個的疊加,因為各項優化間有關聯,比如 classLoader 優化后,其他各項在不改的情況下,耗時也會下降。1.aspectj 相關優化 1)現狀 在進行啟動耗時診斷的時候,意外發現 aspectj 耗時特別久,達到了 54 秒多,不可接受。通過定位發現,如果應用里有使用到通過注解來判斷是否添加切面的規則,aspectj的耗時就會特別久。以下是熱點應用 A 中的例子:七、其他提速 51 2)解決方案 將 aspe
90、ctj 相關 jar 包版本升級到 1.9.0 及以上,熱點應用 A 升級后,aspectj 耗時從 54.5 秒降到了 6.3 秒,提速 48 秒多。另外,需要被 aspectj 識別的 annotation,RetentionPolicy 需要是 RUNTIME,不然會很慢。3)原理 通過工具采集到老版本的 aspectj 在判斷一個 bean 的 method 上是否有annotation 時的代碼堆棧,發現它去 jar 包里讀取 class 文件并解析類信息,耗時耗在類搜索和解析上。當看到這個的時候,第一反應就是,java.lang.Method 不是有 getAnnotation 方
91、法么,為什么要繞一圈自己去從 jar 包里解析出來。不太理解,就嘗試去看看最新版本的 aspectj 這塊是否有改動,最終發現升級即可解決。aspectj 去 class 原始文件中讀取的原因是 annotation 的 RetentionPolicy 如果不是RUNTIME 的話,運行時是獲取不到的,詳見:java.lang.annotation.RetentionPolicy的注釋。七、其他提速 52 1.8.8 版本在判斷是否有注解的邏輯:1.9.8 版本在判斷是否有注解的邏輯:與老版本的差異在于會判斷 annotation 的RetentionPolicy 是不是 RUNTIME 的,
92、是的話,就直接從 Method 里獲取了。老版本 aspectj 的相關執行堆棧:(格式:時間|類名|方法名|行數)七、其他提速 53 2.tbbpm 相關優化(javassist&javac)1)現狀 中臺大部分應用都使用 tbbpm 流程引擎,該引擎會將流程配置文件編譯成 java class 來進行調用,以提升性能。tbbpm 默認是使用 com.sun.tools.javac.Main 工具來實現代碼編譯的,通過工具分析,發現該過程特別耗時,交易應用 A 這塊耗時在 57 秒多。2)解決方案 通過采用 javassist 來編譯 bpm 文件,應用 A 預編譯 bpm 文件的耗時從 5
93、7 秒多降到了 8 秒多,快了 49 秒。3)原理 com.sun.tools.javac.Main 執行編譯時,會把 classpath 傳進去,自行從 jar 包里讀取類信息進行編譯,一樣是慢在類搜索和解析上。而 javassist 是使用 classLoader去獲取這些信息,根據前面的文章“ClassLoader 優化篇”,我們對 classLoader 加了索引,極大的提升搜索速度,所以會快非常多。javac 編譯相關執行堆棧:(格式:時間|類名|方法名|行數)七、其他提速 54 3.中臺框架相關優化 1)現狀 中臺為了能夠讓業務和中臺隔離的更加干凈,打造了業務容器隔離框架,以支持業
94、務實現在 classloader 和 spring context 上的隔離,同時支持熱部署。隨著業務容器數量的增多(目前熱點應用 A 有 500 多個業務容器),類掃描,spring bean 的掃描(getBean 方法)時,需要一個個遍歷業務容器,導致效率低下,啟動動慢。2)原理 這里大致描述一下優化點:對所有業務容器再做一層索引,所以會有兩層索引,還有一層是每個業務容器自己 classloader 的索引。在搜索 spring bean 的時候,會判斷 caller 的 classloader 所屬的業務容器,優先從這里頭去找。多個業務容器并行啟動(這個需要仔細驗證一下,因為 java
95、 可以搞各種黑科技來打破隔離,導致相互間有依賴,不能并行)。掃包工具加一層緩存(且緩存會區分不同業務容器),避免對同一個 package重復掃包。七、其他提速 55 掃包算法優化,當掃多個 pacakge 里的類時,是一個 O(N2)的復雜度,因為jar 包掃描有 IO 開銷,for 循環轉換了一下,并支持并發掃 jar 包。掃包優化前偽代碼:for(String scanPackage:scanPackages)for(URL url:classloader.getResources(scanPackage.replace(.,/)/掃 jar 包里的類,一次一個 package sync
96、scan package class in url 優化后偽代碼:/第一次遍歷,獲取所有的 URL Set allUrls=new HashSet();for(String scanPackage:scanPackages)for(URL url:classloader.getResources(scanPackage.replace(.,/)allUrls.add(url);for(URL url:allUrls)/每個 jar 包一個異步任務,僅掃一次,一次掃所有的 package async scan scanPackages in url 八、卷尾語:持續地.激情 56 卷尾語:持續地.激情 一輛車,可以從直升機上跳傘,也可以飛馳在冰海上,甚至可以安裝上火箭引擎上太空。上天入地沒有什么不可能,只要有想象,有創新。我們的研發基礎設施與工具還在路上,還在不斷改造的路上,還有很多的速度與激情可以追求。