天天看點

服務端架構治理|閑魚如何有效提高應用的編譯和啟動速度背景建構部署耗時分析建構部署速度治理方案效果及展望

作者:閑魚技術——泊垚

背景

應用的釋出是一件非常耗時的事情,尤其是當應用疊代了比較長的時間之後,一次預發的部署就可能需要花費十幾分鐘,其中服務啟動一次,就可能花費五六分鐘。如此漫長的釋出可能帶來兩方面的問題:

  1. 開發過程中,在使用測試環境的時候釋出驗證的時候,我們往往希望快速疊代,快速驗證,但是每次改完一個feature釋出驗證都要經過十幾分鐘的話,效率是非常低下的。
  2. 線上上釋出過程中,如果遇到機器數量非常多的應用,那麼一次釋出,一批一批的部署下來,耗費的時間非常長,很容易超出釋出視窗,帶來線上風險。

針對應用釋出耗時長的問題,筆者結合對手頭應用idle-local的耗時分析,參考和嘗試了多數的方案之後,制定了一套實施方案,能夠有效提高應用的編譯和啟動速度。同時也着手優化和沉澱了一個啟動加速工具(在其他同學的項目上疊代得到),能夠針對整個過程中最耗時的啟動階段進行加速,有不錯的效果。

建構部署耗時分析

idle-local項目耗時統計

服務端架構治理|閑魚如何有效提高應用的編譯和啟動速度背景建構部署耗時分析建構部署速度治理方案效果及展望

注:

  1. mtop是我們項目的api層
  2. hsf是阿裡的RPC架構
  3. pandora是一個的輕量級的隔離容器,用來隔離Webapp和中間件的依賴

重點優化分析

根據統計得到的應用編譯部署耗時情況,以及理論上的加速空間,我們制定了以下幾項優化的重點:

  1. 部署過程中,啟動應用是最主要的耗時項,也是最容易随着應用疊代膨脹的部分,其中啟動應用的主要耗時是bean的初始化。
  2. 建構過程中,代碼編譯是主要的耗時項,理論上存在較大的優化空間。
  3. 鏡像中最後一層打包内容的大小會影響鏡像push和pull的耗時,如果能夠将變動範圍分層優化,會有較大的收益。
  4. 停止應用的過程中,為了處理RPC服務HSF優雅下線和應用優雅關閉,花了比較多的時間,在非生産環境可以省略

建構部署速度治理方案

應用啟動加速-Spring Bean異步初始化的原理與落地

加速效果:☆☆☆☆

配置簡易:☆☆

推薦指數:☆☆☆☆

我們通過分析spring的初始化過程會發現,spring對于bean的建立,不論是通過周遊還是通過依賴觸發,都是通過同步的方式對bean進行初始化的。

這就導緻了當一個bean的初始化過程很久的時候,會嚴重阻塞後續bean的初始化,哪怕這兩個bean之間完全沒有互相依賴。

如果能夠将bean的初始化過程放到異步線程中,則會大大提升bean的建立效率。

服務端架構治理|閑魚如何有效提高應用的編譯和啟動速度背景建構部署耗時分析建構部署速度治理方案效果及展望

如圖,在完成bean的執行個體化後,異步進行初始化,異步初始化實作的關鍵是保證bean在被使用之前初始化完成。

本節我們将從理論出發,逐漸分析springBean的異步初始化辦法:

孤立的Bean

我們将不被其他Bean依賴的Bean,定義為孤立的Bean。如圖,beanA和beanB1兩個bean,互相不依賴也不被其他bean依賴。

服務端架構治理|閑魚如何有效提高應用的編譯和啟動速度背景建構部署耗時分析建構部署速度治理方案效果及展望

理論上,容器中存在孤立的Bean,這些Bean由于不被其他Bean依賴,在容器初始化過程中,這些Bean不會被其他bean使用,是可以自己異步進行初始化的,隻需要容器初始化完成時,保障這些Bean已經被初始化完成即可。

然而完全孤立的Bean其實比較少,同時,找出孤立的Bean,需要周遊整個依賴樹。我們需要一種覆寫範圍更廣,更容易定義的方式進行異步化。

暫時孤立的Bean

我們有這樣的認知:

  1. 孤立的Bean一定是被spring通過周遊的方式建立的
  2. 被spring通過周遊的方式建立的bean不一定是孤立的Bean,他可能被後來的bean依賴并注入
  3. 被spring通過周遊的方式建立的bean有較大機率是孤立的Bean
  4. 被spring通過周遊的方式建立的bean的初始化距離它被其他的Bean依賴并注入,有一些時間差

基于這樣的認知,我們可以按照以下方案進行異步化:

服務端架構治理|閑魚如何有效提高應用的編譯和啟動速度背景建構部署耗時分析建構部署速度治理方案效果及展望

如圖所示,被spring通過周遊的方式建立的bean我們可以暫時認為他是孤立的Bean,當我們發現他不是的時候(被其他Bean調用了getBean),阻塞并等待他的初始化,那麼期間的異步初始化過程,也能為我們節省時間;如果他始終沒有被依賴,那麼說明他就是孤立的Bean。

至于在實際實作中,如何判斷一個bean是被spring周遊到還是被其他bean依賴導緻的建立,有一個可行的方法是:定義一個全局的标記,用來記錄目前spring周遊到的bean,當且僅當這個标記是null的時候,表示目前正在擷取的bean是被spring周遊到的,然後立即将目前bean寫入标記,并在bean傳回前将标記清除,我們可以通過增強beanFactory的getBean方法實作這部分邏輯。

這個方案的優點是能夠自動識别并嘗試異步初始化,無需複雜的配置即可實作效果不錯的加速。

暫時不被使用的Bean

上述的兩種異步化中,我們通過bean是否被“依賴”來決定是否異步初始化。但還有很多Bean,不是被spring通過周遊的方式建立的,這就導緻我們上面的方案覆寫不到一些耗時的bean。

但事實上,我們Bean的初始化過程,隻要在Bean被“使用”前完成即可,被“依賴”這個條件,是過于嚴格的。如果我們将Bean對象的方法通路判定為被“使用”的入口,那麼我們可以通過對Bean進行代理,攔截Bean的方法通路,在其被“使用”之前,等待他初始化完成。

服務端架構治理|閑魚如何有效提高應用的編譯和啟動速度背景建構部署耗時分析建構部署速度治理方案效果及展望

這樣我們可以指定任意的Bean進行異步初始化。但有一種情況是不安全的:這個Bean定義了公共的變量,如果在初始化之前被通路,是不能被代理攔截的。我們在實作bean的時候,要注意不要暴露内部變量,這是一個很重要的習慣。

FactoryBean的處理

然而上述的并行化方式,對于有一種類型的bean是不适用的,那就是FactoryBean。不論有沒有刻意注意過,寫java應用的同學應該都接觸過FactoryBean,最常見的就是我們的Mapper。FactoryBean建立bean的過程比較特殊,他會先建立一個FactoryBean的執行個體,然後由這個FactoryBean執行個體如建立出我們最終想要的bean執行個體。是以FactoryBean初始化的不是最終得到的執行個體,而是生成這個執行個體的工廠,而這個工廠的初始化完成與否,大機率會影響到bean的生成,是以他不能簡單的将初始化過程異步化。

服務端架構治理|閑魚如何有效提高應用的編譯和啟動速度背景建構部署耗時分析建構部署速度治理方案效果及展望

如圖是一個Bean的擷取過程:

  1. 如果這個Bean是一個單例并且沒有被建立過,那麼就會進入createBean,并且在其中完成初始化。
  2. 如果這個Bean是一個FactoryBean,那麼createBean傳回的不是bean本身,而是一個factory,真正的bean要在之後的getInstance方法中擷取。

在我們的項目中,存在着大量HSFSpringConsumerBean的執行個體,他們都是FactoryBean,而且這些bean的初始化還相當的耗時。(HSFSpringConsumerBean是我們RPC架構HSF的consumer的FactoryBean)

好在FactoryBean也不是完全不能異步初始化,我們分析一個Bean的get擷取過程,會發現,他分為createBean和getInstance兩個階段,在FactoryBean的進行中,如果能夠将兩個階段人為分開,先異步完成第一階段的調用,再觸發第二階段,就可以實作我們的目标。

服務端架構治理|閑魚如何有效提高應用的編譯和啟動速度背景建構部署耗時分析建構部署速度治理方案效果及展望

如圖,實作對factoryBean的加速,我們需要在FactoryBean被getBean之前将需要加速的Bean一起找出來,先手動觸發它們的異步初始化,但不觸發getObject方法,等這些FactoryBean初始化完成後,再交由spring按照原來的建立順序,去觸發他們的getBean方法(此時singleton已經建立,會直接進入getObject調用)。但如果這些Bean對其他的bean有依賴,可能會導緻在第一步的異步初始化中産生間接依賴而觸發getBean。

好消息是HSFSpringConsumerBean不對其他的bean有依賴,而且項目中絕大多數耗時的FactoryBean都是HSFSpringConsumerBean。如果在Spring初始化所有Bean之前,我們可以一次性并行把所有HSFSpringConsumerBean初始化掉,也能夠獲得較大的提升。對于其他不産生依賴的FactoryBean,也可以按照一樣的方式處理,比如我們比較常見的mapper。

對于可能對其他bean産生依賴的FactoryBean,理論上我們也可以通過去阻塞這些bean的getBean方法,等待我們第一階段預初始化的完成。目前項目中這些bean的比例很小,是以這部分功能尚未着手實作。

編譯加速-module依賴關系優化

加速效果:☆☆☆

配置簡易:☆

推薦指數:☆☆☆

目前項目使用的多mudule結構,往往含有start,mtop(接口層),service等層,其中module之間又互相依賴,導緻一個低層module依賴的中間件,又會繼續被上層子產品解析。而往往上層子產品自身非常薄,卻因為依賴了低層子產品,不得不反複解析龐大的依賴樹,導緻編譯時間非常長。調整module的方式,是一種解決方案,但會破壞項目的module,失去來原來多module的優勢。

我們針對含有start,mtop,service的項目,提出一種改動較小的優化方式:

服務端架構治理|閑魚如何有效提高應用的編譯和啟動速度背景建構部署耗時分析建構部署速度治理方案效果及展望

如圖所示:

  1. mtop層在依賴service層的時候,排除service層的所有間接依賴,僅針對mtop層自身也需要依賴的内容進行手動引入。
  2. start層依賴mtop和service,這裡不能再做排除,因為項目是在start層進行打包的,如果排除,則會導緻依賴包沒有被正常打包到項目中而無法啟動。
  3. 按照這種方式優化,能夠節省在mtop層解析service依賴樹的開銷,往往有幾十秒之多。

我們同時也提出約定,在使用這種module結構的時候,控制好mtop和service的邊界:

  1. mtop層是對service提供的服務進行mtop接口級别的封裝,盡量僅依賴應用内service層和common定義的服務和對象,不處理複雜的中間件邏輯
  2. 對于外部服務的使用和中間件定義的服務和對象的使用,盡量在service層封裝,同時不宜将外部服務和中間件定義的對象直接透給mtop層,造成依賴擴散
  3. 這約定之後,清晰mtop層與service層的邊界,mtop層就是操作service層定義的方法和對象來完成接口,涉及外部定義的方法和對象的,收口到service。

鏡像治理-分層建構

配置簡易:☆☆☆

服務端架構治理|閑魚如何有效提高應用的編譯和啟動速度背景建構部署耗時分析建構部署速度治理方案效果及展望

如圖,我們使用docker進行建構時,push/pull image 的時候, 如果某一層的鏡像已經存在了, 就會直接使用緩存, 跳過重複的推送和拉取過程。而正常情況下,應用打出的包 (一般是 tgz 包) 是一個整體, 即使使用者隻修改了一行代碼, 也要打出一個完整的包,包含所有依賴的jar檔案, 導緻每次push和pull的時候,都要傳輸所有的jar包,導緻效率低下。

如果在打包的時候将jar包和項目代碼分到不同的層裡面,在絕大多數建構中,jar包不發生變化,則需要被更新的内容大大減小,進而提升push和pull的速度。同時,在應用啟動過程中,tgz包解壓需要花費一定的時間,在分層打包的改造中,去除了壓縮解壓過程,使得速度進一步提升。

服務端架構治理|閑魚如何有效提高應用的編譯和啟動速度背景建構部署耗時分析建構部署速度治理方案效果及展望

該方案對鏡像建構、鏡像拉取、應用啟動三個過程均有提速,在idle-local中,綜合收益超過30秒(一次建構加一次部署)。

應用停止過程加速

停止應用過程中,比較耗時的有兩個步驟:1、hsf優雅下線;2、應用停止。其中,hsf優雅下線過程,會先通知應用進行hsf provider下線,然後等待一個比較安全的時間,對于生産環境來說,15秒是相對安全的值,對于預發環境來說,可以不等待。應用停止是通過kill -0 信号進行應用停止,應用會進行一些停止前的操作,不同應用不盡相同,如果停止失敗,則使用kill -9強制退出;對于已經進行HSF優雅下線并且沒有其他關鍵退出動作的應用來說,可以直接關閉應用,可以加速停止的耗時,對于非線上環境環境來說,是比較适用的。

效果及展望

服務端架構治理|閑魚如何有效提高應用的編譯和啟動速度背景建構部署耗時分析建構部署速度治理方案效果及展望

經過一系列的優化措施,我們可以看到我們的系統編譯和啟動過程得到了不錯的優化。其中紫色部分的耗時基本可以忽略,紅色部分的耗時減少了一倍。

服務端架構治理|閑魚如何有效提高應用的編譯和啟動速度背景建構部署耗時分析建構部署速度治理方案效果及展望

到目前為止,我們落地了一套有效的加速方案,在idle-local上取得了不錯的效果。後續我們将繼續完善這一系列方案,一方面,我們将着力建設一個基于監控的長效管控方案,能夠讓應用長期保持較好的狀态;另一方面,我們會将上述内容抽象成一個一站式落地方案,支援在其他項目快速的配置落地。

項目 優化效果 配置成本
Spring Bean異步初始化(自動) 10秒*n,随代碼規格提升效果更明顯 引入實作了加速的jar包
Spring Bean異步初始化(手動) 20秒*n,随代碼規格提升效果更明顯 引入實作了加速的jar包并配置bean
HSFSpringConsumerBean優化
module依賴關系優化 40秒 需要調整pom并處理依賴
docker鏡像分層 30秒 修改打包腳本
停止過程加速 40秒(非線上環境) 修改停止腳本