一、速度與效率與激情
什么是速度?速度就是快 , 快有很多種 。
有小李飛刀的快,也有閃電俠的快,當然還有周星星的快:(船家)”我是出了名夠快” 。(周星星)“這船好像在下沉?” (船家)“是呀!沉得快嘛” 。
并不是任何事情越快越好,而是那些有價值有意義的事才越快越好 。對于這些越快越好的事來說,快的表現是速度 , 而實質上是提效 。今天我們要講的java應用的研發效率,即如何加快我們的java研發速度,提高我們的研發效率 。
提效的方式也有很多種 。但可以分成二大類 。
我們使用一些工具與平臺進行應用研發與交付 。當一小部分低效應用的用戶找工具與平臺負責人時 , 負責人建議提效的方案是:你看看其他應用都這么快,說明我們平臺沒問題 。可能是你們的應用架構的問題,也可能是你們的應用中祖傳代碼太多了,要自己好好重構下 。這是大家最常見的第一類提效方式 。
而今天我們要講的是第二類,是從工具與平臺方面進行升級 。即通過基礎研發設施與工具的微創新改進,實現研發提效,而用戶要做的可能就是換個工具的版本號 。
買了一輛再好的車 , 帶來的只是速度 。而自己不斷研究與改造發動機,讓車子越來越快 , 在帶來不斷突破的“速度”的同時還帶來了“激情” 。因為這是一個不斷用自己雙手創造奇跡的過程 。
所以我們今天要講的不是買一輛好車,而是講如何改造“發動機” 。
在阿里集團,有上萬多個應用,大部分應用都是java應用,95%應用的構建編譯時間是5分鐘以上,鏡像構建時間是2分鐘以上,啟動時間是8分鐘以上,這樣意味著研發同學的一次改動,大部分需要等待15分鐘左右,才能進行業務驗證 。而且隨著業務迭代和時間的推移 , 應用的整體編譯構建、啟動速度也越來越慢,發布、擴容、混部拉起等等一系列動作都被拖慢 , 極大的影響了研發和運維整體效能 , 應用提速刻不容緩 。
我們將闡述通過基礎設施與工具的改進,實現從構建到啟動全方面大幅提速的實踐和理論,相信能幫助大家 。
二、maven構建提速2.1 現狀
maven其實并不是拖拉機 。
相對于ant時代來說,maven是一輛大奔 。但隨著業務越來越復雜,我們為業務提供服務的軟件也越來越復雜 。雖然我們在提倡要降低軟件復雜度,但對于復雜的業務來說,降低了復雜度的軟件還是復雜的 。而maven卻還是幾年的版本 。在2012年推出.0.0以來 , 直到現在的2022年,正好十年,但maven最新版本還是3系列3.8.6 。所以在十年后的今天,站在復雜軟件面前 , maven變成了一輛拖拉機 。
2.2 解決方案
在這十年,雖然maven還是停留在主版本號是3 , 但當今業界也不斷出現了優秀的構建工具,如,bazel 。但因各工具的生態不同 , 同時工具間遷移有成本與風險,所以目前在java服務端應用仍是以maven構建為主 。所以我們在-maven的基礎上,參照,bazel等其它工具的思路,進行了優化,并以“”命名 。
因為完全兼容-maven,所支持的命令與參數都兼容,所以對我們研發同學來說 , 只要修改一個maven的版本號 。
2.3 效果
從目前試驗來看,對于mvn build耗時在3分鐘以上的應用有效果 。對于典型應用從2325秒降到188秒,提升了10倍多 。
我們再來看持續了一個時間段后的總體效果,典型應用使用后 , 構建耗時p95的時間有較明顯下降,對比使用前后二個月的構建耗時降了50%左右 。
2.4 原理
如果說發動機是一輛車的靈魂,那依賴管理就是maven的靈魂 。
因為maven就是為了系統化的管理依賴而產生的工具 。使用過maven的同學都清楚,我們將依賴寫在pom.xml中,而這依賴又定義了自己的依賴在自己的pom.xml 。通過pom文件的層次化來管理依賴的確讓我們方便很多 。
一次典型的maven構建過程 , 會是這樣:
從上圖可以看出,maven構建主要有二個階段,而第一階段是第二階段的基?。??舊洗蟛糠值牟寮?薊崾褂玫諞喚錐尾??囊覽凳鰨?
解析應用的pom及依賴的pom,生成依賴樹;在解析過程中,一般還會從maven倉庫下載新增的依賴或更新了的包 。執行各maven插件 。
我們也通過分析實際的構建日志 , 發現大于3分鐘的maven構建,瓶頸都在“生成依賴樹”階段 。而“生成依賴樹”階段慢的根本原因是一個配置的依賴太多太復雜,它表現為:依賴太多,則要從maven倉庫下載的可能性越大 。依賴太復雜,則依賴樹解析過程中遞歸次數越多 。
在中通過優化依賴分析算法,與提升下載依賴速度來提升依賴分析的性能 。除此之外,性能優化的經典思想是緩存增量,與分布式并發,我們也遵循這個思想作了優化 。
在不斷優化過程中,也不斷地C/S化了,即不再是一個,而有了端 , 同時將部分復雜的計算從端移到了端 。而當越做越薄 , 端的功能越來越強大時,的計算所需要的資源也會越來越多,將這些資源用彈性伸縮來解決,慢慢地云化了 。
從單個到C/S化再到云化,這也是一個工具不斷進化的趨勢所在 。
2.4.1 依賴樹
2.4.1.1 依賴樹緩存
既然依賴樹生成慢,那我們就將這依賴樹緩存起來 。緩存后 , 這依賴樹可以不用重復生成 , 而且可以不同人,不同的機器的編譯進行共享 。使用依賴樹緩存后,一次典型的mvn構建的過程如下:
從上圖中可以看到- , 它主要負責依賴樹緩存的讀寫性能,保障存儲可靠性 , 及保證緩存的正確性等 。
2.4.1.2 依賴樹生成算法優化
雖在日常研發過程中 , 修改pom文件的概率較修改應用java低,但還是有一定概率;同時當pom中依賴了較多且有更新時,依賴樹緩存會失效掉 。所以還是會有不少的依賴樹重新生成的場景 。所以還是有必要來優化依賴樹生成算法 。
在,及版本中,包括最新的.8.5中,maven是以深度優先遍歷(DF)來生成依賴樹的(在社區版本中,目前上已經支持BF,但還未發版本[1] 。在遍歷過程中通過debug與打日志發現有很多相同的gav或相同的ga會被重復分析很多次,甚至數萬次 。
樹的經典遍歷算法主要有二種:深度優先算法(DF)及 廣度優先算法(BF),BF與DF的效率其實差不多的 , 但當結合maven的版本仲裁機制考慮會發現有些差異 。
我們來看看maven的仲裁機制,無論是還是,最主要的仲裁原則就是depth 。相同ga或相同gav,誰更,誰就skip,當然仲裁的因素還有scope,等 。結合depth的仲裁機制 , 按層遍歷(BF)會更優,也更好理解 。如下圖,如按層來遍歷,則紅色的二個D1,D2就會skip掉,不會重復解析 。(注意,實際場景是C的D1還是會被解析,因為它更左) 。
算法優化的思路是:“提前修枝” 。之前的邏輯是先生成依賴樹再版本仲裁,而優化后是邊生成依賴樹邊仲裁 。就好比一個樹苗,要邊生長邊修枝,而如果等它長成了參天大樹后則修枝成本更大 。
2.4.1.3 依賴下載優化
maven在編譯過程中,會解析pom,然后不斷下載直接依賴與間接依賴到本地 。一般本地目錄是.m2 。對一線研發來說 , 本地的.m2不太會去刪除,所以除非有大的重構,每次編譯只有少量的依賴會下載 。
但對于CICD平臺來說,因為編譯機一般不是獨占的,而是多應用間共享的讀寫分離 oracle 應用層,所以為了應用間不相互影響,每次編譯后可能會刪除掉.m2目錄 。這樣,在CICD平臺要考慮.m2的隔離 , 及當.m2清理后要下載大量依賴包的場景 。
而依賴包的下載,是需要經過網絡,所以當一次編譯,如要下載上千個依賴,那構建耗時大部分是在下載包,即瓶頸是下載 。
1) 增大下載并發數
依賴包是從maven倉庫下載 。.5.0在編譯時默認是啟了5個線程下載 。我們可以通過..basic.來設置更多的線程如20個來下載,但這要求maven倉庫要能撐得住翻倍的并發流量 。所以我們對maven倉庫進行了架構升級,根據包不同的文件大小區間使用了本地硬盤緩存,redis緩存等包文件多級存儲來加快包的下載 。
下表是對熱點應用A用不同的下載線程數來下載5000多個依賴得到的下載耗時結果比較:
在中我們加了對下載耗時的統計報告 , 包括下載多少個依賴,下載線程是多少,下載耗時是多少,方便大家進行性能分析 。如下圖:
同時為了減少網絡開銷,我們還采用了在編譯機本地建立了機制 。
2) 本地
有些應用有些復雜,它會在maven構建的倉庫配置文件.xml(或pom文件)中指定下載多個倉庫 。因為這應用的要下載的依賴的確來自多個倉庫.當指定多個倉庫時,下載一個依賴包,會依次從這多個倉庫查找并下載 。
雖然maven的.xml語法支持多個倉庫,但卻只能指定一個 。所以要看下是否支持將多個目錄到同一個容器中的目錄,但初步看了官網文檔,并不支持 。
為解決按倉庫隔離.m2,且應用依賴多個倉庫時的問題,我們現在通過對的優化來解決 。
(架構5.0:)
當執行mvn build時,當一個依賴包不在本地.m2目錄,而要下載時,會先到中對應的倉庫中找,如找到,則從中對應的倉庫中將包直接復制到.m2,否則就只能到遠程倉庫下載,下載到.m2后,會同時將包復制到中對應的倉庫中 。
通過可以實現同一個構建node上只會下載一次同一個倉庫的同一個文件 。
2.4.1.4 版本號緩存
其實在的緩存中,除了依賴樹,還緩存了的版本號 。
我們的應用會依賴一些包,同時當我們在mvn構建時加上-U就會去檢測這些的更新.而在-maven中檢測需要多次請求maven倉庫,會有一些網絡開銷 。
現在我們結合maven倉庫作了優化,從而讓多次請求maven倉庫,換成了一次cache服務直接拿到的最新版本 。
2.4.2 增量
增量是與緩存息息相關的 , 增量的實現就是用緩存 。maven的開放性是通過插件機制實現的,每個插件實現具體的功能,是一個函數 。當輸入不變,則輸出不變,即復用輸出,而將每次每個函數執行后的輸出緩存起來 。
上面講的依賴樹緩存,也是maven本身(非插件)的一種增量方式 。
要實現增量的關鍵是定義好一個函數的輸入與輸出 , 即要保證定義好的輸入不變時,定義好的輸出肯定不變 。每個插件自己是清楚輸入與輸出是什么的,所以插件的增量不是由統一實現,而是提供了一個機制 。如一個插件按約定定義好了輸入與輸出 , 則在執行前會檢測輸入是否變化,如沒變化,則直接跳過插件的執行 , 而從緩存中取到輸出結果 。
增量的效果是明顯的,如依賴樹緩存與算法的優化能讓maven構建從10分鐘降到2分鐘,那增量則可以將構建耗時從分鐘級降到秒級 。
2.4.3 與分布式
是為了進一步達到10秒內構建的實現途徑 。maven也是java程序 , 運行時要將字節碼轉成機器碼,而這轉化有時間開銷 。雖這開銷只有幾秒時間,但對一個mvn構建只要15秒的應用來說,所占比例也有10%多 。為降低這時間開銷,可以用JIT直接將maven程序編譯成機器碼,同時mvn在構建完成后,常駐進程 , 當有新構建任務來時,直接調用mvn進程 。
一般,一個maven應用編譯不會超過10分鐘,所以,看上去沒必要將構建任務拆成子任務,再調度到不同的機器上執行分布式構建 。因為分布式調度有時間開銷,這開銷可能比直接在本機上編譯耗時更大,即得不償失 。所以分布式構建的使用場景是大庫 。為了簡化版本管理,將二進制依賴轉成源碼依賴,將依賴較密切的源碼放在一個代碼倉庫中,就是大庫 。當一個大庫有成千上萬個時,則非用分布式構建不可了 。使用分布式構建 , 可以將大庫幾個小時的構建降到幾分鐘級別 。
三、本地idea環境提速3.1 從盲俠說起
曾經有有一位盲人叫座頭市,他雙目失明,但卻是一位頂尖的劍客,江湖上稱他為“盲俠” 。
在我們的一線研發同學中,也有不少盲俠 。
這些同學在本地進行寫代碼時,是盲寫 。他們寫的代碼盡管全都顯示紅色警示 , 寫的單測盡管在本地沒跑過,但還是照寫不誤 。
我們一般的開發流程是,接到一個需求 , 從主干拉一個分支,再將本地的代碼切到這新分支 , 再刷新IDEA 。但有些分支在刷新后,盡管等了30分鐘,盡管自己電腦的CPU沙沙直響 , 熱的冒泡,但IDEA的工作區還是有很多紅線 。這些紅線逼我們不少同學走上了“盲俠”之路 。
一個maven工程的java應用,IDEA的導入也是使用了maven的依賴分析 。而我們分析與實際觀測,一個需求的開發,即在一個分支上的開發,在本地使用maven的次數絕對比在CICD平臺上使用的次數多 。
所以本地的maven的性能更需要提升,更需要改造 。因為它能帶來更大的人效 。
3.2 解決方案
要結合在本地的IDEA中使用也很方便 。
1.下載最新版本 。
2.在本地解壓 , 如目錄 /Users//soft/-3.5.0 。
3.設置Maven home path:
4.重啟idea后,點 .
最后我們看看效果,對熱點應用進行 測試,用maven要20分鐘左右 , 而用.5.0在3分鐘左右,在命中緩存情況下最佳能到1分鐘內 。
簡單四步后 , 我們就不用再當“盲俠”了 , 在本地可以流暢地編碼與跑單元測試 。
除了在IDEA中使用的依賴分析能力外,在本地通過命令行來運行mvn 或:tree,也完全兼容-maven的 。
3.3 原理
IDEA是如何調用maven的依賴分析方法的?
在IDEA的源碼文件[2]中979行,調用了.()方法:
就是通過maven home path指定的maven目錄中的.java 。
而.()方法就是依賴分析的入口 。
IDEA主要用了maven的依賴分析的能力 , 在 “maven構建提速”這一小節中,我們已經講了一些加速的原理,其中依賴算法從DF換到BF,依賴下載優化,整個依賴樹緩存,緩存這些特性都是與依賴分析過程相關 , 所以都能用在IDEA提速上,而依賴倉庫等因為在我們自己的本地一般不會刪除.m2,所以不會有所體現 。
可以在本地結合IDEA使用 , 也可以在CICD平臺中使用,只是它們調用maven的方法的方式不同或入口不同而已 。但對于maven協議來說“靈魂”的還是依賴管理與依賴分析 。
四、構建提速4.1 背景
自從阿里巴巴集團容器化后,開發人員經常被鏡像構建速度困擾,每天要發布很多次的應用體感尤其不好 。我們幾年前已經按最佳實踐推薦每個應用要把鏡像拆分成基礎鏡像和應用鏡像,但是高頻修改的應用鏡像的構建速度依然不盡如人意 。
為了跟上主流技術的發展,我們計劃把CICD平臺的構建工具升級到moby-,的最新版本也計劃把構建切換到moby- 了,這個也是業界的趨勢 。同時在 基礎上我們作了一些增強 。
4.2 增強
4.2.1 新語法SYNC
我們先用增量的思想,相對于COPY增加了一個新語法SYNC 。
我們分析java應用高頻構建部分的鏡像構建場景,高頻情況下只會執行中的一個指令:
COPY .tgz /home///.tgz
發現大多數情況下java應用每次構建雖然會生成一個新的app.war目錄,但是里面的大部分jar文件都是從maven等倉庫下載的 , 它們的創建和修改時間雖然會變化但是內容的都是沒有變化的 。對于一個1G大小的war,每次發布變化的文件平均也就三十多個,大小加起來2-3 M,但是由于這個.war目錄是全新生成的 , 這個copy指令每次都需要全新執行,如果全部拷貝 , 對于稍微大點的應用這一層就占有1G大小的空間,鏡像的copy push pull都需要處理很多重復的內容,消耗無謂的時間和空間 。
如果我們能做到定制中的copy指令,拷貝時像Linux上面的rsync一樣只做增量copy的話 , 構建速度、上傳速度、增量下載速度、存儲空間都能得到很好的優化 。因為moby-的代碼架構分層比較好 , 我們基于前端定制了內部的SYNC指令 。我們掃描到SYNC語法時,會在前端生成原生的兩個指令,一個是從基線鏡像中link 拷貝原來那個目錄(COPY),另一個是把兩個目錄做比較(DIFF) , 把有變化的文件和刪除的文件在新的一層上面生效,這樣在基線沒有變化的情況下,就做到了高頻構建每次只拷貝上傳下載幾十個文件僅幾兆內容的這一層 。
而用戶要修改的,只是將原來的COPY語法修改成SYNC就行了 。
如將:
COPY .tgz /home/admin///.tgz
修改為:
SYNC .dir /home/admin///.war
我們再來看看SYNC的效果 。集團最核心的熱點應用A切換到moby-以及我們的sync指令后90分位鏡像構建速度已經從140秒左右降低到80秒左右:
4.2.2 none-gzip實現
為了讓moby- 能在CICD平臺上面用起來,首先要把none-gzip支持起來 。
這個需求在社區也有很多討論[3],內部環境網絡速度不是問題,如果有gzip會導致90%的時間都花在壓縮和解壓縮上面 , 構建和下載時間會加倍,發布環境拉鏡像的時候主機上一些CPU也會被gzip解壓打滿,影響同主機其它容器的運行 。
雖然none-gzip后 , CPU不會高 , 但會讓上傳下載等傳輸過程變慢,因為文件不壓縮變大了 。但相對于CPU資源來說,內網情況下帶寬資源不是瓶頸 。只需要在上傳鏡像層時按配置跳過 gzip 邏輯去掉,并把鏡像層的從 /vnd..image..diff.tar.gzip 改成/vnd..image..diff.tar 就可以在內網環境下充分提速了 。
4.2.3 單層內并發下載
在CICD過程中 , 即使是同一個應用的構建,也可能會被調度到不同的編譯機上 。即使構建調度有一定的親和性 。
為了讓新構建機,或應用換構建機后能快速拉取到基礎鏡像,由于我們以前的最佳實踐是要求用戶把鏡像分成兩個(基礎鏡像與應用鏡像) , 而基礎鏡像一般單層就有超過1G大小的,多層并發拉取對于單層特別大的鏡像已經沒有效果 。
所以我們在“層間并發拉取”的基礎上 , 還增加了“層內并發拉取”,讓拉鏡像的速度提升了4倍左右 。
當然實現這層內并發下載是有前提的,即鏡像的存儲需要支持分段下載 。因為我們公司是用了阿里云的OSS來存儲鏡像,它支持分段下載或多線程下載 。
4.2.4 無中心P2P下載
現在都是用中的 store來存儲鏡像原始數據,也就是說每個節點本身就存儲了一個鏡像的所有原始數據和 。所以如果多個相鄰的節點,都需要拉鏡像的話 , 可以先看到中心目錄服務器上查看鄰居節點上面是否已經有這個鏡像了 , 如果有的話就可以直接從鄰居節點拉這個鏡像 。而不需要走鏡像倉庫去取鏡像layer,而數據還必須從倉庫獲取是為了防止鏡像名對應的數據已經發生了變化了,只要取到后其它的layer數據都可以從相鄰的節點獲取,每個節點可以只在每一層下載后的五分鐘內(時間可配置)提供共享服務,這樣大概率還能用到本地page cache,而不用真正讀磁盤 。
中心OSS服務總共只能提供最多20G的帶寬,從歷史拉鏡像數據能看到每個節點的下載速度都很難超過30M,但是我們現在每個節點都是50G網絡,節點相互之間共享鏡像層數據可以充分利用到節點本地的50G網絡帶寬 , 當然為了不影響其它服務,我們把鏡像共享的帶寬控制在200M以下 。
4.2.5 鏡像支持
社區的 moby- 已經支持了新的格式的鏡像的了 , 但是集團內部還有很多應用 FROM 的基礎鏡像是格式的基礎鏡像,這些基礎鏡像中很多都很巧妙的用了一些指令來減少 FROM 它的 中的公共構建指令 。如果不能解析格式的鏡像,這部分應用的構建雖然會成功,但是其實很多應該執行的指令并沒有執行,對于這個能力缺失,我們在內部補上的同時也把這些修改回饋給了社區[4] 。
五、JDK提速5.1
5.1.1 現狀
CDS(Class Data )[5]在 JDK1.5被首次引入 , 在 [6]中引入了,支持JDK以外的類 ,但是作為商業特性提供 。隨后將貢獻給了社區,在JDK10中CDS逐漸完善,也支持了用戶自定義類加載器(又稱 v2[7]) 。
目前CDS在阿里的落地情況:
經過十年的發展,CDS已經發展為一項成熟的技術 。但是很容易令人不解的是CDS不管在阿里的業務還是業界(即便是AWS )都沒能被大規模使用 。關鍵原因有兩個:
5.1.1.1 在實踐中效果不明顯
jsa中存儲的是對class文件解析的產物 。對于boot (加載jre/lib/rt.jar下面的類的類加載器)和(app) 類加載器(加載-下面的類的類加載器) , CDS有內部機制可以跳過對class文件的讀取,僅僅通過類名在jsa文件中匹配對應的數據結構 。
Java語言還提供用戶自定義類加載器( class )的機制,用戶通過自己的 .() 查找類,在為 class loade時加載類是需要經過如下步驟:
調用用戶定義的.(),拿到class byte 計算class byte 的 , 與jsa中的同類名結構的比較如果匹配成功則返回jsa中的,否則繼續使用slow path解析class文件
5.1.1.2 工程實踐不友好
使用需要如下步驟:
針對當前版本在生產環境啟動應用,收集信息基于信息生成jsa(java) dump將jsa文件和應用本身打包在一起,發布到生產環境
由于這種trace-模式的復雜性,在SAE和FC云產品的落地都是通過發布流程的定制以及開發復雜的命令行工具來解決的 。
5.1.2 解決方案
針對上述的問題1,在熱點應用A上CDS配合或者使用編譯器團隊開發的特性(原理見5.1.3.1)都能讓CDS發揮最佳效果 。
經驗證,在熱點應用A已經使用做優化的前提下進一步使用依然可以獲得15秒左右的啟動加速效果 。
5.1.3 原理
面向對象語言將對象(數據)和方法(對象上的操作)綁定到了一起,來提供更強的封裝性和多態 。這些特性都依賴對象頭中的類型信息來實現,Java、語言都是如此 。Java對象在內存中的如下:
mark表示了對象的狀態,包括是否被加鎖、GC年齡等等 。而Klass*指向了描述對象類型的數據結構:
基于這個結構,諸如 o這樣的表達式就可以有足夠的信息判斷了 。要注意的是結構比較復雜,包含了類的所有方法、field等等 , 方法又包含了字節碼等信息 。這個數據結構是通過運行時解析class文件獲得的,為了保證安全性,解析class時還需要校驗字節碼的合法性(非通過javac產生的方法字節碼很容易引起jvm crash) 。
CDS可以將這個解析、校驗產生的數據結構存儲(dump)到文件,在下一次運行時重復使用 。這個dump產物叫做 ,以jsa后綴(java) 。
為了減少CDS讀取jsa dump的開銷 , 避免將數據反序列化到的開銷,jsa文件中的存儲和對象完全一樣,這樣在使用jsa數據時 , 只需要將jsa文件映射到內存,并且讓對象頭中的類型指針指向這塊內存地址即可,十分高效 。
5.1.3.1對的優化
上述 for的加載流程更加復雜的原因是JVM通過(, )二元組來唯一確定一個類 。
提供的解決方法是讓用戶為標識唯一的,加載相同類的在多次運行間保持唯一的 。并且擴展了 ,記錄用戶定義的 字段 , 這樣便可以在運行時通過(, )二元組來迅速定位到 中的類條目 。從而讓 下的類加載能和 class一樣快 。
在常見的微服務下,我們可以看到優化后的將基礎的的加速效果從10%提升到了40% 。
5.2 啟動工具
5.2.1 現狀
目前有很多Java性能剖析工具,但專門用于Java啟動過程分析的還沒有 。不過有些現有的工具,可以間接用于啟動過程分析,由于不是專門的工具,每個都存在這樣那樣的不足 。
比如async-,其強項是適合診斷CPU熱點、墻鐘熱點、內存分配熱點、JVM內鎖爭搶等場景,展現形式是火焰圖 。可以在應用剛剛啟動后,馬上開啟aync-,持續剖析直到應用啟動完成 。async-的CPU熱點和墻鐘熱點能力對于分析啟動過程有很大幫助,可以找到占用CPU較多的方法,進而指導啟動加速的優化 。async-有2個主要缺點,第1個是展現形式較單一,關聯分析能力較弱,比如無法選擇特定時間區間,也無法支持選中多線程場景下的火焰圖聚合等 。第2個是采集的數據種類較少 , 看不到類加載、GC、文件IO、、編譯、VM 等方面的數據,沒法做精細的分析 。
再比如,的火焰圖底層也是利用async-,所以async-存在的問題也無法回避 。
最后我們自然會想到的JDK,簡稱JFR 。AJDK8.5.10+和支持JFR 。JFR是JVM內置的診斷工具,類似飛機上的黑匣子 , 可以低開銷的記錄很多關鍵數據,存儲到特定格式的JFR文件中,用這些數據可以很方便的還原應用啟動過程,從而指導啟動優化 。JFR的缺點是有一定的使用門檻,需要對虛擬機有一定的理解,高級配置也較復雜,同時還需要搭配桌面軟件Java才能解析和閱讀JFR文件 。
面對上述問題,JVM工具團隊進行了深入的思考,并逐步迭代開發出了針對啟動過程分析的技術產品 。
5.2.2 解決方案
1、我們選擇JFR作為應用啟動性能剖析的基礎工具 。JFR開銷低,內建在JDK中無第三方依賴,且數據豐富 。JFR會周期性記錄狀態的線程的棧,可以構建CPU熱點火焰圖 。JFR也記錄了類加載、GC、文件IO、、編譯、VM 、Lock等事件,可以回溯線程的關鍵活動 。對于早期版本JFR可能存在性能問題的特性,我們也支持自動切換到aync-以更低開銷實現相同功能 。
2、為了降低JFR的使用門檻,我們封裝了一個,通過在啟動命令中增加參數,即可快速使用JFR 。我們在中內置了文件收集和上傳功能,打通數據收集、上傳、分析和交互等關鍵環節,實現開箱即用 。
3、我們開發了一個Web版本的分析器(或者平臺),它接收到收集上傳的數據后,便可以直接查看和分析 。我們開發了功能更豐富和易用的火焰圖和線程活動圖 。在類加載和資源文件加載方面我們也做了專門的分析,類似在大量Jar包場景下的Class 開銷大、的在大量jar包場景下開銷大、并發控制不合理導致鎖爭搶線程等待等問題都變得顯而易見,未來還將提供評估開啟CDS(Class Data )以及后可以節省時間的預估能力 。
5.2.3 原理
當在上開源了JDK之后,阿里巴巴也是作為主要的貢獻者,與社區包括等 , 一起將 JFR 移植到了8 。
JFR是內置的低開銷的監控和性能剖析工具 , 它深度集成在了虛擬機各個角落 。JFR由兩個部分組成:第1個部分分布在虛擬機的各個關鍵路徑上,負責捕獲信息;第2個部分是虛擬機內的單獨模塊,負責接收和存儲第1個部分產生的數據 。這些數據通常也叫做事件 。JFR包含160種以上的事件 。JFR的事件包含了很多有用的上下文信息以及時間戳 。比如文件訪問,特定GC階段的發生 , 或者特定GC階段的耗時,相關的關鍵信息都被記錄到事件中 。
盡管JFR事件在他們發生時被創建,但JFR并不會實時的把事件數據存到硬盤上,JFR會將事件數據保存在線程變量緩存中,這些緩存中的數據隨后會被轉移到一個 ring。當 ring 寫滿時 , 才會被一個周期性的線程持久化到磁盤 。
雖然JFR本身比較復雜,但它被設計為低CPU和內存占用 , 總體開銷非常低,大約1%甚至更低 。所以JFR適合用于生產環境,這一點和很多其它工具不同,他們的開銷一般都比JFR大 。
JFR不僅僅用于監控虛擬機自身,它也允許在應用層自定義事件,讓應用程序開發者可以方便的使用JFR的基礎能力 。有些類庫沒有預埋JFR事件,也不方便直接修改源代碼,我們則用機制,在類加載過程中,直接用ASM修改字節碼插入JFR事件記錄的能力 。比如的,為了記錄事件,我們就采用了這個方法 。
整個系統的結構如下:
六、提速6.1 現狀
集團整套電商系統已經運行好多年了,機器上運行的jar包 , 不會因為最近大環境不好而減少,只會逐年遞增,而中臺的幾個核心應用,所有業務都在上面開發,膨脹得更加明顯,比如熱點應用A機器上運行的jar包就有三千多個,jar包中包含的資源文件數量更是達到了上萬級別,通過工具分析,啟動有180秒以上是花在上,占總耗時的1/3以上,其中占比大頭的是的耗時 。不論是還是 , 最終都會調用到,慢主要是慢在資源的檢索上 。現在框架幾乎是每個java必備的 , 各種,各種掃包,雖然極大的方便開發者,但也給應用的啟動帶來不少的負擔 。目前集團有上萬多個Java應用,如果可以進行優化 , 將帶來非常非常可觀的收益 。
6.2 解決方案
優化的方案可以簡單的用一句話概括,就是給的資源查找加索引 。
6.3 提速效果
目前中臺核心應用都已升級,基本都有100秒以上的啟動提速,占總耗時的20~35%,效果非常明顯!
6.4 原理
6.4.1 原生為什么會慢
java的JIT(just in time)即時編譯,想必大家都不陌生,JDK里不僅僅是類的裝載過程按這個思想去設計的 , 類的查找過程也是一樣的 。通過研讀的實現,你會發現以下幾個特性:
初始化的時候,所有的URL都沒有open;
會比更快的返回,因為實際并沒有查找,而是在調用的next() 的時候才會去遍歷查找,而去找了第一個;
URL是在遍歷過程逐個open的,會轉成,放到里(數組結構,決定了順序)和lmap中(Map結構, 防止重復加載);
一個URL可以通過Class-Path引入新的URL(所以 , 理論上是可能存在新URL又引入新的URL , 無限循環的場景);
因為URL和是會在遍歷過程中動態新增,所以#(int index) 里加了兩把鎖;
這些特性就是為了按需加載(懶加載),遍歷的過程是O(N)的復雜度,按順序從頭到尾的遍歷,而且遍歷過程可能會伴隨著URL的打開,和新URL的引入,所以,隨著jar包數量的增多,每次或者的耗時會線性增長,調用次數也會增長(加載的類也變多了),啟動就慢下去了 。慢的另一個次要原因是,(int index)加了兩把鎖 。
6.4.2 JDK為什么不給加索引
跟數據庫查詢一樣,數量多了,加個索引,立桿見效,那為什么里沒加索引 。其實 , 在JDK8里的代碼里面 , 是可以看到索引的蹤影的,通過加“-Dsun.cds.che=true”來打開 , 但是,換各種姿勢嘗試了數次,發現都沒生效,始終是false,通過debug發現JDK啟動的過程會把這個變量從的里移除掉 。另外,最近都在升JDK11,也看了一下它里面的實現,發現這塊代碼直接被刪除的干干凈凈,不見蹤影了 。
通過仔細閱讀的代碼,JDK沒支持索引的原因有以下3點:
原因一:跟按需加載相矛盾,且URL的加載有不確定性
建索引就得提前將所有URL打開并遍歷一遍 , 這與原先的按需加載設計相矛盾 。另外,URL的加載有2個不確定性:一是可能是非本地文件 , 需要從網絡上下載jar包,下載可能快,可能慢,也可能會失敗;二是URL的加載可能會引入新的URL , 新的URL又可能會引入新的URL 。
原因二:不是所有URL都支持遍歷
URL的類型可以歸為3種:1. 本地文件目錄 , 如目錄;2. 本地或者遠程下載下來的jar包;3. 其他URL 。前2種是最基本最常見的,可以進行遍歷的,而第3種是不一定支持遍歷,默認只有一個get接口,傳入確定性的name , 返回有或者沒有 。
原因三:URL里的內容可能在運行時被修改
比如本地文件目錄(目錄)的URL , 就可以在運行時往改目錄下動態添加文件和類,是能加載到的 , 而索引要支持動態更新,這個非常難 。
6.4.3 如何進行提速
首先必須承認,需要支持所有場景都能建索引,這是有點不太現實的 , 所以,設計之初只為滿足絕大部分使用場景能夠提速,我們設計了一個的開關 , 關閉則跟原生是一樣的 。另外,一個java進程里經常會存在非常多的實例,不能將所有實例都開打fast模式,這也是沒有直接在里修改原生的實現,而是新寫了個類的原因 。
繼承了 , 核心是將的實現重寫了,在初始化過程,會將所有的進行初始化 , 并遍歷一遍生成index索引,后續的時候,不是從0開始,而是從index里獲取需要遍歷的數組,這將原來的O(N)復雜度優化到了O(1),且查找過程是無鎖的 。
會有以下特征:
特征一:初始化過程不是懶加載 , 會慢一些
索引是在構造函數里進行初始化的,如果url都是本地文件(目錄或Jar包),這個過程不會暫用過多的時間,3000+的jar,建索引耗時在0.5秒以內 , 內部會根據jar包數量進行多線程并發建索引 。這個耗時,懶加載方式只是將它打散了,實際并沒有少,而且集團大部分應用都使用了框架,啟動過程有各種掃包,第一次掃包 , 所有URL就都打開了 。
特征二:目前只支持本地文件夾和Jar類型的URL
如果包含其他類型的URL,會直接拋異常 。雖然如ftp協議的URL也是支持遍歷的,但得針對性的去開發 , 而且ftp有網絡開銷,可能懶加載更適合讀寫分離 oracle 應用層,后續有需要再支持 。
特征三:目前不支持通過META-INF/INDEX.LIST引入更多URL
當前正式版本支持通過Class-Path引入更多的URL,但還不支持通過META-INF/INDEX.LIST來引入 , 目前還沒碰用到這個的場景,但可以支持 。通過Class-Path引入更多的URL比較常見,比如idea啟動 , 如果jar太多,會因為參數過長而無法啟動 , 轉而選擇使用”JAR “模式啟動 。
特征四:索引是初始化過程創建的,除了主動調用時會更新,其他場景不會更新
比如在目錄下,新增文件或者子目錄,將不會更新到索引里 。為此,做了一個兜底保護 , 如果通過索引找不到,會降級逐一到本地目錄類型的URL里找一遍(大部分場景下 , 目錄類型的URL只有一個),Jar包類型的URL一般不會動態修改 , 所以沒找 。
6.5 注意事項
索引對內存的開銷:索引的是jar包和它目錄和根目錄文件的關系,所以不是特別大,熱點應用A有3000+個jar包,INDEX.LIST的大小是3.2M
同名類的仲裁:在沒有INDEX.LIST的情況下,同名類使用哪個jar包中的,存在一定不確性,添加索引后,仲裁優先級是jar包名稱按字母排序來的,保險起見,可以對啟動后應用加載的類進行對比驗證 。
七、阿里中間件提速
在阿里集團的大部分應用都是依賴了各種中間件的Java應用,通過對核心中間件的集中優化 , 提升了各java應用的整體啟動時間,提速8% 。
7.1啟動優化
7.1.1 現狀
作為阿里巴巴使用最為廣泛的分布式服務框架 , 服務集團內數萬個應用,它的重要性自然不言而喻;但是隨著業務的發展 , 應用依賴的 Jar 包 和 HSF 服務也變得越來越多,導致應用啟動速度變得越來越慢,接下來我們將看一下如何優化啟動速度 。
7.1.2為什么會慢
作為一個優秀的 RPC 服務框架,當然能夠讓用戶能夠進行靈活擴展,因此框架提供各種各樣的擴展點一共 200+ 個 。
的擴展點機制有點類似 JAVA 標準的 SPI 機制 , 但是設置了 3 個不同的加載路徑,具體的加載路徑如下:
也就是說,一個 SPI 的加載,一個就需要掃描這個下所有的 Jar 包 3 次 。
以 熱點應用A為例 , 總的業務數達到 582 個左右,那么所有的 SPI 加載需要的次數為: 200(spi) 3(路徑) 582() = 次 。
可以看到掃描次數接近 35萬 次! 并且整個過程是串行掃描的,而我們知道 java.lang.# 是一個比較耗時的操作,因此整個 SPI 加載過程耗時是非常久的 。
7.1.3 SPI 加載慢的解決方法
由我們前面的分析可以知道,要想減少耗時,第一是需要減少 SPI 掃描的次數,第二是提升并發度,減少無效等待時間 。
第一個減少 SPI 掃描的次數,我們經過分析得知,在整個集團的業務應用中,使用到的 SPI 集中在不到 10 個 SPI,因此我們疏理出一個 SPI 列表,在這個 SPI 列表中,默認只從框架所在的限定目錄加載 , 這樣大大下降了掃描次數,使熱點應用A總掃描計數下降到不到 2萬 次,占原來的次數 5% 這樣 。
第二個提升了對多個掃描的效率,采用并發線程池的方式來減少等待的時間,具體代碼如下:
7.1.4 其他優化手段
1、去除啟動關鍵鏈路的非必要同步耗時動作,轉成異步后臺處理 。
2、緩存啟動過程中查詢第三方可緩存的結果,反復重復使用 。
7.1.5 優化結果
熱點應用A啟動時間從 603秒 下降到 220秒,總體時間下降了 383秒 => 603秒 下降到 220秒,總體時間下降了 383秒 。
7.2啟動優化
背景介紹:1、tair:阿里巴巴內部的緩存服務,類似于公有云的redis;2、:阿里巴巴內部配置中心 , 目前已經升級成MSE,和公有云一樣的中間件產品
7.2.1 現狀
目前中臺基礎服務使用的tair集群均使用獨立集群,獨立集群中使用多個NS(命名空間)來區分不同的業務域,同時部分小的業務也會和其他業務共享一個公共集群內單個NS 。
早期tair的集群是通過進行初始化,后來為了容災及設計上的考慮 , 調整為使用進行初始化訪問 , 但內部還是會使用來確定需要鏈接的集群 。整個tair初始化過程中讀取的配置的流程如下:
1、根據獲取配置信息,從配置信息中可以獲得信息,用于標識所在集群
2、根據信息,獲取當前tair的路由規則,規定某一個機房會訪問的集群信息 。
通過該配置可以確定當前機房會訪問的目標集群配置 , 以機房A為例,對應的配置集群tair.mdb.mc.XXX.機房A
3、獲取對應集群的信息,確定tair集群的cs列表
從上面的分析來看,在每次初始化的過程中,都會訪問相同的配置,在初始化多個同集群的的時候,部分關鍵配置就會多次訪問 。但實際這部分配置的數據本身是完全一致 。
由于本身為了保護自身的穩定性,在客戶端對訪問單個配置的頻率做了控制,超過一定的頻率會進入等待超時階段 , 這一部分導致了應用的啟動延遲 。
7.2.2 優化方案
tair客戶端進行改造,啟動過程中,對的配置數據做緩存,配置監聽器維護緩存的數據一致性,tair客戶端啟動時 , 優先從緩存中獲取配置,當緩存獲取不到時,再重新配置配置監聽及獲取配置信息 。
7.3啟動優化
背景介紹::阿里巴巴集團內部的開關平臺,對應阿里云AHAS云產品[8]
7.3.1 現狀
Alladdmade this class to besafe.op is not , so don't care abouthere.
這是源碼里存放各個 bean 的中的注釋,可見當時的作者認為 bean只需初始化一次,本身對性能的影響不大 。但沒有預料到隨著業務的增長, bean的初始化可能會成為應用啟動的瓶頸 。
業務平臺的定位導致了平臺啟動期間有大量業務容器初始化,由于中間件的大部分方法全部被修飾,因此所有應用容器初始化到了加載開關配置時(入口為com..csp..core.#init())就需要串行執行,嚴重影響啟動速度 。
7.3.2 解決方案
去除了關鍵路徑上的所有鎖 。
7.3.3 原理
本次升級將存放配置的核心數據結構修改為了,并基于等 j.u.c API 做了小重構 。值得的是修改后原先串行的對配置的獲取變成了并行,觸發了服務端限流,在大量獲取相同開關配置的情況下有很大概率拋異常啟動失敗 。
(如圖: 去鎖后,配置獲取的總次數不變,但是請求速率變快)
為了避免上述問題:
7.4 TDDL啟動優化
背景介紹:TDDL:基于 Java 語言的分布式數據庫系統,核心能力包括:分庫分表、透明讀寫分離、數據存儲平滑擴容、成熟的管控系統 。
7.4.1 現狀
TDDL在啟動過程 , 隨著分庫分表規則的增加,啟動耗時呈線性上漲趨勢,在國際化多站點的場景下,耗時增長會特別明顯,未優化前,我們一個核心應用TDDL啟動耗時為120秒+(6個庫),單個庫啟動耗時20秒+,且通過多個庫并行啟動 , 無法有效降低耗時 。
7.4.2 解決方案
通過工具分析,發現將分庫分表規則轉成腳本,并生成的class,這塊邏輯總耗時非常久 , 調用次數非常多,且在里頭有加鎖(所以并行無效果) 。調用次數多,是因為生成class的個數,會剩以物理表的數量 , 比如配置里只有一個邏輯表 + 一個規則(不同表的規則也存在大量重復),分成1024張物理表,實際啟動時會產生1024個規則類 , 存在大量的重復 , 不僅啟動慢 , 還浪費了很多 。
優化方案是新增一個全局的,將規則和生成的規則類實例存放進去,避免相同的規則去創建不同的類和實例 。
八、其他提速
除了前面幾篇文章提到的優化點(優化、中間件優化等)以外,我們還對中臺核心應用做了其他啟動優化的工作 。
8.1 相關優化
8.1.1 現狀
在進行啟動耗時診斷的時候,意外發現耗時特別久 , 達到了54秒多,不可接受 。
通過定位發現,如果應用里有使用到通過注解來判斷是否添加切面的規則,的耗時就會特別久 。
以下是熱點應用A中的例子:
8.1.2 解決方案
將相關jar包版本升級到1.9.0及以上,熱點應用A升級后,耗時從54.5秒降到了6.3秒,提速48秒多 。
另外 , 需要被識別的,需要是 , 不然會很慢 。
8.1.3 原理
通過工具采集到老版本的在判斷一個bean的上是否有時的代碼堆棧,發現它去jar包里讀取class文件并解析類信息,耗時耗在類搜索和解析上 。當看到這個的時候,第一反應就是 , java.lang,不是有方法么,為什么要繞一圈自己去從jar包里解析出來 。不太理解,就嘗試去看看最新版本的這塊是否有改動,最終發現升級即可解決 。
去class原始文件中讀取的原因是的如果不是的話,運行時是獲取不到的,詳見:java.lang..的注釋
1.8.8版本在判斷是否有注解的邏輯:
1.9.8版本在判斷是否有注解的邏輯:與老版本的差異在于會判斷的是不是的,是的話,就直接從里獲取了 。
老版本的相關執行堆棧:(格式:時間|類名|方法名|行數)
8.2 tbbpm相關優化( & javac)
8.2.1 現狀
中臺大部分應用都使用tbbpm流程引擎,該引擎會將流程配置文件編譯成java class來進行調用,以提升性能 。tbbpm默認是使用com.sun.tools.javac.Main工具來實現代碼編譯的,通過工具分析,發現該過程特別耗時,交易應用A這塊耗時在57秒多 。
8.2.2 解決方案
通過采用來編譯bpm文件,應用A預編譯bpm文件的耗時從57秒多降到了8秒多,快了49秒 。
8.2.3 原理
com.sun.tools.javac.Main執行編譯時,會把傳進去,自行從jar包里讀取類信息進行編譯,一樣是慢在類搜索和解析上 。而是使用去獲取這些信息 , 根據前面的文章“優化篇”,我們對加了索引,極大的提升搜索速度,所以會快非常多 。
javac編譯相關執行堆棧:(格式:時間|類名|方法名|行數)
九、持續地…激情
一輛車,可以從直升機上跳?。?部梢苑沙墼詒?I希?踔量梢園滄吧匣鵂??嬪咸??。上天入地沒有什么不可能,只要有想象,有創新 。
我們的研發基礎設施與工具還在路上 , 還在不斷改造的路上,還有很多的速度與激情可以追求 。
【速度與激情 java應用提速】本文到此結束,希望對大家有所幫助 。
- 唐嫣與羅晉曾因戲生情 ?羅晉唐嫣最新消息
- 熊玲:強迫癥的性格解剖
- Latex的下載與安裝,讓你認識寫作的軟件
- 肖戰方辟謠與李沁戀情 ?肖戰微博澄清李沁
- 中國傳統倫理道德的核心精神 ?倫理與道德的區別與聯系
- 許凱被評百搭男主臉 ?許凱與程瀟有沒有在橫店見面
- ?真的嗎?我不信!Jennie疑似與YG解約
- ?朱時茂個人資料結過幾次婚 與妙齡女吻別朱時茂妻子什么反應
- 30歲馬思純與26歲歐豪分手 ?馬思純歐豪為何分手
- 張柏芝與花臂男共進晚餐 ?張柏芝說不會再公開戀情
