快速啟動:基於CRaC實現Spring Boot 3恢復預熱

banq發表於2024-05-31

在本教程中,我們將瞭解檢查點協調恢復 (CRaC),這是一個 OpenJDK 專案,它允許我們在更短的時間內啟動 Java 程式以完成第一個事務。此外,我們將瞭解Alpaquita Containers如何讓我們輕鬆地在 Spring Boot 應用程式中實現 CRaC。

OpenJDK CRaC 如何解決 Java 中的緩慢預熱問題?

  • Java 應用程式歷來因啟動速度慢和預熱時間較長(達到穩定峰值效能所需的時間)而受到不少批評
  • 此外,它們在預熱期間消耗的計算資源比穩定執行時所需的資源還要多。

這種行為很大程度上可以歸因於 HotSpot Java 虛擬機器 (JVM) 的根本工作方式。當應用程式啟動時,JVM 會在程式碼中查詢熱點並對其進行編譯以獲得更好的效能。但是,這需要時間和計算資源來實現。

此外,必須對應用程式的每個例項重複此操作。在微服務和無伺服器等雲原生架構中,問題更加嚴重。在這裡,我們需要儘可能縮短預熱時間,同時保持資源消耗相對穩定。

如果我們可以將應用程式執行到其最佳效能並檢查該狀態會怎麼樣? 然後,我們可以使用此檢查點啟動應用程式的多個例項,而無需花費太多時間進行預熱。 這基本上是 OpenJDK CRaC API 向我們承諾的。

CRaC 基於使用者空間檢查點和恢復 (CRIU),這是一個為 Linux 實現檢查點和恢復功能的專案。CRIU 允許凍結容器或單個應用程式並從儲存的檢查點檔案中恢復它。

然而,CRaC 採用了 CRIU 的通用方法,並增加了一些增強和調整,使其適用於 Java 應用程式。例如,CRaC 對應用程式的狀態施加了某些限制,以保證檢查點的一致性和安全性。

採用 CRaC 的挑戰
CRaC 為基於 Java 的應用程式在雲環境中更高效地提供了新的機會。在這裡,Spring 是開發基於 Java 的應用程式的流行框架之一。隨著 Spring Boot 3.2 的釋出,我們現在在 Spring 框架中初步支援 CRaC 。

但是,CRaC 並不像看上去那麼便攜。正如我們已經討論過的,CRaC 僅適用於 Linux,因為 CRIU 是 Linux 特有的功能。在其他作業系統上,CRaC 具有用於建立和載入快照的無操作實現。

此外,CRaC 要求在拍攝快照之前關閉所有檔案和網路連線。恢復檢查點後必須重新開啟這些檔案和網路連線。這需要 Java 執行時和框架的支援。

因此,我們不僅需要 Spring 的支援,還需要支援 CRaC 的 JDK 版本,例如 BellSoft 提供的 Liberica JDK。此外,我們需要在 Linux 發行版上執行 Spring 應用程式,例如 BellSoft 的 Alpaquita Linux。

因此,如果我們可以將我們的應用程式與在類似 Linux 的環境中執行的支援 CRaC 的 JDK 一起打包為可移植容器,那麼解決方案將變得非常便攜且即插即用。這正是 BellSoft 為現代 Java 應用程式提供的承諾!

CRaC 與 Alpaquita 容器
BellSoft是一家 OpenJDK 供應商,為雲原生 Java 應用程式提供端到端解決方案。作為其中的一部分,它提供了一套針對執行 Java 應用程式高度最佳化的容器。他們打包了Alpaquita Linux和Liberica JDK,這兩者都是 BellSoft 的產品。

Alpaquita Linux 是唯一專為 Java 構建且針對雲原生應用部署進行最佳化的 Linux 發行版。它透過核心最佳化、記憶體管理和最佳化的 malloc 實現了更好的效能。它的基本映像大小僅為 3.28 MB!

Liberica JDK 是用於雲原生 Java 部署的開源 Java 執行時。它支援最廣泛的架構和作業系統,是真正的統一 Java 執行時。除了安全且合規之外,它還有助於構建成本和時間高效的容器。

BellSoft 管理多個公共映象,提供各種 JDK 型別(jre、jdk 或 jdk-all)、Java 版本(包括對最新 LTS 版本 Java 21 的支援)和 libc 型別(glibc 或 musl)的組合。現在,BellSoft 還提供提供 CRaC 和 CDS(類資料共享)的映象。

這些即用型映象使我們能夠將 CRaC 無縫整合到 Spring Boot 應用程式中。目前,此功能適用於具有 x86_64 架構的 JDK 17 和 21。BellSoft 聲稱,帶有 CRaC 的 Alpaquita Containers 可將啟動時間提高 164 倍,並將映象縮小 1.1 倍。

映象大小的減小主要歸因於駐留集大小 (RSS) 的減少,駐留集大小是程序佔用的記憶體中儲存在主記憶體 (RAM) 中的部分。其中一個關鍵因素是帶有 CRaC 的 Liberica JDK 在檢查點之前執行完整的垃圾收集。

BellSsoft 的產品非常適合基於 Spring Boot 的 Java 應用程式。Spring建議使用 BellSsoft Liberica JDK,它是Spring Boot 中的預設 Java 執行時。對於我們的教程,我們將使用 Spring Boot 應用程式並使用 Alpaquita 容器執行 CRaC。

1. 準備應用程式
在本教程中,我們將建立一個簡單的 Spring Boot 應用程式來探索 CRaC。我們將重用上一個教程中建立的應用程式。在本教程中,我們將使用 Java 21 和 Spring Boot 3.2.5。CRaC 在這種組合下執行良好。

但是,為了能夠使用 CRaC,我們需要在 Spring Boot 應用程式中新增Maven 中央儲存庫中可用的 crac 包作為依賴項:

implementation(<font>"org.crac:crac:1.4.0")

現在,我們必須使用 Gradle 構建應用程式以在目錄“ ./build/libs ”中生成可執行 JAR:

$ ./gradlew clean build

現在我們已經建立了一個具有 CRaC 依賴項的簡單 Spring Boot 應用程式,我們需要使用支援 CRaC 的 JDK 來執行它。為此,我們將使用支援 CRaC 的 Alpaquita 容器。BellSoft 在其Docker Hub 儲存庫上管理多個映像。

值得慶幸的是,所有支援 CRaC 的影像都帶有標籤“ crac ”。我們將在本教程中在我們的機器上提取一個這樣的影像:

$ docker pull bellsoft/liberica-runtime-container:jdk-21-crac-slim-glibc

這裡,“ jdk-21-crac-slim-glibc ”是映象的標籤。這樣,我們就可以嘗試使用 CRaC 的檢查點和恢復功能了。我們將看到 Alpaquita Containers 如何讓這一切變得輕鬆且便攜。

2. 啟動應用程式
首先,我們在“ ./build/libs ”中建立一個名為“ checkpoint ”的目錄,用於儲存應用程式轉儲。現在,我們將使用之前提取的 Alpaquita 容器映象來執行我們在上一小節中建立的應用程式 JAR:

$ docker run -p 8080:8080 \
  --rm --privileged \
  -v $(pwd)/build/libs:/crac/ \
  -w /crac \
  -n fibonacci-crac \
  bellsoft/liberica-runtime-container:jdk-21-crac-slim-glibc \
  java -Xmx512m -XX:CRaCCheckpointTo=/crac/checkpoint \
  -jar spring-bellsoft-0.0.1-SNAPSHOT.jar

讓我們花點時間瞭解一下這個命令。在這裡,我們將容器埠 8080 對映到主機埠 8080。我們還使用了“特權”模式,因為這對於底層 CRIU 正常工作是必要的。

此外,我們將應用程式 JAR 所在的目錄對映為容器內的卷,並將其用作工作目錄。最後,我們提供了 Java 命令來執行 JAR 以及一些必要的引數。

如果一切順利,我們應該能夠檢查容器日誌並驗證我們的應用程式確實已啟動:

2024-04-22T15:27:39.730Z  INFO 129 --- [main] 
  com.baeldung.demo.Application : Started Application in 3.203 seconds (process running for 4.727)

現在,我們應該對應用程式執行一些請求,以便 JVM 可以獲取已編譯的熱程式碼,從而獲得更好的效能。不過,對於我們這個簡單的應用程式來說,這些影響可以忽略不計。

3. 執行檢查點
此時,我們已準備好執行應用程式的檢查點。但在執行此操作之前,讓我們檢查 RSS 的大小,以將其與恢復後看到的大小進行比較。我們需要應用程式的程序 ID (PID) 才能執行此操作:

$ docker exec fibonacci-crac ps -a | pgrep spring-bellsoft

一旦我們得到了 PID,我們就可以使用‘ pmap ’命令來查詢 RSS 的大小:

$ docker exec fibonacci-crac pmap -x <PID> | tail -1
total            4845016  134128  118736       0

該命令的輸出顯示 RSS 的大小(以千位元組為單位),這裡的第二個值(134128)。

現在,讓我們在此狀態下執行應用程式的檢查點。我們可以透過使用“ jcmd ”命令來執行此操作,該命令向 JVM 傳送命令以執行檢查點:

$ docker exec fibonacci-crac jcmd <PID> JDK.checkpoint

請注意,“ fibonacci-crac ”是我們在啟動容器時使用的名稱。執行此命令後,Java 例項將被轉儲,容器將停止。應用程式轉儲包含我們提到的位置的多個檔案:

$ ls
core-129.img  core-139.img  core-149.img  core-198.img   pagemap-129.img
core-130.img  core-140.img  core-150.img  core-199.img   pages-1.img
core-131.img  core-141.img  core-151.img  core-200.img   pstree.img
core-132.img  core-142.img  core-152.img  dump4.log      seccomp.img
core-133.img  core-143.img  core-154.img  fdinfo-2.img   stats-dump
core-134.img  core-144.img  core-155.img  files.img      timens-0.img
core-135.img  core-145.img  core-156.img  fs-129.img
core-136.img  core-146.img  core-158.img  ids-129.img
core-137.img  core-147.img  core-159.img  inventory.img
core-138.img  core-148.img  core-160.img  mm-129.img

此轉儲包括正在執行的 Java 應用程式的確切狀態以及有關堆、JIT 編譯程式碼等的資訊。但是,正如我們之前討論的那樣,我們在此使用的 Liberica JDK 在檢查點之前執行完整的垃圾收集。

4. 從轉儲啟動應用程式
現在,我們要做的就是使用我們之前建立的應用程式轉儲來恢復應用程式的例項。這就像定期啟動應用程式一樣簡單:

docker run -p 8080:8080 \
  --rm --privileged \
  -v $(pwd)/build/libs:/crac/ \
  -w /storage \
  -n fibonacci-crac-from-checkpoint \
  bellsoft/liberica-runtime-container:jdk-21-crac-slim-glibc \
  java -XX:CRaCRestoreFrom=/crac/checkpoint
Like before, if everything goes smoothly, we should be able to verify this from the application log:
2024-04-22T16:02:21.582Z  INFO 129 --- [Attach Listener] 
  o.s.c.support.DefaultLifecycleProcessor : 
  Spring-managed lifecycle restart completed (restored JVM running for 1494 ms)

我們可以看到,應用程式已恢復到建立此檢查點時的狀態。我們可以注意到恢復速度快得多,但是對於這個簡單的應用程式來說,恢復速度不太明顯。

結果概述
正如我們在採取檢查點之前所做的那樣,讓我們​​在恢復之後再次檢查 RSS 的大小,最好是在嚮應用程式發出幾個請求之後:

$ docker exec fibonacci-crac-from-checkpoint pmap -x 129 | tail -1
total            5044580  120261   62728       0

我們可以看到,該值 (120261) 小於我們在檢查點之前注意到的值。儘管對於我們在本教程中使用的應用程式的性質來說,這一現象並不那麼明顯。

我們可能還會注意到,恢復後的 RSS 在第一次請求後增加,然後達到某個穩定狀態。但是,這個值仍然低於我們在進行應用程式轉儲之前觀察到的 RSS。

RSS 的減少主要歸因於 Liberica JDK 和 CRaC 在檢查點之前執行了完整的垃圾收集。在恢復時,HotSpot 虛擬機器將部分本機記憶體返回給作業系統,其中包括 GC 期間釋放的頁面。

CRaC 與 GraalVM 原生映象
我們討論過的 Java 問題自誕生之日起就一直存在。但直到最近,我們才有了在雲上儘可能節省成本的嚴格要求。實現這一目標的關鍵因素之一是Scale-to-Zero,即在不使用時自動將所有資源縮減為零。

當然,這要求我們的應用程式能夠快速啟動並開始響應請求。因此,CRaC 之前的解決方案也是為了滿足這一需求而提出的。其中,GraalVM Native Image解決了更廣泛的目標,包括啟動時間緩慢。

因此,值得將 CRaC 與 GraalVM Native Image 進行比較。GraalVM Native Image 是一個預先 (AOT) 編譯器,可為 Linux、Windows 和 macOS 建立本機可執行檔案。BellSoft 提供了一個Liberica Native Image Kit來基於 GraalVM 生成本機映像。

與 CRaC 一樣,GraalVM Native Image 可以幫助顯著減少啟動時間。但 GraalVM在記憶體使用量更少、安全性更高、應用程式檔案大小更小方面表現更佳。此外,我們可以為多種作業系統生成 GraalVM Native Image。

但是,使用 GraalVM,我們無法使用一些 Java 功能,例如在執行時載入任意類。此外,許多可觀察性和測試框架不支援 GraalVM,因為它不允許在執行時動態生成程式碼,並且我們無法執行 Java 代理。

那麼 CRaC 和 GraalVM Native Image 哪個更好?好吧,這兩種技術都有自己的空間。然而,GraalVM Native Image 解決了與 CRaC 相同的問題,但限制更多,並且故障排除體驗可能更昂貴。

 

相關文章