摘要:本文介紹了華為云對(duì)冷啟動(dòng)優(yōu)化這一業(yè)界難題的探索之路,創(chuàng)新提出了基于進(jìn)程級(jí)快照的優(yōu)化方案。
作者信息——
子游:華為元戎高級(jí)工程師
平山:華為云中間件 Serverless 負(fù)責(zé)人
琪君:華為元戎負(fù)責(zé)人
Key Takeaways
- 冷啟動(dòng) (Cold Start)一直是Serverless領(lǐng)域面臨的優(yōu)化難題之一,華為云創(chuàng)新提出了基于進(jìn)程級(jí)快照的冷啟動(dòng)加速解決方案,致力于在用戶幾乎無感知的前提下,有效提升應(yīng)用的冷啟動(dòng)性能;
 - 特別的,Java應(yīng)用冷啟動(dòng)速度慢的問題尤為突出。本文以Java場(chǎng)景為例,介紹華為云在冷啟動(dòng)性能優(yōu)化方面的探索歷程,并揭秘90%+性能提升背后的技術(shù)實(shí)現(xiàn)原理。文末我們也提供了Quick Start,幫助用戶更快地上手該新特性。
 
一、問題引言:Java應(yīng)用冷啟動(dòng)速度面臨巨大挑戰(zhàn)
Serverless應(yīng)用啟動(dòng)時(shí),都需要先進(jìn)行初始化。其初始化時(shí)長(zhǎng)一般取決于應(yīng)用本身的屬性,如業(yè)務(wù)邏輯、編程語言等,其中Java應(yīng)用的初始化過程通常是最慢的。以下基于一個(gè)典型的Java應(yīng)用,對(duì)其啟動(dòng)時(shí)延進(jìn)行拆解,各階段耗時(shí)分布如圖1所示:

圖1:Java應(yīng)用啟動(dòng)耗時(shí)分解
其中,端到端冷啟動(dòng)耗時(shí)可分為2大部分:
- 平臺(tái)側(cè)時(shí)間:
 
主要包含執(zhí)行環(huán)境創(chuàng)建(如容器啟動(dòng))、執(zhí)行環(huán)境初始化(如代碼包下載、部署)等準(zhǔn)備工作,此階段最多是秒級(jí)響應(yīng),在冷啟動(dòng)整體耗時(shí)中占比很低,通常不到5%,平臺(tái)側(cè)也支持一些優(yōu)化方式,將耗時(shí)進(jìn)一步壓縮至毫秒級(jí);
- 服務(wù)側(cè)時(shí)間:
 
主要包含應(yīng)用框架啟動(dòng)(如構(gòu)建Spring ApplicationContext)、業(yè)務(wù)初始化(如業(yè)務(wù)數(shù)據(jù)初始化)等動(dòng)作,此階段耗時(shí)一般較長(zhǎng)。在本例中,應(yīng)用框架啟動(dòng)耗時(shí)占比約30%,業(yè)務(wù)初始化占比約65%。由此推斷,該階段執(zhí)行的動(dòng)作是Java應(yīng)用啟動(dòng)慢的核心所在。
Java應(yīng)用啟動(dòng)慢的根因其實(shí)也不難理解,主要有:
- 框架復(fù)雜:Spring作為一個(gè)企業(yè)級(jí)的框架,為了支持廣泛的應(yīng)用需求,存在大量的可配置和初始化邏輯,并通過復(fù)雜的設(shè)計(jì)模式來支撐這種靈活性。例如,一個(gè)spring-boot-web的hello world,依賴的class文件就多達(dá)7404個(gè),見圖2;
 - JVM的一次編譯,到處運(yùn)行:類加載時(shí),查找類、校驗(yàn)類的開銷會(huì)隨著應(yīng)用復(fù)雜度而增長(zhǎng);同時(shí),在應(yīng)用剛啟動(dòng)時(shí),方法還沒有完全被JIT編譯完成,因此大部分情況停留在解釋執(zhí)行,影響了應(yīng)用啟動(dòng)的速度。
 

圖2: hello world依賴的class個(gè)數(shù)
因此,對(duì)于時(shí)延敏感型的Java應(yīng)用程序,在突發(fā)流量下發(fā)生冷啟動(dòng)時(shí),可能會(huì)導(dǎo)致用戶體驗(yàn)下降。為了應(yīng)對(duì)這一挑戰(zhàn),用戶可以提前預(yù)留資源來減少冷啟動(dòng)發(fā)生的頻率,或者對(duì)自己的應(yīng)用進(jìn)行性能調(diào)優(yōu),但是第一類方案無形中增加了用戶的keep-alive成本,第二類方案也有著較高的技術(shù)門檻且往往效果比較有限。
二、基于快照技術(shù)的冷啟動(dòng)加速:華為云的優(yōu)化探索之路
Part I:站在巨人的肩膀上
業(yè)界針對(duì)Java應(yīng)用的啟動(dòng)速度優(yōu)化已有一些優(yōu)秀的實(shí)踐,可分為以下幾類:
- AOT:
 
主要有GraalVM[1]、EJET等,AOT方案是通過在程序運(yùn)行前,直接將Java源碼編譯成本地機(jī)器碼,因?yàn)樘崆熬幾g并不占用運(yùn)行時(shí)間,以此來顯著提升應(yīng)用的啟動(dòng)速度,同時(shí)本地機(jī)器碼可以持久化于磁盤中,不占用內(nèi)存且可重復(fù)使用。但是該類方案在特定場(chǎng)景也存在一定的局限性,如GraalVM對(duì)反射的支持并不友好,在涉及反射的地方都需要新增配置;EJET雖然解決了反射的問題,但是其編譯時(shí)間較長(zhǎng)且不穩(wěn)定,在復(fù)雜應(yīng)用場(chǎng)景下也存在性能劣化問題。
- AppCDS[2]:
 
AppCDS方案是通過在JVM啟動(dòng)時(shí)從JSA文件讀取共享數(shù)據(jù),省略了共享類的加載過程,提升JVM啟動(dòng)速度;同時(shí),多個(gè)JVM共享同一個(gè)歸檔文件,減少動(dòng)態(tài)內(nèi)存占用,可以提升內(nèi)存使用率。該類方案主要適用于類加載比較多的場(chǎng)景,在一般場(chǎng)景下提升有限,且其對(duì)共享類的支持有一定限制,如運(yùn)行時(shí)動(dòng)態(tài)生成類不支持共享等。
- 其他針對(duì)性(Spring框架)方案:
 
如Lazy Initialization[3]、Scanning-index[4]等,前者通過懶加載的方式來減少啟動(dòng)時(shí)加載類的數(shù)量,一定程度上提升啟動(dòng)速度;后者通過在編譯階段創(chuàng)建索引,避免啟動(dòng)時(shí)掃描所有路徑來進(jìn)行加速。但是該類方案在Serverless場(chǎng)景缺乏一定的普適性。
華為云FunctionGraph創(chuàng)新提出的基于進(jìn)程級(jí)快照的冷啟動(dòng)加速解決方案,致力于在用戶無感知(無需/少量進(jìn)行代碼適配)的前提下,幫助用戶突破冷啟動(dòng)的性能瓶頸。本優(yōu)化方案直接從應(yīng)用初始化后的快照進(jìn)行運(yùn)行環(huán)境恢復(fù),跳過復(fù)雜的框架、業(yè)務(wù)初始化階段,從而顯著降低Java應(yīng)用的啟動(dòng)時(shí)延,實(shí)測(cè)性能提升達(dá)90%+。
Part II:快照方案如何優(yōu)化Java應(yīng)用啟動(dòng)速度
當(dāng)用戶Java函數(shù)打開冷啟動(dòng)加速的配置開關(guān)后,華為云FunctionGraph會(huì)預(yù)先執(zhí)行函數(shù)對(duì)應(yīng)的初始化代碼,獲取其初始化執(zhí)行上下文環(huán)境的快照,并進(jìn)行加密緩存。后續(xù)調(diào)用該函數(shù)并觸發(fā)冷啟動(dòng)擴(kuò)容時(shí),會(huì)直接從提前初始化后的應(yīng)用快照來恢復(fù)執(zhí)行環(huán)境,而非重新走一遍初始化流程,以此達(dá)到極大提升啟動(dòng)性能的效果。
先結(jié)合圖3直觀對(duì)比一下優(yōu)化前、后的冷啟動(dòng)流程差異:

圖3:基于快照加速的冷啟動(dòng)流程
基于快照的冷啟動(dòng)流程,主要包含以下幾個(gè)關(guān)鍵步驟:
Step 1:平臺(tái)側(cè)提前準(zhǔn)備執(zhí)行環(huán)境,并預(yù)執(zhí)行初始化代碼、保存應(yīng)用快照,此動(dòng)作后續(xù)統(tǒng)稱為Checkpoint
- 與圖1對(duì)應(yīng),此階段一般占總耗時(shí)的90%左右。
 
Step 2:在請(qǐng)求到達(dá),觸發(fā)函數(shù)新實(shí)例擴(kuò)容時(shí),直接從應(yīng)用快照來恢復(fù)新的執(zhí)行環(huán)境,此動(dòng)作后續(xù)統(tǒng)稱為Restore
- Restore 耗時(shí)是秒級(jí),相當(dāng)于將數(shù)十秒完整的初始化時(shí)間(在圖 1 的示例中)縮短至秒級(jí) Restore 耗時(shí),啟動(dòng)性能提升了一個(gè)數(shù)量級(jí)。
 
Step 3:(可選)應(yīng)用進(jìn)程從快照恢復(fù)后,執(zhí)行Restore Hook完成業(yè)務(wù)狀態(tài)的刷新
- 由于Image File是進(jìn)程運(yùn)行時(shí)的快照,在重建進(jìn)程之后,會(huì)涉及到進(jìn)程持有狀態(tài)的有效性更新。例如已建立的外部鏈接、加載到進(jìn)程里的緩存信息等。故我們引入了Restore Hook的概念,提供手段讓業(yè)務(wù)對(duì)這些狀態(tài)進(jìn)行刷新,詳見Part IV。
 
Step 4:應(yīng)用Ready,具備接著往下執(zhí)行業(yè)務(wù)邏輯的能力
- 特別的,容器本身也是主機(jī)上的進(jìn)程,故本優(yōu)化方案也支持容器粒度的Checkpoint,即對(duì)容器內(nèi)指定進(jìn)程進(jìn)行CR,與傳統(tǒng)的輕量化虛機(jī)快照相比,其精細(xì)化程度更高、也更靈活。其原理詳見圖4:
 

圖4:基于容器的CR流程
- 在Source機(jī)器上啟動(dòng)微服務(wù),通過健康檢查和初始化調(diào)用后,進(jìn)行Checkpoint,停止服務(wù),生成進(jìn)程快照信息;
 - 在Source機(jī)器上將進(jìn)程快照信息和微服務(wù)所有相關(guān)依賴,進(jìn)行壓縮,加密生成內(nèi)存快照包,并上傳至云端存儲(chǔ)。
 - 在Target機(jī)器上從持久化存儲(chǔ)中下載對(duì)應(yīng)微服務(wù)的內(nèi)存快照包,進(jìn)行解壓恢復(fù)。
 - 在Target機(jī)器上Restore微服務(wù)進(jìn)程;
 
Part III:快照技術(shù)揭秘
華為云提出的基于進(jìn)程級(jí)快照的冷啟動(dòng)加速方案,其核心技術(shù)依托于CRIU[5],它支持對(duì)用戶空間指定的進(jìn)程進(jìn)行“凍結(jié)”(即停止進(jìn)程,并將該進(jìn)程運(yùn)行的所有上下文持久化為 鏡像 文件),并在必要時(shí)對(duì)其進(jìn)行“解凍”(即通過保存的鏡像文件來正確恢復(fù)進(jìn)程運(yùn)行的上下文),其核心工作流程如圖5-6所示[6]:

圖5:CRIU如何工作——Checkpoint

圖6:CRIU如何工作——Restore
- Checkpoint
- CRIU首先通過操作系統(tǒng)的 /proc 目錄獲取指定進(jìn)程和該進(jìn)程下所有子進(jìn)程的信息,包含文件描述符(/proc/$pid/fd)、管道參數(shù)、網(wǎng)絡(luò)配置和內(nèi)存映射文件(/proc/$pid/maps)等;
 - CRIU接著通過Linux 的ptrace syscall接口把一段特殊代碼動(dòng)態(tài)注入到該進(jìn)程的地址空間,通過執(zhí)行該動(dòng)態(tài)代碼,CRIU以UNIX守護(hù)進(jìn)程的方式收集dumpee進(jìn)程存放在寄存器里的內(nèi)存數(shù)據(jù);
 - CRIU將所有進(jìn)程信息都收集完畢后,再次調(diào)用ptrace接口,去掉動(dòng)態(tài)注入的代碼,恢復(fù)該進(jìn)程的原有代碼;
 - CRIU根據(jù)收集的進(jìn)程內(nèi)存信息,生成多個(gè)以功能分類的鏡像文件,并默認(rèn)殺死進(jìn)程,完成Checkpoint;
 
 - Restore
- CRIU解析Checkpoint階段生成的鏡像文件,并分析多進(jìn)程的共享資源;
 - CRIU通過Linux 的fork 接口重新構(gòu)建、恢復(fù)進(jìn)程和其共享資源;
 - CRIU恢復(fù)所有任務(wù)的資源,但不包含內(nèi)存映射地址,定時(shí)器,線程等;
 - CRIU根據(jù)鏡像文件重新映射內(nèi)存空間,切換進(jìn)程上下文,恢復(fù)進(jìn)程的繼續(xù)執(zhí)行,完成Restore;
 
 
Part IV:Restore Hook
如Part II所述,雖然本優(yōu)化方案能極大提升Java應(yīng)用的冷啟動(dòng)速度,但是快照技術(shù)在某些場(chǎng)景也存在一定的局限性,較難做到對(duì)現(xiàn)有應(yīng)用的全透明化。通過快照恢復(fù)后,應(yīng)用的網(wǎng)絡(luò)連接狀態(tài)會(huì)受到影響,涉及到TCP Socket重連等場(chǎng)景,如服務(wù)注冊(cè)、DB連接,分布式通信,消息隊(duì)列等。
這部分場(chǎng)景依賴應(yīng)用本身的網(wǎng)絡(luò)重連機(jī)制來更新正確,因此,本優(yōu)化方案中也引入了Restore Hook的概念,提供手段讓業(yè)務(wù)對(duì)這些狀態(tài)進(jìn)行刷新。
Restore Hook當(dāng)前已支持大部分主流第三方組件的重連,詳見圖7:

圖7:Restore Hook支持的第三方組件
不難發(fā)現(xiàn),Restore Hook需要應(yīng)用本身進(jìn)行少量的代碼適配。為了進(jìn)一步簡(jiǎn)化應(yīng)用的改造負(fù)擔(dān),我們也進(jìn)行了一種新的技術(shù)嘗試,可以理解其充當(dāng)了用戶應(yīng)用與BaaS之間的紐帶,通過狀態(tài)卸載等手段,對(duì)開發(fā)者透明,幫助應(yīng)用完成狀態(tài)的 自動(dòng)化 刷新。這部分探索會(huì)在后續(xù)的技術(shù)博文中跟大家分享,敬請(qǐng)期待。
三、效果實(shí)測(cè):Java冷啟動(dòng)時(shí)延降低90%+
我們選取了公司內(nèi)部典型的Java應(yīng)用,對(duì)其原始初始化流程、Restore流程進(jìn)行了對(duì)比測(cè)試,如圖8所示。測(cè)試結(jié)果表明,本優(yōu)化方案將應(yīng)用的啟動(dòng)速度平均提升了95%+,即使快照包的增大一定程度上增加了包下載、解壓的耗時(shí),但最終端到端的冷啟動(dòng)時(shí)延也降低了90%+。

圖8:冷啟加速前后的數(shù)據(jù)對(duì)比
四、快速上手:基于華為云FunctionGraph的簡(jiǎn)單實(shí)戰(zhàn)
華為云發(fā)布的基于進(jìn)程級(jí)快照的冷啟動(dòng)加速方案,是一種性能優(yōu)化服務(wù),用戶無需額外付費(fèi),只需進(jìn)行簡(jiǎn)單的配置、少量的代碼修改,即可享受到該創(chuàng)新方案帶來的冷啟動(dòng)性能提升。
下文基于華為云FunctionGraph,為大家?guī)硖匦?strong>Quick Start:
1、登錄FunctionGraph控制臺(tái),創(chuàng)建Java函數(shù),并打開“快照式冷啟動(dòng)”開關(guān)

2、(可選)配置Restore Hook,并在函數(shù)代碼中實(shí)現(xiàn)對(duì)應(yīng)的Hook邏輯


3、函數(shù)發(fā)布新版本后,觸發(fā)快照的自動(dòng)化制作

4、請(qǐng)耐心等待快照制作完成(5min超時(shí)時(shí)間)


5、調(diào)用Java函數(shù),體驗(yàn)快照優(yōu)化后的性能提升

五、總結(jié)與展望
本文介紹了華為云對(duì)冷啟動(dòng)優(yōu)化這一業(yè)界難題的探索之路,創(chuàng)新提出了基于進(jìn)程級(jí)快照的優(yōu)化方案。當(dāng)然,本方案也并非十全十美,它依然面臨著一系列挑戰(zhàn),如文中提到的應(yīng)用狀態(tài)刷新、進(jìn)程級(jí)CR的精細(xì)化控制、多平臺(tái)的兼容性等,我們也在持續(xù)探索、優(yōu)化中。
同時(shí),F(xiàn)unctionGraph 作為華為元戎內(nèi)核加持的下一代 Serverless 函數(shù)計(jì)算與編排服務(wù),致力于持續(xù)為用戶提供方便、迅捷的Serverless 服務(wù)體驗(yàn)。您可以登錄華為云FunctionGraph控制臺(tái)來深入體驗(yàn),更多信息請(qǐng)參閱FunctionGraph官方文檔[7]。后續(xù)我們將分享更多圍繞通用全場(chǎng)景 Serverless的前沿理論及其案例實(shí)踐,回饋社區(qū)。
參考資料
[1]https://www.graalvm.org/22.3/reference-manual/java/compiler/
[2]https://wiki.openjdk.org/display/HotSpot/Application+Class+Data+Sharing+-+AppCDS
[3]https://spring.io/blog/2019/03/14/lazy-initialization-in-spring-boot-2-2
[4]https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-scanning-index
[5]https://github.com/checkpoint-restore/criu
[6]https://speakerdeck.com/udzura/introduction-to-criu?slide=32