日韩无码专区无码一级三级片|91人人爱网站中日韩无码电影|厨房大战丰满熟妇|AV高清无码在线免费观看|另类AV日韩少妇熟女|中文日本大黄一级黄色片|色情在线视频免费|亚洲成人特黄a片|黄片wwwav色图欧美|欧亚乱色一区二区三区

RELATEED CONSULTING
相關(guān)咨詢
選擇下列產(chǎn)品馬上在線溝通
服務(wù)時間:8:30-17:00
你可能遇到了下面的問題
關(guān)閉右側(cè)工具欄

新聞中心

這里有您想知道的互聯(lián)網(wǎng)營銷解決方案
Java應(yīng)用提速(速度與激情)

作者 | 道延 微波 沈陵 梁希 大熊 斷嶺 北緯 未宇 岱澤 浮圖

創(chuàng)新互聯(lián)從2013年成立,是專業(yè)互聯(lián)網(wǎng)技術(shù)服務(wù)公司,擁有項(xiàng)目網(wǎng)站建設(shè)、成都網(wǎng)站建設(shè)網(wǎng)站策劃,項(xiàng)目實(shí)施與項(xiàng)目整合能力。我們以讓每一個夢想脫穎而出為使命,1280元灤平做網(wǎng)站,已為上家服務(wù),為灤平各地企業(yè)和個人服務(wù),聯(lián)系電話:18982081108

一、速度與效率與激情

什么是速度?速度就是快,快有很多種。

有小李飛刀的快,也有閃電俠的快,當(dāng)然還有周星星的快:(船家)"我是出了名夠快"。(周星星)“這船好像在下沉?” (船家)“是呀!沉得快嘛”。

并不是任何事情越快越好,而是那些有價值有意義的事才越快越好。對于這些越快越好的事來說,快的表現(xiàn)是速度,而實(shí)質(zhì)上是提效。今天我們要講的java應(yīng)用的研發(fā)效率,即如何加快我們的java研發(fā)速度,提高我們的研發(fā)效率。

提效的方式也有很多種。但可以分成二大類。

我們使用一些工具與平臺進(jìn)行應(yīng)用研發(fā)與交付。當(dāng)一小部分低效應(yīng)用的用戶找工具與平臺負(fù)責(zé)人時,負(fù)責(zé)人建議提效的方案是:你看看其他應(yīng)用都這么快,說明我們平臺沒問題??赡苁悄銈兊膽?yīng)用架構(gòu)的問題,也可能是你們的應(yīng)用中祖?zhèn)鞔a太多了,要自己好好重構(gòu)下。這是大家最常見的第一類提效方式。

而今天我們要講的是第二類,是從工具與平臺方面進(jìn)行升級。即通過基礎(chǔ)研發(fā)設(shè)施與工具的微創(chuàng)新改進(jìn),實(shí)現(xiàn)研發(fā)提效,而用戶要做的可能就是換個工具的版本號。

買了一輛再好的車,帶來的只是速度。而自己不斷研究與改造發(fā)動機(jī),讓車子越來越快,在帶來不斷突破的“速度”的同時還帶來了“激情”。因?yàn)檫@是一個不斷用自己雙手創(chuàng)造奇跡的過程。

所以我們今天要講的不是買一輛好車,而是講如何改造“發(fā)動機(jī)”。

在阿里集團(tuán),有上萬多個應(yīng)用,大部分應(yīng)用都是java應(yīng)用,95%應(yīng)用的構(gòu)建編譯時間是5分鐘以上,鏡像構(gòu)建時間是2分鐘以上,啟動時間是8分鐘以上,這樣意味著研發(fā)同學(xué)的一次改動,大部分需要等待15分鐘左右,才能進(jìn)行業(yè)務(wù)驗(yàn)證。而且隨著業(yè)務(wù)迭代和時間的推移,應(yīng)用的整體編譯構(gòu)建、啟動速度也越來越慢,發(fā)布、擴(kuò)容、混部拉起等等一系列動作都被拖慢,極大的影響了研發(fā)和運(yùn)維整體效能,應(yīng)用提速刻不容緩。

我們將闡述通過基礎(chǔ)設(shè)施與工具的改進(jìn),實(shí)現(xiàn)從構(gòu)建到啟動全方面大幅提速的實(shí)踐和理論,相信能幫助大家。

二、maven構(gòu)建提速

2.1 現(xiàn)狀

maven其實(shí)并不是拖拉機(jī)。

相對于ant時代來說,maven是一輛大奔。但隨著業(yè)務(wù)越來越復(fù)雜,我們?yōu)闃I(yè)務(wù)提供服務(wù)的軟件也越來越復(fù)雜。雖然我們在提倡要降低軟件復(fù)雜度,但對于復(fù)雜的業(yè)務(wù)來說,降低了復(fù)雜度的軟件還是復(fù)雜的。而maven卻還是幾年的版本。在2012年推出maven3.0.0以來,直到現(xiàn)在的2022年,正好十年,但maven最新版本還是3系列3.8.6。所以在十年后的今天,站在復(fù)雜軟件面前,maven變成了一輛拖拉機(jī)。

2.2 解決方案

在這十年,雖然maven還是停留在主版本號是3,但當(dāng)今業(yè)界也不斷出現(xiàn)了優(yōu)秀的構(gòu)建工具,如gradle,bazel。但因各工具的生態(tài)不同,同時工具間遷移有成本與風(fēng)險(xiǎn),所以目前在java服務(wù)端應(yīng)用仍是以maven構(gòu)建為主。所以我們在apache-maven的基礎(chǔ)上,參照gradle,bazel等其它工具的思路,進(jìn)行了優(yōu)化,并以“amaven”命名。

因?yàn)閍maven完全兼容apache-maven,所支持的命令與參數(shù)都兼容,所以對我們研發(fā)同學(xué)來說,只要修改一個maven的版本號。

2.3 效果

從目前試驗(yàn)來看,對于mvn build耗時在3分鐘以上的應(yīng)用有效果。對于典型應(yīng)用從2325秒降到188秒,提升了10倍多。

我們再來看持續(xù)了一個時間段后的總體效果,典型應(yīng)用使用amaven后,構(gòu)建耗時p95的時間有較明顯下降,對比使用前后二個月的構(gòu)建耗時降了50%左右。

2.4 原理

如果說發(fā)動機(jī)是一輛車的靈魂,那依賴管理就是maven的靈魂。

因?yàn)閙aven就是為了系統(tǒng)化的管理依賴而產(chǎn)生的工具。使用過maven的同學(xué)都清楚,我們將依賴寫在pom.xml中,而這依賴又定義了自己的依賴在自己的pom.xml。通過pom文件的層次化來管理依賴的確讓我們方便很多。

一次典型的maven構(gòu)建過程,會是這樣:

從上圖可以看出,maven構(gòu)建主要有二個階段,而第一階段是第二階段的基礎(chǔ),基本上大部分的插件都會使用第一階段產(chǎn)生的依賴樹:

1.解析應(yīng)用的pom及依賴的pom,生成依賴樹;在解析過程中,一般還會從maven倉庫下載新增的依賴或更新了的SNAPSHOT包。

2.執(zhí)行各maven插件。

我們也通過分析實(shí)際的構(gòu)建日志,發(fā)現(xiàn)大于3分鐘的maven構(gòu)建,瓶頸都在“生成依賴樹”階段。而“生成依賴樹”階段慢的根本原因是一個module配置的依賴太多太復(fù)雜,它表現(xiàn)為:依賴太多,則要從maven倉庫下載的可能性越大。依賴太復(fù)雜,則依賴樹解析過程中遞歸次數(shù)越多。

在amaven中通過優(yōu)化依賴分析算法,與提升下載依賴速度來提升依賴分析的性能。除此之外,性能優(yōu)化的經(jīng)典思想是緩存增量,與分布式并發(fā),我們也遵循這個思想作了優(yōu)化。

在不斷優(yōu)化過程中,amaven也不斷地C/S化了,即amaven不再是一個client,而有了server端,同時將部分復(fù)雜的計(jì)算從client端移到了server端。而當(dāng)client越做越薄,server端的功能越來越強(qiáng)大時,server的計(jì)算所需要的資源也會越來越多,將這些資源用彈性伸縮來解決,慢慢地amaven云化了。

從單個client到C/S化再到云化,這也是一個工具不斷進(jìn)化的趨勢所在。

2.4.1 依賴樹

2.4.1.1 依賴樹緩存

既然依賴樹生成慢,那我們就將這依賴樹緩存起來。緩存后,這依賴樹可以不用重復(fù)生成,而且可以不同人,不同的機(jī)器的編譯進(jìn)行共享。使用依賴樹緩存后,一次典型的mvn構(gòu)建的過程如下:

從上圖中可以看到amaven-server,它主要負(fù)責(zé)依賴樹緩存的讀寫性能,保障存儲可靠性,及保證緩存的正確性等。

2.4.1.2 依賴樹生成算法優(yōu)化

雖在日常研發(fā)過程中,修改pom文件的概率較修改應(yīng)用java低,但還是有一定概率;同時當(dāng)pom中依賴了較多SNAPSHOT且SNAPSHOT有更新時,依賴樹緩存會失效掉。所以還是會有不少的依賴樹重新生成的場景。所以還是有必要來優(yōu)化依賴樹生成算法。

在maven2,及maven3版本中,包括最新的maven3.8.5中,maven是以深度優(yōu)先遍歷(DF)來生成依賴樹的(在社區(qū)版本中,目前master上已經(jīng)支持BF,但還未發(fā)release版本[1]。在遍歷過程中通過debug與打日志發(fā)現(xiàn)有很多相同的gav或相同的ga會被重復(fù)分析很多次,甚至數(shù)萬次。

樹的經(jīng)典遍歷算法主要有二種:深度優(yōu)先算法(DF)及 廣度優(yōu)先算法(BF),BF與DF的效率其實(shí)差不多的,但當(dāng)結(jié)合maven的版本仲裁機(jī)制考慮會發(fā)現(xiàn)有些差異。

我們來看看maven的仲裁機(jī)制,無論是maven2還是maven3,最主要的仲裁原則就是depth。相同ga或相同gav,誰更deeper,誰就skip,當(dāng)然仲裁的因素還有scope,profile等。結(jié)合depth的仲裁機(jī)制,按層遍歷(BF)會更優(yōu),也更好理解。如下圖,如按層來遍歷,則紅色的二個D1,D2就會skip掉,不會重復(fù)解析。(注意,實(shí)際場景是C的D1還是會被解析,因?yàn)樗螅?/p>

算法優(yōu)化的思路是:“提前修枝”。之前maven3的邏輯是先生成依賴樹再版本仲裁,而優(yōu)化后是邊生成依賴樹邊仲裁。就好比一個樹苗,要邊生長邊修枝,而如果等它長成了參天大樹后則修枝成本更大。

2.4.1.3 依賴下載優(yōu)化

maven在編譯過程中,會解析pom,然后不斷下載直接依賴與間接依賴到本地。一般本地目錄是.m2。對一線研發(fā)來說,本地的.m2不太會去刪除,所以除非有大的重構(gòu),每次編譯只有少量的依賴會下載。

但對于CICD平臺來說,因?yàn)榫幾g機(jī)一般不是獨(dú)占的,而是多應(yīng)用間共享的,所以為了應(yīng)用間不相互影響,每次編譯后可能會刪除掉.m2目錄。這樣,在CICD平臺要考慮.m2的隔離,及當(dāng).m2清理后要下載大量依賴包的場景。

而依賴包的下載,是需要經(jīng)過網(wǎng)絡(luò),所以當(dāng)一次編譯,如要下載上千個依賴,那構(gòu)建耗時大部分是在下載包,即瓶頸是下載。

1) 增大下載并發(fā)數(shù)

依賴包是從maven倉庫下載。maven3.5.0在編譯時默認(rèn)是啟了5個線程下載。我們可以通過aether.connector.basic.threads來設(shè)置更多的線程如20個來下載,但這要求maven倉庫要能撐得住翻倍的并發(fā)流量。所以我們對maven倉庫進(jìn)行了架構(gòu)升級,根據(jù)包不同的文件大小區(qū)間使用了本地硬盤緩存,redis緩存等包文件多級存儲來加快包的下載。

下表是對熱點(diǎn)應(yīng)用A用不同的下載線程數(shù)來下載5000多個依賴得到的下載耗時結(jié)果比較:

在amaven中我們加了對下載耗時的統(tǒng)計(jì)報(bào)告,包括下載多少個依賴,下載線程是多少,下載耗時是多少,方便大家進(jìn)行性能分析。如下圖:

同時為了減少網(wǎng)絡(luò)開銷,我們還采用了在編譯機(jī)本地建立了mirror機(jī)制。

2) 本地mirror

有些應(yīng)用有些復(fù)雜,它會在maven構(gòu)建的倉庫配置文件settings.xml(或pom文件)中指定下載多個倉庫。因?yàn)檫@應(yīng)用的要下載的依賴的確來自多個倉庫.當(dāng)指定多個倉庫時,下載一個依賴包,會依次從這多個倉庫查找并下載。

雖然maven的settings.xml語法支持多個倉庫,但localRepository卻只能指定一個。所以要看下docker是否支持將多個目錄volume到同一個容器中的目錄,但初步看了docker官網(wǎng)文檔,并不支持。

為解決按倉庫隔離.m2,且應(yīng)用依賴多個倉庫時的問題,我們現(xiàn)在通過對amaven的優(yōu)化來解決。

(架構(gòu)5.0:repo_mirror)

當(dāng)amaven執(zhí)行mvn build時,當(dāng)一個依賴包不在本地.m2目錄,而要下載時,會先到repo_mirror中對應(yīng)的倉庫中找,如找到,則從repo_mirror中對應(yīng)的倉庫中將包直接復(fù)制到.m2,否則就只能到遠(yuǎn)程倉庫下載,下載到.m2后,會同時將包復(fù)制到repo_mirror中對應(yīng)的倉庫中。

通過repo_mirror可以實(shí)現(xiàn)同一個構(gòu)建node上只會下載一次同一個倉庫的同一個文件。

2.4.1.4 SNAPSHOT版本號緩存

其實(shí)在amavenServer的緩存中,除了依賴樹,還緩存了SNAPSHOT的版本號。

我們的應(yīng)用會依賴一些SNAPSHOT包,同時當(dāng)我們在mvn構(gòu)建時加上-U就會去檢測這些SNAPSHOT的更新.而在apache-maven中檢測SNAPSHOT需要多次請求maven倉庫,會有一些網(wǎng)絡(luò)開銷。

現(xiàn)在我們結(jié)合maven倉庫作了優(yōu)化,從而讓多次請求maven倉庫,換成了一次cache服務(wù)直接拿到SNAPSHOT的最新版本。

2.4.2 增量

增量是與緩存息息相關(guān)的,增量的實(shí)現(xiàn)就是用緩存。maven的開放性是通過插件機(jī)制實(shí)現(xiàn)的,每個插件實(shí)現(xiàn)具體的功能,是一個函數(shù)。當(dāng)輸入不變,則輸出不變,即復(fù)用輸出,而將每次每個函數(shù)執(zhí)行后的輸出緩存起來。

上面講的依賴樹緩存,也是maven本身(非插件)的一種增量方式。

要實(shí)現(xiàn)增量的關(guān)鍵是定義好一個函數(shù)的輸入與輸出,即要保證定義好的輸入不變時,定義好的輸出肯定不變。每個插件自己是清楚輸入與輸出是什么的,所以插件的增量不是由amaven統(tǒng)一實(shí)現(xiàn),而是amaven提供了一個機(jī)制。如一個插件按約定定義好了輸入與輸出,則amaven在執(zhí)行前會檢測輸入是否變化,如沒變化,則直接跳過插件的執(zhí)行,而從緩存中取到輸出結(jié)果。

增量的效果是明顯的,如依賴樹緩存與算法的優(yōu)化能讓maven構(gòu)建從10分鐘降到2分鐘,那增量則可以將構(gòu)建耗時從分鐘級降到秒級。

2.4.3 daemon與分布式

daemon是為了進(jìn)一步達(dá)到10秒內(nèi)構(gòu)建的實(shí)現(xiàn)途徑。maven也是java程序,運(yùn)行時要將字節(jié)碼轉(zhuǎn)成機(jī)器碼,而這轉(zhuǎn)化有時間開銷。雖這開銷只有幾秒時間,但對一個mvn構(gòu)建只要15秒的應(yīng)用來說,所占比例也有10%多。為降低這時間開銷,可以用JIT直接將maven程序編譯成機(jī)器碼,同時mvn在構(gòu)建完成后,常駐進(jìn)程,當(dāng)有新構(gòu)建任務(wù)來時,直接調(diào)用mvn進(jìn)程。

一般,一個maven應(yīng)用編譯不會超過10分鐘,所以,看上去沒必要將構(gòu)建任務(wù)拆成子任務(wù),再調(diào)度到不同的機(jī)器上執(zhí)行分布式構(gòu)建。因?yàn)榉植际秸{(diào)度有時間開銷,這開銷可能比直接在本機(jī)上編譯耗時更大,即得不償失。所以分布式構(gòu)建的使用場景是大庫。為了簡化版本管理,將二進(jìn)制依賴轉(zhuǎn)成源碼依賴,將依賴較密切的源碼放在一個代碼倉庫中,就是大庫。當(dāng)一個大庫有成千上萬個module時,則非用分布式構(gòu)建不可了。使用分布式構(gòu)建,可以將大庫幾個小時的構(gòu)建降到幾分鐘級別。

三、本地idea環(huán)境提速

3.1 從盲俠說起

曾經(jīng)有有一位盲人叫座頭市,他雙目失明,但卻是一位頂尖的劍客,江湖上稱他為“盲俠”。

在我們的一線研發(fā)同學(xué)中,也有不少盲俠。

這些同學(xué)在本地進(jìn)行寫代碼時,是盲寫。他們寫的代碼盡管全都顯示紅色警示,寫的單測盡管在本地沒跑過,但還是照寫不誤。

我們一般的開發(fā)流程是,接到一個需求,從主干拉一個分支,再將本地的代碼切到這新分支,再刷新IDEA。但有些分支在刷新后,盡管等了30分鐘,盡管自己電腦的CPU沙沙直響,熱的冒泡,但I(xiàn)DEA的工作區(qū)還是有很多紅線。這些紅線逼我們不少同學(xué)走上了“盲俠”之路。

一個maven工程的java應(yīng)用,IDEA的導(dǎo)入也是使用了maven的依賴分析。而我們分析與實(shí)際觀測,一個需求的開發(fā),即在一個分支上的開發(fā),在本地使用maven的次數(shù)絕對比在CICD平臺上使用的次數(shù)多。

所以本地的maven的性能更需要提升,更需要改造。因?yàn)樗軒砀蟮娜诵А?/p>

3.2 解決方案

amaven要結(jié)合在本地的IDEA中使用也很方便。

  • 下載amaven最新版本。
  • 在本地解壓,如目錄 /Users/userName/soft/amaven-3.5.0。
  • 設(shè)置Maven home path:

  • 重啟idea后,點(diǎn)import project.

最后我們看看效果,對熱點(diǎn)應(yīng)用進(jìn)行import project測試,用maven要20分鐘左右,而用amaven3.5.0在3分鐘左右,在命中緩存情況下最佳能到1分鐘內(nèi)。

簡單四步后,我們就不用再當(dāng)“盲俠”了,在本地可以流暢地編碼與跑單元測試。

除了在IDEA中使用amaven的依賴分析能力外,在本地通過命令行來運(yùn)行mvn compile或dependency:tree,也完全兼容apache-maven的。

3.3 原理

IDEA是如何調(diào)用maven的依賴分析方法的?

在IDEA的源碼文件[2]中979行,調(diào)用了dependencyResolver.resolve(resolution)方法:

dependencyResolver就是通過maven home path指定的maven目錄中的DefaultProjectDependenciesResolver.java。

而DefaultProjectDependenciesResolver.resolve()方法就是依賴分析的入口。

IDEA主要用了maven的依賴分析的能力,在 “maven構(gòu)建提速”這一小節(jié)中, 我們已經(jīng)講了一些amaven加速的原理,其中依賴算法從DF換到BF,依賴下載優(yōu)化,整個依賴樹緩存,SNAPSHOT緩存這些特性都是與依賴分析過程相關(guān),所以都能用在IDEA提速上,而依賴倉庫mirror等因?yàn)樵谖覀冏约旱谋镜匾话悴粫h除.m2,所以不會有所體現(xiàn)。

amaven可以在本地結(jié)合IDEA使用,也可以在CICD平臺中使用,只是它們調(diào)用maven的方法的方式不同或入口不同而已。但對于maven協(xié)議來說“靈魂”的還是依賴管理與依賴分析。

四、docker構(gòu)建提速

4.1 背景

自從阿里巴巴集團(tuán)容器化后,開發(fā)人員經(jīng)常被鏡像構(gòu)建速度困擾,每天要發(fā)布很多次的應(yīng)用體感尤其不好。我們幾年前已經(jīng)按最佳實(shí)踐推薦每個應(yīng)用要把鏡像拆分成基礎(chǔ)鏡像和應(yīng)用鏡像,但是高頻修改的應(yīng)用鏡像的構(gòu)建速度依然不盡如人意。

為了跟上主流技術(shù)的發(fā)展,我們計(jì)劃把CICD平臺的構(gòu)建工具升級到moby-buildkit,docker的最新版本也計(jì)劃把構(gòu)建切換到moby- buildkit了,這個也是業(yè)界的趨勢。同時在 buildkit基礎(chǔ)上我們作了一些增強(qiáng)。

4.2 增強(qiáng)

4.2.1 新語法SYNC

我們先用增量的思想,相對于COPY增加了一個新語法SYNC。

我們分析java應(yīng)用高頻構(gòu)建部分的鏡像構(gòu)建場景,高頻情況下只會執(zhí)行Dockerfile中的一個指令:

COPY appName.tgz /home/appName/target/appName.tgz

發(fā)現(xiàn)大多數(shù)情況下java應(yīng)用每次構(gòu)建雖然會生成一個新的app.war目錄,但是里面的大部分jar文件都是從maven等倉庫下載的,它們的創(chuàng)建和修改時間雖然會變化但是內(nèi)容的都是沒有變化的。對于一個1G大小的war,每次發(fā)布變化的文件平均也就三十多個,大小加起來2-3 M,但是由于這個appName.war目錄是全新生成的,這個copy指令每次都需要全新執(zhí)行,如果全部拷貝,對于稍微大點(diǎn)的應(yīng)用這一層就占有1G大小的空間,鏡像的copy push pull都需要處理很多重復(fù)的內(nèi)容,消耗無謂的時間和空間。

如果我們能做到定制dockerfile中的copy指令,拷貝時像Linux上面的rsync一樣只做增量copy的話,構(gòu)建速度、上傳速度、增量下載速度、存儲空間都能得到很好的優(yōu)化。因?yàn)閙oby-buildkit的代碼架構(gòu)分層比較好,我們基于dockerfile前端定制了內(nèi)部的SYNC指令。我們掃描到SYNC語法時,會在前端生成原生的兩個指令,一個是從基線鏡像中l(wèi)ink 拷貝原來那個目錄(COPY),另一個是把兩個目錄做比較(DIFF),把有變化的文件和刪除的文件在新的一層上面生效,這樣在基線沒有變化的情況下,就做到了高頻構(gòu)建每次只拷貝上傳下載幾十個文件僅幾兆內(nèi)容的這一層。

而用戶要修改的,只是將原來的COPY語法修改成SYNC就行了。

如將:COPY appName.tgz /home/admin/appName/target/appName.tgz

修改為:SYNC appName.dir /home/admin/appName/target/appName.war

我們再來看看SYNC的效果。集團(tuán)最核心的熱點(diǎn)應(yīng)用A切換到moby-buildkit以及我們的sync指令后90分位鏡像構(gòu)建速度已經(jīng)從140秒左右降低到80秒左右:

4.2.2 none-gzip實(shí)現(xiàn)

為了讓moby- buildkit能在CICD平臺上面用起來,首先要把none-gzip支持起來。

這個需求在 docker 社區(qū)也有很多討論[3],內(nèi)部環(huán)境網(wǎng)絡(luò)速度不是問題,如果有g(shù)zip會導(dǎo)致90%的時間都花在壓縮和解壓縮上面,構(gòu)建和下載時間會加倍,發(fā)布環(huán)境拉鏡像的時候主機(jī)上一些CPU也會被gzip解壓打滿,影響同主機(jī)其它容器的運(yùn)行。

雖然none-gzip后,CPU不會高,但會讓上傳下載等傳輸過程變慢,因?yàn)槲募粔嚎s變大了。但相對于CPU資源來說,內(nèi)網(wǎng)情況下帶寬資源不是瓶頸。只需要在上傳鏡像層時按配置跳過 gzip 邏輯去掉,并把鏡像層的MediaType從 application/vnd.docker.image.rootfs.diff.tar.gzip 改成application/vnd.docker.image.rootfs.diff.tar 就可以在內(nèi)網(wǎng)環(huán)境下充分提速了。

4.2.3 單層內(nèi)并發(fā)下載

在CICD過程中,即使是同一個應(yīng)用的構(gòu)建,也可能會被調(diào)度到不同的編譯機(jī)上。即使構(gòu)建調(diào)度有一定的親和性。

為了讓新構(gòu)建機(jī),或應(yīng)用換構(gòu)建機(jī)后能快速拉取到基礎(chǔ)鏡像,由于我們以前的最佳實(shí)踐是要求用戶把鏡像分成兩個(基礎(chǔ)鏡像與應(yīng)用鏡像),而基礎(chǔ)鏡像一般單層就有超過1G大小的,多層并發(fā)拉取對于單層特別大的鏡像已經(jīng)沒有效果。

所以我們在“層間并發(fā)拉取”的基礎(chǔ)上,還增加了“層內(nèi)并發(fā)拉取”,讓拉鏡像的速度提升了4倍左右。

當(dāng)然實(shí)現(xiàn)這層內(nèi)并發(fā)下載是有前提的,即鏡像的存儲需要支持分段下載。因?yàn)槲覀児臼怯昧税⒗镌频腛SS來存儲docker鏡像,它支持分段下載或多線程下載。

4.2.4 無中心P2P下載

現(xiàn)在都是用containerd中的content store來存儲鏡像原始數(shù)據(jù),也就是說每個節(jié)點(diǎn)本身就存儲了一個鏡像的所有原始數(shù)據(jù)manifest和layers。所以如果多個相鄰的節(jié)點(diǎn),都需要拉鏡像的話,可以先看到中心目錄服務(wù)器上查看鄰居節(jié)點(diǎn)上面是否已經(jīng)有這個鏡像了,如果有的話就可以直接從鄰居節(jié)點(diǎn)拉這個鏡像。而不需要走鏡像倉庫去取鏡像layer,而manifest數(shù)據(jù)還必須從倉庫獲取是為了防止鏡像名對應(yīng)的數(shù)據(jù)已經(jīng)發(fā)生了變化了,只要取到manifest后其它的layer數(shù)據(jù)都可以從相鄰的節(jié)點(diǎn)獲取,每個節(jié)點(diǎn)可以只在每一層下載后的五分鐘內(nèi)(時間可配置)提供共享服務(wù),這樣大概率還能用到本地page cache,而不用真正讀磁盤。

中心OSS服務(wù)總共只能提供最多20G的帶寬,從歷史拉鏡像數(shù)據(jù)能看到每個節(jié)點(diǎn)的下載速度都很難超過30M,但是我們現(xiàn)在每個節(jié)點(diǎn)都是50G網(wǎng)絡(luò),節(jié)點(diǎn)相互之間共享鏡像層數(shù)據(jù)可以充分利用到節(jié)點(diǎn)本地的50G網(wǎng)絡(luò)帶寬,當(dāng)然為了不影響其它服務(wù),我們把鏡像共享的帶寬控制在200M以下。

4.2.5 鏡像ONBUILD支持

社區(qū)的 moby-buidkit 已經(jīng)支持了新的 schema2 格式的鏡像的 ONBUILD 了,但是集團(tuán)內(nèi)部還有很多應(yīng)用 FROM 的基礎(chǔ)鏡像是 schema1 格式的基礎(chǔ)鏡像,這些基礎(chǔ)鏡像中很多都很巧妙的用了一些 ONBUILD 指令來減少 FROM 它的 Dockerfile中的公共構(gòu)建指令。如果不能解析 schema1 格式的鏡像,這部分應(yīng)用的構(gòu)建雖然會成功,但是其實(shí)很多應(yīng)該執(zhí)行的指令并沒有執(zhí)行,對于這個能力缺失,我們在內(nèi)部補(bǔ)上的同時也把這些修改回饋給了社區(qū)[4]。

五、JDK提速

5.1 AppCDS

5.1.1 現(xiàn)狀

CDS(Class Data Sharing)[5]在Oracle JDK1.5被首次引入,在Oracle JDK8u40[6]中引入了AppCDS,支持JDK以外的類 ,但是作為商業(yè)特性提供。隨后Oracle將AppCDS貢獻(xiàn)給了社區(qū),在JDK10中CDS逐漸完善,也支持了用戶自定義類加載器(又稱AppCDS v2[7])。

目前CDS在阿里的落地情況:

熱點(diǎn)應(yīng)用A使用CDS減少了10秒啟動時間

云產(chǎn)品SAE和FC在使用Dragonwell11時開啟CDS、AOT等特性加速啟動

經(jīng)過十年的發(fā)展,CDS已經(jīng)發(fā)展為一項(xiàng)成熟的技術(shù)。但是很容易令人不解的是CDS不管在阿里的業(yè)務(wù)還是業(yè)界(即便是AWS Lambda)都沒能被大規(guī)模使用。關(guān)鍵原因有兩個:

5.1.1.1 AppCDS在實(shí)踐中效果不明顯

jsa中存儲的InstanceKlass是對class文件解析的產(chǎn)物。對于boot classloader(加載jre/lib/rt.jar下面的類的類加載器)和system(app) 類加載器(加載-classpath下面的類的類加載器),CDS有內(nèi)部機(jī)制可以跳過對class文件的讀取,僅僅通過類名在jsa文件中匹配對應(yīng)的數(shù)據(jù)結(jié)構(gòu)。

Java語言還提供用戶自定義類加載器(custom class loader)的機(jī)制,用戶通過Override自己的 Classloader.loadClass() 查找類,AppCDS 在為customer class loade時加載類是需要經(jīng)過如下步驟:

調(diào)用用戶定義的Classloader.loadClass(),拿到class byte stream

計(jì)算class byte stream的checksum,與jsa中的同類名結(jié)構(gòu)的checksum比較

如果匹配成功則返回jsa中的InstanceKlass,否則繼續(xù)使用slow path解析class文件

5.1.1.2 工程實(shí)踐不友好

使用AppCDS需要如下步驟:

  • 針對當(dāng)前版本在生產(chǎn)環(huán)境啟動應(yīng)用,收集profiling信息
  • 基于profiling信息生成jsa(java sahred archive) dump
  • 將jsa文件和應(yīng)用本身打包在一起,發(fā)布到生產(chǎn)環(huán)境

由于這種trace-replay模式的復(fù)雜性,在SAE和FC云產(chǎn)品的落地都是通過發(fā)布流程的定制以及開發(fā)復(fù)雜的命令行工具來解決的。

5.1.2 解決方案

針對上述的問題1,在熱點(diǎn)應(yīng)用A上CDS配合JarIndex或者使用編譯器團(tuán)隊(duì)開發(fā)的EagerAppCDS特性(原理見5.1.3.1)都能讓CDS發(fā)揮最佳效果。

經(jīng)驗(yàn)證,在熱點(diǎn)應(yīng)用A已經(jīng)使用JarIndex做優(yōu)化的前提下進(jìn)一步使用EagerAppCDS依然可以獲得15秒左右的啟動加速效果。

5.1.3 原理

面向?qū)ο笳Z言將對象(數(shù)據(jù))和方法(對象上的操作)綁定到了一起,來提供更強(qiáng)的封裝性和多態(tài)。這些特性都依賴對象頭中的類型信息來實(shí)現(xiàn),Java、Python語言都是如此。Java對象在內(nèi)存中的layout如下:

+-------------+
| mark |
+-------------+
| Klass* |
+-------------+
| fields |
| |
+-------------+

mark表示了對象的狀態(tài),包括是否被加鎖、GC年齡等等。而Klass*指向了描述對象類型的數(shù)據(jù)結(jié)構(gòu) InstanceKlass :

//  InstanceKlass layout:
// [C++ vtbl pointer ] Klass
// [java mirror ] Klass
// [super ] Klass
// [access_flags ] Klass
// [name ] Klass
// [methods ]
// [fields ]
...

基于這個結(jié)構(gòu),諸如 o instanceof String 這樣的表達(dá)式就可以有足夠的信息判斷了。要注意的是InstanceKlass結(jié)構(gòu)比較復(fù)雜,包含了類的所有方法、field等等,方法又包含了字節(jié)碼等信息。這個數(shù)據(jù)結(jié)構(gòu)是通過運(yùn)行時解析class文件獲得的,為了保證安全性,解析class時還需要校驗(yàn)字節(jié)碼的合法性(非通過javac產(chǎn)生的方法字節(jié)碼很容易引起jvm crash)。

CDS可以將這個解析、校驗(yàn)產(chǎn)生的數(shù)據(jù)結(jié)構(gòu)存儲(dump)到文件,在下一次運(yùn)行時重復(fù)使用。這個dump產(chǎn)物叫做Shared Archive,以jsa后綴(java shared archive)。

為了減少CDS讀取jsa dump的開銷,避免將數(shù)據(jù)反序列化到InstanceKlass的開銷,jsa文件中的存儲layout和InstanceKlass對象完全一樣,這樣在使用jsa數(shù)據(jù)時,只需要將jsa文件映射到內(nèi)存,并且讓對象頭中的類型指針指向這塊內(nèi)存地址即可,十分高效。

Object:
+-------------+
| mark | +-------------------------+
+-------------+ |classes.jsa file |
| Klass* +--------->java_mirror|super|methods|
+-------------+ |java_mirror|super|methods|
| fields | |java_mirror|super|methods|
| | +-------------------------+
+-------------+

5.1.3.1 Alibaba Dragonwell對AppCDS的優(yōu)化

上述AppCDS for custom classloader的加載流程更加復(fù)雜的原因是JVM通過(classloader, className)二元組來唯一確定一個類。

對于BootClassloader、AppClassloader在每次運(yùn)行都是唯一的,因此可以在多次運(yùn)行之間確定唯一的身份

對于customClassloader除了類型,并沒有明顯的唯一標(biāo)識。AppCDS因此無法在加載類階段通過classloader對象和類型去shared archive定位到需要的InstanceKlass條目。

Dragonwell提供的解決方法是讓用戶為customClassloader標(biāo)識唯一的identifier,加載相同類的classloader在多次運(yùn)行間保持唯一的identifier。并且擴(kuò)展了shared archive,記錄用戶定義的classloader identifier字段,這樣AppCDS便可以在運(yùn)行時通過(identifier, className)二元組來迅速定位到shared archive中的類條目。從而讓custom classloader下的類加載能和buildin class一樣快。

在常見的微服務(wù)workload下,我們可以看到Dragonwell優(yōu)化后的AppCDS將基礎(chǔ)的AppCDS的加速效果從10%提升到了40%。

5.2 啟動profiling工具

5.2.1 現(xiàn)狀

目前有很多Java性能剖析工具,但專門用于Java啟動過程分析的還沒有。不過有些現(xiàn)有的工具,可以間接用于啟動過程分析,由于不是專門的工具,每個都存在這樣那樣的不足。

比如async-profiler,其強(qiáng)項(xiàng)是適合診斷CPU熱點(diǎn)、墻鐘熱點(diǎn)、內(nèi)存分配熱點(diǎn)、JVM內(nèi)鎖爭搶等場景,展現(xiàn)形式是火焰圖??梢栽趹?yīng)用剛剛啟動后,馬上開啟aync-profiler,持續(xù)剖析直到應(yīng)用啟動完成。async-profiler的CPU熱點(diǎn)和墻鐘熱點(diǎn)能力對于分析啟動過程有很大幫助,可以找到占用CPU較多的方法 ,進(jìn)而指導(dǎo)啟動加速的優(yōu)化。async-profiler有2個主要缺點(diǎn),第1個是展現(xiàn)形式較單一,關(guān)聯(lián)分析能力較弱,比如無法選擇特定時間區(qū)間,也無法支持選中多線程場景下的火焰圖聚合等。第2個是采集的數(shù)據(jù)種類較少,看不到類加載、GC、文件IO、SocketIO、編譯、VM Operation等方面的數(shù)據(jù),沒法做精細(xì)的分析。

再比如arthas,arthas的火焰圖底層也是利用async-profiler,所以async-profiler存在的問題也無法回避。

最后我們自然會想到OpenJDK的JDK Flight Recorder,簡稱JFR。AJDK8.5.10+和AJDK11支持JFR。JFR是JVM內(nèi)置的診斷工具,類似飛機(jī)上的黑匣子,可以低開銷的記錄很多關(guān)鍵數(shù)據(jù),存儲到特定格式的JFR文件中,用這些數(shù)據(jù)可以很方便的還原應(yīng)用啟動過程,從而指導(dǎo)啟動優(yōu)化。JFR的缺點(diǎn)是有一定的使用門檻,需要對虛擬機(jī)有一定的理解,高級配置也較復(fù)雜,同時還需要搭配桌面軟件Java Mission Control才能解析和閱讀JFR文件。

面對上述問題,JVM工具團(tuán)隊(duì)進(jìn)行了深入的思考,并逐步迭代開發(fā)出了針對啟動過程分析的技術(shù)產(chǎn)品。

5.2.2 解決方案

1、我們選擇JFR作為應(yīng)用啟動性能剖析的基礎(chǔ)工具。JFR開銷低,內(nèi)建在JDK中無第三方依賴,且數(shù)據(jù)豐富。JFR會周期性記錄Running狀態(tài)的線程的棧,可以構(gòu)建CPU熱點(diǎn)火焰圖。JFR也記錄了類加載、GC、文件IO、SocketIO、編譯、VM Operation、Lock等事件,可以回溯線程的關(guān)鍵活動。對于早期版本JFR可能存在性能問題的特性,我們也支持自動切換到aync-profiler以更低開銷實(shí)現(xiàn)相同功能。

2、為了降低JFR的使用門檻,我們封裝了一個javaagent,通過在啟動命令中增加javaagent參數(shù),即可快速使用JFR。我們在javaagent中內(nèi)置了文件收集和上傳功能,打通數(shù)據(jù)收集、上傳、分析和交互等關(guān)鍵環(huán)節(jié),實(shí)現(xiàn)開箱即用。

3、我們開發(fā)了一個Web版本的分析器(或者平臺),它接收到j(luò)avaagent收集上傳的數(shù)據(jù)后,便可以直接查看和分析。我們開發(fā)了功能更豐富和易用的火焰圖和線程活動圖。在類加載和資源文件加載方面我們也做了專門的分析,類似URLClassLoader在大量Jar包場景下的Class Loading開銷大、Tomcat的WebAppClassLoader在大量jar包場景下getResource開銷大、并發(fā)控制不合理導(dǎo)致鎖爭搶線程等待等問題都變得顯而易見,未來還將提供評估開啟CDS(Class Data Sharing)以及JarIndex后可以節(jié)省時間的預(yù)估能力。

5.2.3 原理

當(dāng)Oracle在OpenJDK11上開源了JDK Flight Recorder之后,阿里巴巴也是作為主要的貢獻(xiàn)者,與社區(qū)包括 RedHat 等,一起將 JFR 移植到了 OpenJDK 8。

JFR是OpenJDK內(nèi)置的低開銷的監(jiān)控和性能剖析工具,它深度集成在了虛擬機(jī)各個角落。JFR由兩個部分組成:第1個部分分布在虛擬機(jī)的各個關(guān)鍵路徑上,負(fù)責(zé)捕獲信息;第2個部分是虛擬機(jī)內(nèi)的單獨(dú)模塊,負(fù)責(zé)接收和存儲第1個部分產(chǎn)生的數(shù)據(jù)。這些數(shù)據(jù)通常也叫做事件。JFR包含160種以上的事件。JFR的事件包含了很多有用的上下文信息以及時間戳。比如文件訪問,特定GC階段的發(fā)生,或者特定GC階段的耗時,相關(guān)的關(guān)鍵信息都被記錄到事件中。

盡管JFR事件在他們發(fā)生時被創(chuàng)建,但JFR并不會實(shí)時的把事件數(shù)據(jù)存到硬盤上,JFR會將事件數(shù)據(jù)保存在線程變量緩存中,這些緩存中的數(shù)據(jù)隨后會被轉(zhuǎn)移到一個global ring buffer。當(dāng)global ring buffer寫滿時,才會被一個周期性的線程持久化到磁盤。

雖然JFR本身比較復(fù)雜,但它被設(shè)計(jì)為低CPU和內(nèi)存占用,總體開銷非常低,大約1%甚至更低。所以JFR適合用于生產(chǎn)環(huán)境,這一點(diǎn)和很多其它工具不同,他們的開銷一般都比JFR大。

JFR不僅僅用于監(jiān)控虛擬機(jī)自身,它也允許在應(yīng)用層自定義事件,讓應(yīng)用程序開發(fā)者可以方便的使用JFR的基礎(chǔ)能力。有些類庫沒有預(yù)埋JFR事件,也不方便直接修改源代碼,我們則用javaagent機(jī)制,在類加載過程中,直接用ASM修改字節(jié)碼插入JFR事件記錄的能力。比如Tomcat的WebAppClassLoader,為了記錄getResource事件,我們就采用了這個方法。

整個系統(tǒng)的結(jié)構(gòu)如下:

六、ClassLoader提速

6.1 現(xiàn)狀

集團(tuán)整套電商系統(tǒng)已經(jīng)運(yùn)行好多年了,機(jī)器上運(yùn)行的jar包,不會因?yàn)樽罱蟓h(huán)境不好而減少,只會逐年遞增,而中臺的幾個核心應(yīng)用,所有業(yè)務(wù)都在上面開發(fā),膨脹得更加明顯,比如熱點(diǎn)應(yīng)用A機(jī)器上運(yùn)行的jar包就有三千多個,jar包中包含的資源文件數(shù)量更是達(dá)到了上萬級別,通過工具分析,啟動有180秒以上是花在ClassLoader上,占總耗時的1/3以上,其中占比大頭的是findResource的耗時。不論是loadClass還是getResource,最終都會調(diào)用到findResource,慢主要是慢在資源的檢索上?,F(xiàn)在spring框架幾乎是每個java必備的,各種annotation,各種掃包,雖然極大的方便開發(fā)者,但也給應(yīng)用的啟動帶來不少的負(fù)擔(dān)。目前集團(tuán)有上萬多個Java應(yīng)用,ClassLoader如果可以進(jìn)行優(yōu)化,將帶來非常非??捎^的收益。

6.2 解決方案

優(yōu)化的方案可以簡單的用一句話概括,就是給URLClassLoader的資源查找加索引。

6.3 提速效果

目前中臺核心應(yīng)用都已升級,基本都有100秒以上的啟動提速,占總耗時的20~35%,效果非常明顯!

6.4 原理

6.4.1 原生URLClassLoader為什么會慢

java的JIT(just in time)即時編譯,想必大家都不陌生,JDK里不僅僅是類的裝載過程按這個思想去設(shè)計(jì)的,類的查找過程也是一樣的。通過研讀URLClassPath的實(shí)現(xiàn),你會發(fā)現(xiàn)以下幾個特性:

  • URLClassPath初始化的時候,所有的URL都沒有open;
  • findResources會比findResource更快的返回,因?yàn)閷?shí)際并沒有查找,而是在調(diào)用Enumeration的next() 的時候才會去遍歷查找,而findResource去找了第一個;
  • URL是在遍歷過程逐個open的,會轉(zhuǎn)成Loader,放到loaders里(數(shù)組結(jié)構(gòu),決定了順序)和lmap中(Map結(jié)構(gòu), 防止重復(fù)加載);
  • 一個URL可以通過Class-Path引入新的URL(所以,理論上是可能存在新URL又引入新的URL,無限循環(huán)的場景);
  • 因?yàn)閁RL和Loader是會在遍歷過程中動態(tài)新增,所以URLClassPath#getLoader(int index) 里加了兩把鎖;

這些特性就是為了按需加載(懶加載),遍歷的過程是O(N)的復(fù)雜度,按順序從頭到尾的遍歷,而且遍歷過程可能會伴隨著URL的打開,和新URL的引入,所以,隨著jar包數(shù)量的增多,每次loadClass或者findResources的耗時會線性增長,調(diào)用次數(shù)也會增長(加載的類也變多了),啟動就慢下去了。慢的另一個次要原因是,getLoader(int index)加了兩把鎖。

6.4.2 JDK為什么不給URLClassLoader加索引

跟數(shù)據(jù)庫查詢一樣,數(shù)量多了,加個索引,立桿見效,那為什么URLClassLoader里沒加索引。其實(shí),在JDK8里的URLClassPath代碼里面,是可以看到索引的蹤影的,通過加“-Dsun.cds.enableSharedLookupCache=true”來打開,但是,換各種姿勢嘗試了數(shù)次,發(fā)現(xiàn)都沒生效,lookupCacheEnabled始終是false,通過debug發(fā)現(xiàn)JDK啟動的過程會把這個變量從System的properties里移除掉。另外,最近都在升JDK11,也看了一下它里面的實(shí)現(xiàn),發(fā)現(xiàn)這塊代碼直接被刪除的干干凈凈,不見蹤影了。

通過仔細(xì)閱讀URLClassPath的代碼,JDK沒支持索引的原因有以下3點(diǎn):

原因一:跟按需加載相矛盾,且URL的加載有不確定性

建索引就得提前將所有URL打開并遍歷一遍,這與原先的按需加載設(shè)計(jì)相矛盾。另外,URL的加載有2個不確定性:一是可能是非本地文件,需要從網(wǎng)絡(luò)上下載jar包,下載可能快,可能慢,也可能會失??;二是URL的加載可能會引入新的URL,新的URL又可能會引入新的URL。

原因二:不是所有URL都支持遍歷

URL的類型可以歸為3種:1. 本地文件目錄,如classes目錄;2. 本地或者遠(yuǎn)程下載下來的jar包;3. 其他URL。前2種是最基本最常見的,可以進(jìn)行遍歷的,而第3種是不一定支持遍歷,默認(rèn)只有一個get接口,傳入確定性的name,返回有或者沒有。

原因三:URL里的內(nèi)容可能在運(yùn)行時被修改

比如本地文件目錄(classes目錄)的URL,就可以在運(yùn)行時往改目錄下動態(tài)添加文件和類,URLClassLoader是能加載到的,而索引要支持動態(tài)更新,這個非常難。

6.4.3 FastURLClassLoader如何進(jìn)行提速

首先必須承認(rèn),URLClassLoader需要支持所有場景都能建索引,這是有點(diǎn)不太現(xiàn)實(shí)的,所以,F(xiàn)astURLClassLoader設(shè)計(jì)之初只為滿足絕大部分使用場景能夠提速,我們設(shè)計(jì)了一個enable的開關(guān),關(guān)閉則跟原生URLClassLoader是一樣的。另外,一個java進(jìn)程里經(jīng)常會存在非常多的URLClassLoader實(shí)例,不能將所有實(shí)例都開打fast模式,這也是沒有直接在AliJDK里修改原生URLClassLoader的實(shí)現(xiàn),而是新寫了個類的原因。

FastURLClassLoader繼承了URLClassLoader,核心是將URLClassPath的實(shí)現(xiàn)重寫了,在初始化過程,會將所有的Loader進(jìn)行初始化,并遍歷一遍生成index索引,后續(xù)findResources的時候,不是從0開始,而是從index里獲取需要遍歷的Loader數(shù)組,這將原來的O(N)復(fù)雜度優(yōu)化到了O(1),且查找過程是無鎖的。

FastURLClassLoader會有以下特征:

特征一:初始化過程不是懶加載,會慢一些

索引是在構(gòu)造函數(shù)里進(jìn)行初始化的,如果url都是本地文件(目錄或Jar包),這個過程不會暫用過多的時間,3000+的jar,建索引耗時在0.5秒以內(nèi),內(nèi)部會根據(jù)jar包數(shù)量進(jìn)行多線程并發(fā)建索引。這個耗時,懶加載方式只是將它打散了,實(shí)際并沒有少,而且集團(tuán)大部分應(yīng)用都使用了spring框架,spring啟動過程有各種掃包,第一次掃包,所有URL就都打開了。

特征二:目前只支持本地文件夾和Jar類型的URL

如果包含其他類型的URL,會直接拋異常。雖然如ftp協(xié)議的URL也是支持遍歷的,但得針對性的去開發(fā),而且ftp有網(wǎng)絡(luò)開銷,可能懶加載更適合,后續(xù)有需要再支持。

特征三:目前不支持通過META-INF/INDEX.LIST引入更多URL

當(dāng)前正式版本支持通過Class-Path引入更多的URL,但還不支持通過META-INF/INDEX.LIST來引入,目前還沒碰用到這個的場景,但可以支持。通過Class-Path引入更多的URL比較常見,比如idea啟動,如果jar太多,會因?yàn)閰?shù)過長而無法啟動,轉(zhuǎn)而選擇使用"JAR manifest"模式啟動。

特征四:索引是初始化過程創(chuàng)建的,除了主動調(diào)用addURL時會更新,其他場景不會更新

比如在classes目錄下,新增文件或者子目錄,將不會更新到索引里。為此,F(xiàn)astURLClassLoader做了一個兜底保護(hù),如果通過索引找不到,會降級逐一到本地目錄類型的URL里找一遍(大部分場景下,目錄類型的URL只有一個),Jar包類型的URL一般不會動態(tài)修改,所以沒找。

6.5 注意事項(xiàng)

索引對內(nèi)存的開銷:索引的是jar包和它目錄和根目錄文件的關(guān)系,所以不是特別大,熱點(diǎn)應(yīng)用A有3000+個jar包,INDEX.LIST的大小是3.2M

同名類的仲裁:tomcat在沒有INDEX.LIST的情況下,同名類使用哪個jar包中的,存在一定不確性,添加索引后,仲裁優(yōu)先級是jar包名稱按字母排序來的,保險(xiǎn)起見,可以對啟動后應(yīng)用加載的類進(jìn)行對比驗(yàn)證。

七、阿里中間件提速

在阿里集團(tuán)的大部分應(yīng)用都是依賴了各種中間件的Java應(yīng)用,通過對核心中間件的集中優(yōu)化,提升了各java應(yīng)用的整體啟動時間,提速8%。

7.1 Dubbo3 啟動優(yōu)化

7.1.1 現(xiàn)狀

Dubbo3 作為阿里巴巴使用最為廣泛的分布式服務(wù)框架,服務(wù)集團(tuán)內(nèi)數(shù)萬個應(yīng)用,它的重要性自然不言而喻;但是隨著業(yè)務(wù)的發(fā)展,應(yīng)用依賴的 Jar 包 和 HSF 服務(wù)也變得越來越多,導(dǎo)致應(yīng)用啟動速度變得越來越慢,接下來我們將看一下 Dubbo3 如何優(yōu)化啟動速度。

7.1.2 Dubbo3 為什么會慢

Dubbo3 作為一個優(yōu)秀的 RPC 服務(wù)框架,當(dāng)然能夠讓用戶能夠進(jìn)行靈活擴(kuò)展,因此 Dubbo3 框架提供各種各樣的擴(kuò)展點(diǎn)一共 200+ 個。

Dubbo3 的擴(kuò)展點(diǎn)機(jī)制有點(diǎn)類似 JAVA 標(biāo)準(zhǔn)的 SPI 機(jī)制,但是 Dubbo3 設(shè)置了 3 個不同的加載路徑,具體的加載路徑如下:

META-INF/dubbo/internal/
META-INF/dubbo/
META-INF/services/

也就是說,一個 SPI 的加載,一個 ClassLoader 就需要掃描這個 ClassLoader 下所有的 Jar 包 3 次。

以 熱點(diǎn)應(yīng)用A為例,總的業(yè)務(wù) Bundle ClassLoader 數(shù)達(dá)到 582 個左右,那么所有的 SPI 加載需要的次數(shù)為: 200(spi) * 3(路徑) * 582(classloader) = 349200次。

可以看到掃描次數(shù)接近 35萬 次! 并且整個過程是串行掃描的,而我們知道 java.lang.ClassLoader#getResources 是一個比較耗時的操作,因此整個 SPI 加載過程耗時是非常久的。

7.1.3 SPI 加載慢的解決方法

由我們前面的分析可以知道,要想減少耗時,第一是需要減少 SPI 掃描的次數(shù),第二是提升并發(fā)度,減少無效等待時間。

第一個減少 SPI 掃描的次數(shù),我們經(jīng)過分析得知,在整個集團(tuán)的業(yè)務(wù)應(yīng)用中,使用到的 SPI 集中在不到 10 個 SPI,因此我們疏理出一個 SPI 列表,在這個 SPI 列表中,默認(rèn)只從 Dubbo3 框架所在 ClassLoader 的限定目錄加載,這樣大大下降了掃描次數(shù),使熱點(diǎn)應(yīng)用A總掃描計(jì)數(shù)下降到不到 2萬 次,占原來的次數(shù) 5% 這樣。

第二個提升了對多個 ClassLoader 掃描的效率,采用并發(fā)線程池的方式來減少等待的時間,具體代碼如下:

CountDownLatch countDownLatch = new CountDownLatch(classLoaders.size());
for (ClassLoader classLoader : classLoaders) {
GlobalResourcesRepository.getGlobalExecutorService().submit(() -> {
resources.put(classLoader, loadResources(fileName, classLoader));
countDownLatch.countDown();
});
}

7.1.4 其他優(yōu)化手段

  • 去除啟動關(guān)鍵鏈路的非必要同步耗時動作,轉(zhuǎn)成異步后臺處理。2、緩存啟動過程中查詢第三方可緩存的結(jié)果,反復(fù)重復(fù)使用。

7.1.5 優(yōu)化結(jié)果

熱點(diǎn)應(yīng)用A啟動時間從 603秒 下降到 220秒,總體時間下降了 383秒 => 603秒 下降到 220秒,總體時間下降了 383秒。

7.2 TairClient 啟動優(yōu)化

背景介紹:1、tair:阿里巴巴內(nèi)部的緩存服務(wù),類似于公有云的redis;2、diamond:阿里巴巴內(nèi)部配置中心,目前已經(jīng)升級成MSE,和公有云一樣的中間件產(chǎn)品

7.2.1 現(xiàn)狀

目前中臺基礎(chǔ)服務(wù)使用的tair集群均使用獨(dú)立集群,獨(dú)立集群中使用多個NS(命名空間)來區(qū)分不同的業(yè)務(wù)域,同時部分小的業(yè)務(wù)也會和其他業(yè)務(wù)共享一個公共集群內(nèi)單個NS。

早期tair的集群是通過configID進(jìn)行初始化,后來為了容災(zāi)及設(shè)計(jì)上的考慮,調(diào)整為使用username進(jìn)行初始化訪問,但username內(nèi)部還是會使用configid來確定需要鏈接的集群。整個tair初始化過程中讀取的diamond配置的流程如下:

根據(jù)userName獲取配置信息,從配置信息中可以獲得TairConfigId信息,用于標(biāo)識所在集群

  • dataid:ocs.userinfo.{username}
  • group :   DEFAULT_GROUP

根據(jù)ConfigId信息,獲取當(dāng)前tair的路由規(guī)則,規(guī)定某一個機(jī)房會訪問的集群信息。

  • dataId:  {tairConfigId}
  • group : {tairConfigId}.TGROUP

通過該配置可以確定當(dāng)前機(jī)房會訪問的目標(biāo)集群配置,以機(jī)房A為例,對應(yīng)的配置集群tair.mdb.mc.XXX.機(jī)房A

獲取對應(yīng)集群的信息,確定tair集群的cs列表

  • dataid:{tairConfigId}   // tair.mdb.mc.uic
  • group :    {tairClusterConfig}  // tair.mdb.mc.uic.機(jī)房A

從上面的分析來看,在每次初始化的過程中,都會訪問相同的diamond配置,在初始化多個同集群的namespace的時候,部分關(guān)鍵配置就會多次訪問。但實(shí)際這部分diamond配置的數(shù)據(jù)本身是完全一致。

由于diamond本身為了保護(hù)自身的穩(wěn)定性,在客戶端對訪問單個配置的頻率做了控制,超過一定的頻率會進(jìn)入等待超時階段,這一部分導(dǎo)致了應(yīng)用的啟動延遲。

在一分鐘的時間窗口內(nèi),限制單個diamond配置的訪問次數(shù)低于-DlimitTime配置,默認(rèn)配置為5,對于超過限制的配置會進(jìn)入等待狀態(tài)。

7.2.2 優(yōu)化方案

tair客戶端進(jìn)行改造,啟動過程中,對Diamond的配置數(shù)據(jù)做緩存,配置監(jiān)聽器維護(hù)緩存的數(shù)據(jù)一致性,tair客戶端啟動時,優(yōu)先從緩存中獲取配置,當(dāng)緩存獲取不到時,再重新配置Diamond配置監(jiān)聽及獲取Diamond配置信息。

7.3 SwitchCenter 啟動優(yōu)化

背景介紹:SwitchCenter:阿里巴巴集團(tuán)內(nèi)部的開關(guān)平臺,對應(yīng)阿里云AHAS云產(chǎn)品[8]

7.3.1 現(xiàn)狀

All methods add synchronized made this class to be thread safe. switch op is not frequent, so don't care about performance here.

這是switch源碼里存放各個switch bean 的SwitchContainer中的注釋,可見當(dāng)時的作者認(rèn)為switch bean只需初始化一次,本身對性能的影響不大。但沒有預(yù)料到隨著業(yè)務(wù)的增長,switch bean的初始化可能會成為應(yīng)用啟動的瓶頸。

業(yè)務(wù)平臺的定位導(dǎo)致了平臺啟動期間有大量業(yè)務(wù)容器初始化,由于switch中間件的大部分方法全部被synchronized修飾,因此所有應(yīng)用容器初始化到了加載開關(guān)配置時(入口為com.taobao.csp.switchcenter.core.SwitchManager#init())就需要串行執(zhí)行,嚴(yán)重影響啟動速度。

7.3.2 解決方案

去除了關(guān)鍵路徑上的所有鎖。

7.3.3 原理

本次升級將存放配置的核心數(shù)據(jù)結(jié)構(gòu)修改為了ConcurrentMap,并基于putIfAbsent等 j.u.c API 做了小重構(gòu)。值得關(guān)注的是修改后原先串行的對diamond配置的獲取變成了并行,觸發(fā)了diamond服務(wù)端限流,在大量獲取相同開關(guān)配置的情況下有很大概率拋異常啟動失敗。

(如圖: 去鎖后,配置獲取的總次數(shù)不變,但是請求速率變快)

為了避免上述問題:

  • 在本地緩存switch配置的獲取
  • diamond監(jiān)聽switch配置的變更,確保即使switch配置被更新,本地的緩存依然是最新的

7.4 TDDL啟動優(yōu)化

背景介紹:TDDL:基于 Java 語言的分布式數(shù)據(jù)庫系統(tǒng),核心能力包括:分庫分表、透明讀寫分離、數(shù)據(jù)存儲平滑擴(kuò)容、成熟的管控系統(tǒng)。

7.4.1 現(xiàn)狀

TDDL在啟動過程,隨著分庫分表規(guī)則的增加,啟動耗時呈線性上漲趨勢,在國際化多站點(diǎn)的場景下,耗時增長會特別明顯,未優(yōu)化前,我們一個核心應(yīng)用TDDL啟動耗時為120秒+(6個庫),單個庫啟動耗時20秒+,且通過多個庫并行啟動,無法有效降低耗時。

7.4.2 解決方案

通過工具分析,發(fā)現(xiàn)將分庫分表規(guī)則轉(zhuǎn)成groovy腳本,并生成groovy的class,這塊邏輯總耗時非常久,調(diào)用次數(shù)非常多,且groovy在parseClass里頭有加鎖(所以并行無效果)。調(diào)用次數(shù)多,是因?yàn)樯蒫lass的個數(shù),會剩以物理表的數(shù)量,比如配置里只有一個邏輯表 + 一個規(guī)則(不同表的規(guī)則也存在大量重復(fù)),分成1024張物理表,實(shí)際啟動時會產(chǎn)生1024個規(guī)則類,存在大量的重復(fù),不僅啟動慢,還浪費(fèi)了很多metaspace。

優(yōu)化方案是新增一個全局的GuavaCache,將規(guī)則和生成的規(guī)則類實(shí)例存放進(jìn)去,避免相同的規(guī)則去創(chuàng)建不同的類和實(shí)例。

八、其他提速

除了前面幾篇文章提到的優(yōu)化點(diǎn)(ClassLoader優(yōu)化、中間件優(yōu)化等)以外,我們還對中臺核心應(yīng)用做了其他啟動優(yōu)化的工作。

8.1 aspectj相關(guān)優(yōu)化

8.1.1 現(xiàn)狀

在進(jìn)行啟動耗時診斷的時候,意外發(fā)現(xiàn)aspectj耗時特別久,達(dá)到了54秒多,不可接受。

通過定位發(fā)現(xiàn),如果應(yīng)用里有使用到通過注解來判斷是否添加切面的規(guī)則,aspectj的耗時就會特別久。

以下是熱點(diǎn)應(yīng)用A中的例子:

8.1.2 解決方案

將aspectj相關(guān)jar包版本升級到1.9.0及以上,熱點(diǎn)應(yīng)用A升級后,aspectj耗時從54.5秒降到了6.3秒,提速48秒多。

另外,需要被aspectj識別的annotation,RetentionPolicy需要是RUNTIME,不然會很慢。

8.1.3 原理

通過工具采集到老版本的aspectj在判斷一個bean的method上是否有annotation時的代碼堆棧,發(fā)現(xiàn)它去jar包里讀取class文件并解析類信息,耗時耗在類搜索和解析上。當(dāng)看到這個的時候,第一反應(yīng)就是,java.lang,Method不是有g(shù)etAnnotation方法么,為什么要繞一圈自己去從jar包里解析出來。不太理解,就嘗試去看看最新版本的aspectj這塊是否有改動,最終發(fā)現(xiàn)升級即可解決。

aspectj去class原始文件中讀取的原因是annotation的RetentionPolicy如果不是RUNTIME的話,運(yùn)行時是獲取不到的,詳見:java.lang.annotation.RetentionPolicy的注釋

8.8.8版本在判斷是否有注解的邏輯:

8.9.8版本在判斷是否有注解的邏輯:與老版本的差異在于會判斷annotation的RetentionPolicy是不是RUNTIME的,是的話,就直接從Method里獲取了。

老版本aspectj的相關(guān)執(zhí)行堆棧:(格式:時間|類名|方法名|行數(shù))

8.2 tbbpm相關(guān)優(yōu)化(javassist & javac)

8.2.1 現(xiàn)狀

中臺大部分應(yīng)用都使用tbbpm流程引擎,該引擎會將流程配置文件編譯成java class來進(jìn)行調(diào)用,以提升性能。tbbpm默認(rèn)是使用com.sun.tools.javac.Main工具來實(shí)現(xiàn)代碼編譯的,通過工具分析,發(fā)現(xiàn)該過程特別耗時,交易應(yīng)用A這塊耗時在57秒多。

8.2.2 解決方案

通過采用javassist來編譯bpm文件,應(yīng)用A預(yù)編譯bpm文件的耗時從57秒多降到了8秒多,快了49秒。

8.2.3 原理

com.sun.tools.javac.Main執(zhí)行編譯時,會把classpath傳進(jìn)去,自行從jar包里讀取類信息進(jìn)行編譯,一樣是慢在類搜索和解析上。而javassist是使用ClassLoader去獲取這些信息,根據(jù)前面的文章“ClassLoader優(yōu)化篇”,我們對ClassLoader加了索引,極大的提升搜索速度,所以會快非常多。

javac編譯相關(guān)執(zhí)行堆棧:(格式:時間|類名|方法名|行數(shù))

九、持續(xù)地...激情

一輛車,可以從直升機(jī)上跳傘,也可以飛馳在冰海上,甚至可以安裝上火箭引擎上太空。上天入地沒有什么不可能,只要有想象,有創(chuàng)新。

我們的研發(fā)基礎(chǔ)設(shè)施與工具還在路上,還在不斷改造的路上,還有很多的速度與激情可以追求。

參考鏈接:

[1]https://github.com/apache/maven-resolver/blob/master/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/bf/BfDependencyCollector.java

[2]https://github.com/JetBrains/intellij-community/blob/1e1f83264bbb4cb7ba3ed08fe0915aa990231611/plugins/maven/maven3-server-impl/src/org/jetbrains/idea/maven/server/Maven3XServerEmbedder.java

[3]https://github.com/moby/moby/issues/1266

[4]https://github.com/moby/buildkit/pull/3053

[5]https://docs.oracle.com/javase/8/docs/technotes/guides/vm/class-data-sharing.html

[6]https://docs.oracle.com/javase/8/docs/technotes/tools/enhancements-8.html

[7]https://openjdk.java.net/jeps/310

[8]https://help.aliyun.com/document_detail/155939.html


本文標(biāo)題:Java應(yīng)用提速(速度與激情)
網(wǎng)頁網(wǎng)址:http://m.5511xx.com/article/djdphpc.html