日本综合一区二区|亚洲中文天堂综合|日韩欧美自拍一区|男女精品天堂一区|欧美自拍第6页亚洲成人精品一区|亚洲黄色天堂一区二区成人|超碰91偷拍第一页|日韩av夜夜嗨中文字幕|久久蜜综合视频官网|精美人妻一区二区三区

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

新聞中心

這里有您想知道的互聯(lián)網(wǎng)營(yí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)目整合能力。我們以讓每一個(gè)夢(mèng)想脫穎而出為使命,1280元灤平做網(wǎng)站,已為上家服務(wù),為灤平各地企業(yè)和個(gè)人服務(wù),聯(lián)系電話:18982081108

一、速度與效率與激情

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

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

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

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

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

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

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

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

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

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

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

2.1 現(xiàn)狀

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

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

2.2 解決方案

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

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

2.3 效果

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

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

2.4 原理

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

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

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

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

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

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

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

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

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

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

2.4.1 依賴樹(shù)

2.4.1.1 依賴樹(shù)緩存

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

2) 本地mirror

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

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

為解決按倉(cāng)庫(kù)隔離.m2,且應(yīng)用依賴多個(gè)倉(cāng)庫(kù)時(shí)的問(wèn)題,我們現(xiàn)在通過(guò)對(duì)amaven的優(yōu)化來(lái)解決。

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

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

通過(guò)repo_mirror可以實(shí)現(xiàn)同一個(gè)構(gòu)建node上只會(huì)下載一次同一個(gè)倉(cāng)庫(kù)的同一個(gè)文件。

2.4.1.4 SNAPSHOT版本號(hào)緩存

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

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

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

2.4.2 增量

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

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

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

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

2.4.3 daemon與分布式

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

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

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

3.1 從盲俠說(shuō)起

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

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

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

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

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

所以本地的maven的性能更需要提升,更需要改造。因?yàn)樗軒?lái)更大的人效。

3.2 解決方案

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

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

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

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

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

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

3.3 原理

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

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

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

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

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

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

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

4.1 背景

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

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

4.2 增強(qiáng)

4.2.1 新語(yǔ)法SYNC

我們先用增量的思想,相對(duì)于COPY增加了一個(gè)新語(yǔ)法SYNC。

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

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

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

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

而用戶要修改的,只是將原來(lái)的COPY語(yǔ)法修改成SYNC就行了。

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

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

我們?cè)賮?lái)看看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平臺(tái)上面用起來(lái),首先要把none-gzip支持起來(lái)。

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

雖然none-gzip后,CPU不會(huì)高,但會(huì)讓上傳下載等傳輸過(guò)程變慢,因?yàn)槲募粔嚎s變大了。但相對(duì)于CPU資源來(lái)說(shuō),內(nèi)網(wǎng)情況下帶寬資源不是瓶頸。只需要在上傳鏡像層時(shí)按配置跳過(guò) 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過(guò)程中,即使是同一個(gè)應(yīng)用的構(gòu)建,也可能會(huì)被調(diào)度到不同的編譯機(jī)上。即使構(gòu)建調(diào)度有一定的親和性。

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

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

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

4.2.4 無(wú)中心P2P下載

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

中心OSS服務(wù)總共只能提供最多20G的帶寬,從歷史拉鏡像數(shù)據(jù)能看到每個(gè)節(jié)點(diǎn)的下載速度都很難超過(guò)30M,但是我們現(xiàn)在每個(gè)節(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 指令來(lái)減少 FROM 它的 Dockerfile中的公共構(gòu)建指令。如果不能解析 schema1 格式的鏡像,這部分應(yīng)用的構(gòu)建雖然會(huì)成功,但是其實(shí)很多應(yīng)該執(zhí)行的指令并沒(méi)有執(zhí)行,對(duì)于這個(gè)能力缺失,我們?cè)趦?nèi)部補(bǔ)上的同時(shí)也把這些修改回饋給了社區(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秒啟動(dòng)時(shí)間

云產(chǎn)品SAE和FC在使用Dragonwell11時(shí)開(kāi)啟CDS、AOT等特性加速啟動(dòng)

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

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

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

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

調(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需要如下步驟:

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

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

5.1.2 解決方案

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

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

5.1.3 原理

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

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

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

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

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

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

為了減少CDS讀取jsa dump的開(kāi)銷,避免將數(shù)據(jù)反序列化到InstanceKlass的開(kāi)銷,jsa文件中的存儲(chǔ)layout和InstanceKlass對(duì)象完全一樣,這樣在使用jsa數(shù)據(jù)時(shí),只需要將jsa文件映射到內(nèi)存,并且讓對(duì)象頭中的類型指針指向這塊內(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對(duì)AppCDS的優(yōu)化

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

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

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

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

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

5.2 啟動(dòng)profiling工具

5.2.1 現(xiàn)狀

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

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

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

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

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

5.2.2 解決方案

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

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

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

5.2.3 原理

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

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

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

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

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

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

六、ClassLoader提速

6.1 現(xiàn)狀

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

6.2 解決方案

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

6.3 提速效果

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

6.4 原理

6.4.1 原生URLClassLoader為什么會(huì)慢

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

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

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

6.4.2 JDK為什么不給URLClassLoader加索引

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

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

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

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

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

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

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

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

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

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

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

FastURLClassLoader會(huì)有以下特征:

特征一:初始化過(guò)程不是懶加載,會(huì)慢一些

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

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

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

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

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

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

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

6.5 注意事項(xiàng)

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

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

七、阿里中間件提速

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

7.1 Dubbo3 啟動(dòng)優(yōu)化

7.1.1 現(xiàn)狀

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

7.1.2 Dubbo3 為什么會(huì)慢

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

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

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

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

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

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

7.1.3 SPI 加載慢的解決方法

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

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

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

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)化手段

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

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

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

7.2 TairClient 啟動(dòng)優(yōu)化

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

7.2.1 現(xiàn)狀

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

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

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

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

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

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

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

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

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

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

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

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

7.2.2 優(yōu)化方案

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

7.3 SwitchCenter 啟動(dòng)優(yōu)化

背景介紹:SwitchCenter:阿里巴巴集團(tuán)內(nèi)部的開(kāi)關(guān)平臺(tái),對(duì)應(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源碼里存放各個(gè)switch bean 的SwitchContainer中的注釋,可見(jiàn)當(dāng)時(shí)的作者認(rèn)為switch bean只需初始化一次,本身對(duì)性能的影響不大。但沒(méi)有預(yù)料到隨著業(yè)務(wù)的增長(zhǎng),switch bean的初始化可能會(huì)成為應(yīng)用啟動(dòng)的瓶頸。

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

7.3.2 解決方案

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

7.3.3 原理

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

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

為了避免上述問(wèn)題:

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

7.4 TDDL啟動(dòng)優(yōu)化

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

7.4.1 現(xiàn)狀

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

7.4.2 解決方案

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

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

八、其他提速

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

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

8.1.1 現(xiàn)狀

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

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

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

8.1.2 解決方案

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

另外,需要被aspectj識(shí)別的annotation,RetentionPolicy需要是RUNTIME,不然會(huì)很慢。

8.1.3 原理

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

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

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

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

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

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

8.2.1 現(xiàn)狀

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

8.2.2 解決方案

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

8.2.3 原理

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

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

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

一輛車(chē),可以從直升機(jī)上跳傘,也可以飛馳在冰海上,甚至可以安裝上火箭引擎上太空。上天入地沒(méi)有什么不可能,只要有想象,有創(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


當(dāng)前文章:Java應(yīng)用提速(速度與激情)
轉(zhuǎn)載注明:http://www.dlmjj.cn/article/djdphpc.html