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

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

新聞中心

這里有您想知道的互聯網營銷解決方案
Java應用提速(速度與激情)

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

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

一、速度與效率與激情

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

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

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

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

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

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

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

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

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

我們將闡述通過基礎設施與工具的改進,實現從構建到啟動全方面大幅提速的實踐和理論,相信能幫助大家。

二、maven構建提速

2.1 現狀

maven其實并不是拖拉機。

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

2.2 解決方案

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

因為amaven完全兼容apache-maven,所支持的命令與參數都兼容,所以對我們研發(fā)同學來說,只要修改一個maven的版本號。

2.3 效果

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

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

2.4 原理

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

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

一次典型的maven構建過程,會是這樣:

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

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

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

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

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

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

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

2.4.1 依賴樹

2.4.1.1 依賴樹緩存

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

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

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

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

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

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

我們來看看maven的仲裁機制,無論是maven2還是maven3,最主要的仲裁原則就是depth。相同ga或相同gav,誰更deeper,誰就skip,當然仲裁的因素還有scope,profile等。結合depth的仲裁機制,按層遍歷(BF)會更優(yōu),也更好理解。如下圖,如按層來遍歷,則紅色的二個D1,D2就會skip掉,不會重復解析。(注意,實際場景是C的D1還是會被解析,因為它更左)。

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

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

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

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

而依賴包的下載,是需要經過網絡,所以當一次編譯,如要下載上千個依賴,那構建耗時大部分是在下載包,即瓶頸是下載。

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

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

下表是對熱點應用A用不同的下載線程數來下載5000多個依賴得到的下載耗時結果比較:

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

同時為了減少網絡開銷,我們還采用了在編譯機本地建立了mirror機制。

2) 本地mirror

有些應用有些復雜,它會在maven構建的倉庫配置文件settings.xml(或pom文件)中指定下載多個倉庫。因為這應用的要下載的依賴的確來自多個倉庫.當指定多個倉庫時,下載一個依賴包,會依次從這多個倉庫查找并下載。

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

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

(架構5.0:repo_mirror)

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

通過repo_mirror可以實現同一個構建node上只會下載一次同一個倉庫的同一個文件。

2.4.1.4 SNAPSHOT版本號緩存

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

我們的應用會依賴一些SNAPSHOT包,同時當我們在mvn構建時加上-U就會去檢測這些SNAPSHOT的更新.而在apache-maven中檢測SNAPSHOT需要多次請求maven倉庫,會有一些網絡開銷。

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

2.4.2 增量

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

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

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

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

2.4.3 daemon與分布式

daemon是為了進一步達到10秒內構建的實現途徑。maven也是java程序,運行時要將字節(jié)碼轉成機器碼,而這轉化有時間開銷。雖這開銷只有幾秒時間,但對一個mvn構建只要15秒的應用來說,所占比例也有10%多。為降低這時間開銷,可以用JIT直接將maven程序編譯成機器碼,同時mvn在構建完成后,常駐進程,當有新構建任務來時,直接調用mvn進程。

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

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

3.1 從盲俠說起

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

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

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

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

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

所以本地的maven的性能更需要提升,更需要改造。因為它能帶來更大的人效。

3.2 解決方案

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

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

  • 重啟idea后,點import project.

最后我們看看效果,對熱點應用進行import project測試,用maven要20分鐘左右,而用amaven3.5.0在3分鐘左右,在命中緩存情況下最佳能到1分鐘內。

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

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

3.3 原理

IDEA是如何調用maven的依賴分析方法的?

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

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

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

IDEA主要用了maven的依賴分析的能力,在 “maven構建提速”這一小節(jié)中, 我們已經講了一些amaven加速的原理,其中依賴算法從DF換到BF,依賴下載優(yōu)化,整個依賴樹緩存,SNAPSHOT緩存這些特性都是與依賴分析過程相關,所以都能用在IDEA提速上,而依賴倉庫mirror等因為在我們自己的本地一般不會刪除.m2,所以不會有所體現。

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

四、docker構建提速

4.1 背景

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

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

4.2 增強

4.2.1 新語法SYNC

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

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

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

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

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

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

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

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

我們再來看看SYNC的效果。集團最核心的熱點應用A切換到moby-buildkit以及我們的sync指令后90分位鏡像構建速度已經從140秒左右降低到80秒左右:

4.2.2 none-gzip實現

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

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

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

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

在CICD過程中,即使是同一個應用的構建,也可能會被調度到不同的編譯機上。即使構建調度有一定的親和性。

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

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

當然實現這層內并發(fā)下載是有前提的,即鏡像的存儲需要支持分段下載。因為我們公司是用了阿里云的OSS來存儲docker鏡像,它支持分段下載或多線程下載。

4.2.4 無中心P2P下載

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

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

4.2.5 鏡像ONBUILD支持

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

五、JDK提速

5.1 AppCDS

5.1.1 現狀

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

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

熱點應用A使用CDS減少了10秒啟動時間

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

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

5.1.1.1 AppCDS在實踐中效果不明顯

jsa中存儲的InstanceKlass是對class文件解析的產物。對于boot classloader(加載jre/lib/rt.jar下面的類的類加載器)和system(app) 類加載器(加載-classpath下面的類的類加載器),CDS有內部機制可以跳過對class文件的讀取,僅僅通過類名在jsa文件中匹配對應的數據結構。

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

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

計算class byte stream的checksum,與jsa中的同類名結構的checksum比較

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

5.1.1.2 工程實踐不友好

使用AppCDS需要如下步驟:

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

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

5.1.2 解決方案

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

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

5.1.3 原理

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

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

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

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

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

CDS可以將這個解析、校驗產生的數據結構存儲(dump)到文件,在下一次運行時重復使用。這個dump產物叫做Shared Archive,以jsa后綴(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|super|methods|
| | +-------------------------+
+-------------+

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

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

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

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

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

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

5.2 啟動profiling工具

5.2.1 現狀

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

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

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

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

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

5.2.2 解決方案

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

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

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

5.2.3 原理

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

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

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

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

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

整個系統(tǒng)的結構如下:

六、ClassLoader提速

6.1 現狀

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

6.2 解決方案

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

6.3 提速效果

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

6.4 原理

6.4.1 原生URLClassLoader為什么會慢

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

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

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

6.4.2 JDK為什么不給URLClassLoader加索引

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

通過仔細閱讀URLClassPath的代碼,JDK沒支持索引的原因有以下3點:

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

建索引就得提前將所有URL打開并遍歷一遍,這與原先的按需加載設計相矛盾。另外,URL的加載有2個不確定性:一是可能是非本地文件,需要從網絡上下載jar包,下載可能快,可能慢,也可能會失?。欢荱RL的加載可能會引入新的URL,新的URL又可能會引入新的URL。

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

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

原因三:URL里的內容可能在運行時被修改

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

6.4.3 FastURLClassLoader如何進行提速

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

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

FastURLClassLoader會有以下特征:

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

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

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

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

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

當前正式版本支持通過Class-Path引入更多的URL,但還不支持通過META-INF/INDEX.LIST來引入,目前還沒碰用到這個的場景,但可以支持。通過Class-Path引入更多的URL比較常見,比如idea啟動,如果jar太多,會因為參數過長而無法啟動,轉而選擇使用"JAR manifest"模式啟動。

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

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

6.5 注意事項

索引對內存的開銷:索引的是jar包和它目錄和根目錄文件的關系,所以不是特別大,熱點應用A有3000+個jar包,INDEX.LIST的大小是3.2M

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

七、阿里中間件提速

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

7.1 Dubbo3 啟動優(yōu)化

7.1.1 現狀

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

7.1.2 Dubbo3 為什么會慢

Dubbo3 作為一個優(yōu)秀的 RPC 服務框架,當然能夠讓用戶能夠進行靈活擴展,因此 Dubbo3 框架提供各種各樣的擴展點一共 200+ 個。

Dubbo3 的擴展點機制有點類似 JAVA 標準的 SPI 機制,但是 Dubbo3 設置了 3 個不同的加載路徑,具體的加載路徑如下:

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

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

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

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

7.1.3 SPI 加載慢的解決方法

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

第一個減少 SPI 掃描的次數,我們經過分析得知,在整個集團的業(yè)務應用中,使用到的 SPI 集中在不到 10 個 SPI,因此我們疏理出一個 SPI 列表,在這個 SPI 列表中,默認只從 Dubbo3 框架所在 ClassLoader 的限定目錄加載,這樣大大下降了掃描次數,使熱點應用A總掃描計數下降到不到 2萬 次,占原來的次數 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)化手段

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

7.1.5 優(yōu)化結果

熱點應用A啟動時間從 603秒 下降到 220秒,總體時間下降了 383秒 => 603秒 下降到 220秒,總體時間下降了 383秒。

7.2 TairClient 啟動優(yōu)化

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

7.2.1 現狀

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

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

根據userName獲取配置信息,從配置信息中可以獲得TairConfigId信息,用于標識所在集群

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

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

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

通過該配置可以確定當前機房會訪問的目標集群配置,以機房A為例,對應的配置集群tair.mdb.mc.XXX.機房A

獲取對應集群的信息,確定tair集群的cs列表

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

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

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

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

7.2.2 優(yōu)化方案

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

7.3 SwitchCenter 啟動優(yōu)化

背景介紹:SwitchCenter:阿里巴巴集團內部的開關平臺,對應阿里云AHAS云產品[8]

7.3.1 現狀

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中的注釋,可見當時的作者認為switch bean只需初始化一次,本身對性能的影響不大。但沒有預料到隨著業(yè)務的增長,switch bean的初始化可能會成為應用啟動的瓶頸。

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

7.3.2 解決方案

去除了關鍵路徑上的所有鎖。

7.3.3 原理

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

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

為了避免上述問題:

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

7.4 TDDL啟動優(yōu)化

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

7.4.1 現狀

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

7.4.2 解決方案

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

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

八、其他提速

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

8.1 aspectj相關優(yōu)化

8.1.1 現狀

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

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

以下是熱點應用A中的例子:

8.1.2 解決方案

將aspectj相關jar包版本升級到1.9.0及以上,熱點應用A升級后,aspectj耗時從54.5秒降到了6.3秒,提速48秒多。

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

8.1.3 原理

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

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

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

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

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

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

8.2.1 現狀

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

8.2.2 解決方案

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

8.2.3 原理

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

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

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

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

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

參考鏈接:

[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


網頁名稱:Java應用提速(速度與激情)
文章地址:http://www.5511xx.com/article/djdphpc.html