使用 Spring Boot 3.2 和 CRaC 实现更快启动

借助 Spring Boot 3.2 和 Spring Framework 6.1,我们获得了对检查点协调恢复(CRaC) 的支持,这是一种使 Java 应用程序能够更快启动的机制。借助 Spring Boot,我们可以以一种简化的方式使用 CRaC,即启动时自动检查点/恢复。

这篇博文将展示一个示例,其中 Spring Boot 应用程序的启动时间减少了 90%。

CRAC 简介、优势和挑战
检查点协调恢复 (CRaC)是 OpenJDK 中的一项功能,最初由Azul开发,旨在通过允许 Java 应用程序快速恢复到之前保存的状态来提高其启动性能。CRaC 使 Java 应用程序能够保存其在特定时间点(检查点)的状态,然后在稍后从该状态恢复。这对于快速启动时间至关重要的场景特别有用,例如无服务器环境、微服务,以及通常必须能够快速扩展其实例并在不使用时支持缩减到零的应用程序。

 CRAC 的工作原理
1、检查点创建:在应用程序执行期间的选定点处,将创建一个检查点。这涉及捕获 Java 应用程序的整个状态,包括堆、堆栈和所有活动线程。然后将状态序列化并保存到文件系统。在检查点过程中,应用程序通常会暂停以确保捕获一致的状态。此暂停经过协调,以最大限度地减少中断并确保应用程序可以正确恢复。

在进行检查点之前,通常会向应用程序发送一些请求以确保它已预热,即已加载所有相关类,并且 JVM HotSpot 引擎有机会根据运行时的使用方式优化字节码。

执行检查点的命令:

 java -XX:CRaCCheckpointTo=<some-folder> -jar my_app.jar
 # Make calls to the app to warm up the JVM...
 jcmd my_app.jar JDK.checkpoint

2、状态恢复:当应用程序从检查点启动时,先前保存的状态将从文件系统反序列化并重新加载到内存中。然后,应用程序将从检查点的确切位置继续执行,绕过通常的启动顺序。

从检查点恢复的命令:
java -XX:CRaCRestoreFrom=<some-folder>

从检查点恢复允许应用程序跳过初始启动过程,包括类加载、预热初始化和其他启动例程,从而显著减少启动时间。

挑战和考虑
与任何新技术一样,CRaC 也面临着一系列新的挑战和考虑:

  1. 状态管理:在创建检查点之前,必须关闭打开的文件和与外部资源(如数据库)的连接。还原后,必须重新打开它们。CRaC 公开了一个 Java 生命周期接口,应用程序可以使用它来处理此问题,org.crac.Resource使用回调方法beforeCheckpoint和afterRestore。
  2. 敏感信息:存储在 JVM 内存中的凭据和机密将序列化到检查点创建的文件中。因此,这些文件需要受到保护。另一种方法是针对使用其他凭据的临时环境运行检查点命令,并在恢复时替换凭据。
  3. Linux 依赖性:检查点技术基于 Linux 的一项功能,称为CRIU,“用户空间中的检查点/恢复”。此功能仅适用于 Linux,因此在 Mac 或 Windows PC 上测试 CRaC 的最简单方法是将应用程序打包到 Linux Docker 映像中。
  4. 需要 Linux 权限:CRIU 需要特殊的 Linux 权限,因此构建 Docker 镜像和创建 Docker 容器的 Docker 命令也需要 Linux 权限才能运行。
  5. 存储开销:存储和管理检查点数据需要额外的存储资源,检查点大小会影响恢复时间。还需要原始 jar 文件才能从检查点重新启动 Java 应用程序。

SPRING BOOT 3.2 与 CRAC 集成
Spring Boot 3.2(以及底层 Spring Framework)有助于处理关闭和重新打开与外部资源的连接。在创建检查点之前,Spring 会停止所有正在运行的 bean,让它们有机会在需要时关闭资源。还原后,相同的 bean 会重新启动,从而允许 bean 重新打开与资源的连接。

基于 Spring Boot 3.2 的应用程序唯一需要添加的是对crac- 库的依赖。使用 Gradle,它在文件中如下所示gradle.build:

dependencies {
    implementation 'org.crac:crac'

正常的 Spring Boot BOM 机制负责对crac依赖项进行版本控制。

Spring Boot 处理的连接自动关闭和重新打开通常有效。不幸的是,在撰写这篇博文时,一些 Spring 模块缺少这种支持。为了跟踪 Spring 生态系统中 CRaC 支持的状态,我们创建了一个专用测试项目Spring Lifecycle Smoke Tests 。当前状态可在项目的[url=https://github.com/spring-projects/spring-lifecycle-smoke-tests/blob/main/STATUS.adoc]状态页面[/url]上找到。

如果需要,应用程序可以通过实现上述Resource接口来注册要在检查点之前和恢复之后调用的回调方法。本博文中使用的微服务已扩展为注册回调方法,以演示如何使用它们。代码如下所示:

import org.crac.*;

public class MyApplication implements Resource {

  public MyApplication() {
    Core.getGlobalContext().register(this);
  }

  @Override
  public void beforeCheckpoint(Context<? extends Resource> context) {
    LOG.info("CRaC's beforeCheckpoint callback method called...");
  }

  @Override
  public void afterRestore(Context<? extends Resource> context) {
    LOG.info(
"CRaC's afterRestore callback method called...");
  }
}

与上面描述的默认按需替代方案相比,Spring Boot 3.2 提供了一种简化的替代方案来设置检查点。它被称为启动时自动检查点/恢复。它通过将 JVM 系统属性添加-Dspring.context.checkpoint=onRefresh到java -jar命令来触发。

设置后,应用程序启动时会自动创建检查点。检查点是在 Spring bean 创建但尚未启动之后创建的,即在大多数初始化工作之后但该应用程序启动之前。有关详细信息,请参阅Spring Boot 文档Spring Framework 文档

使用自动检查点,我们无法获得完全预热的应用程序,并且必须在构建时指定运行时配置。这意味着生成的 Docker 映像(镜像)将是特定于运行时的,并且包含来自配置的敏感信息,例如凭据和机密。因此,Docker 映像镜像必须存储在私有且受保护的容器注册表中。

介绍了 CRaC 和 Spring Boot 3.2 对 CRaC 的支持后,让我们看看如何为使用 CRaC 的 Spring Boot 应用程序创建 Docker 镜像。

 使用 DOCKERFILE 创建基于 CRAC 的 DOCKER 镜像
在学习如何使用 CRaC 时,我研究了几篇关于在 Spring Boot 3.2 应用程序中使用 CRaC 的博客文章。它们都使用相当复杂的 bash 脚本(取决于您的 bash 经验),使用 Docker 命令(如docker run、docker exec和)docker commit。尽管它们有效,但与使用 Dockerfile 生成 Docker 映像相比,这似乎是一种不必要的复杂解决方案。

最后,我们解决了使用 Dockerfile 构建 Spring Boot 应用程序所产生的问题,该应用程序可以使用 Docker 镜像中的 CRaC 快速恢复。

生成的 Dockerfilecrac/Dockerfile-crac-automatic如下所示:

# syntax=docker/dockerfile:1.3-labs

FROM azul/zulu-openjdk:21-jdk-crac

ADD build/libs/*.jar app.jar

RUN --security=insecure \
  SPRING_PROFILES_ACTIVE=docker,crac \
  java -Dspring.context.checkpoint=onRefresh \
       -XX:CRaCCheckpointTo=checkpoint -jar app.jar \
  || if [ $? -eq 137 ]; then return 0; else return 1; fi

EXPOSE 8080
ENTRYPOINT ["java", "-XX:CRaCRestoreFrom=checkpoint"]

注意:所有微服务都使用同一个 Dockerfile 来创建其 Docker 镜像的 CRaC 版本。

尝试使用自动检查点/恢复功能进行 CRAC
运行以下命令从 GitHub 获取源代码,跳转到文件Chapter06夹,检出分支SB3.2-crac-automatic,并确保使用 Java 21 JDK(这里使用 Eclipse Temurin):

git clone https://github.com/PacktPublishing/Microservices-with-Spring-Boot-and-Spring-Cloud-Third-Edition.git
cd Microservices-with-Spring-Boot-and-Spring-Cloud-Third-Edition/Chapter06
git checkout SB3.2-crac-automatic
sdk use java 21.0.3-tem

从编译微服务源代码开始:

./gradlew build

如果尚未创建,请使用以下命令创建不安全的构建器:

docker buildx create --name insecure-builder --buildkitd-flags '--allow-insecure-entitlement security.insecure'

现在我们可以构建一个 Docker 镜像,其中构建使用以下命令为每个微服务执行 CRaC 检查点:

docker buildx --builder insecure-builder build --allow security.insecure -f crac/Dockerfile-crac-automatic -t product-composite-crac --load microservices/product-composite-service

docker buildx --builder insecure-builder build --allow security.insecure -f crac/Dockerfile-crac-automatic -t product-crac --load microservices/product-service

docker buildx --builder insecure-builder build --allow security.insecure -f crac/Dockerfile-crac-automatic -t recommendation-crac --load microservices/recommendation-service

docker buildx --builder insecure-builder build --allow security.insecure -f crac/Dockerfile-crac-automatic -t review-crac --load microservices/review-service

运行端到端测试
要启动系统环境,我们将使用 Docker Compose。由于 CRaC 需要特殊的 Linux 权限,因此源代码附带了一个特定于 CRaC 的 docker-compose 文件。通过指定以下内容,crac/docker-compose-crac.yml为每个微服务赋予所需的权限:CHECKPOINT_RESTORE

cap_add:
  - CHECKPOINT_RESTORE

注意: CRaC 上的几篇博客文章建议使用特权容器,即使用 Docker Compose 文件启动它们run --privleged或添加它们。这是一个非常糟糕的想法,因为控制此类容器的攻击者可以轻松控制运行 Docker 的主机。有关更多信息,请参阅 Docker 的运行时特权和 Linux 功能privileged: true文档。

CRaC 特定 Docker Compose 文件的最后添加内容是 MySQL 的卷映射

volumes:
  - "./sql-scripts/create-tables.sql:/docker-entrypoint-initdb.d/create-tables.sql"

使用这个 Docker Compose 文件,我们可以启动系统环境并使用以下命令运行端到端验证脚本:

export COMPOSE_FILE=crac/docker-compose-crac.yml
docker compose up -d

让我们首先验证 CRaCafterRestore回调方法是否被调用:

docker compose logs | grep "CRaC's afterRestore callback method called..."

期望类似以下内容:

...ReviewServiceApplication           : CRaC's afterRestore callback method called...
...RecommendationServiceApplication   : CRaC's afterRestore callback method called...
...ProductServiceApplication          : CRaC's afterRestore callback method called...
...ProductCompositeServiceApplication : CRaC's afterRestore callback method called...

现在,运行端到端验证脚本:

./test-em-all.bash

如果脚本以类似以下内容的日志输出结束:

End, all tests OK: Fri Jun 28 17:40:43 CEST 2024


...这意味着所有测试运行正常,并且微服务的行为符合预期。

使用以下命令关闭系统环境:

docker compose down
unset COMPOSE_FILE


验证微服务从 CRaC 检查点启动时行为正确后,我们可以将它们的启动时间与未使用 CRaC 启动的微服务进行比较。

总结
检查点协调恢复 (CRaC)是 OpenJDK 中的一项强大功能,它允许 Java 应用程序从之前保存的状态(即检查点)恢复,从而提高 Java 应用程序的启动性能。借助 Spring Boot 3.2,我们还可以使用 CRaC 创建检查点的简化方法,即启动时自动检查点/恢复。

本博文中的测试表明,启动性能提高了 10 倍,即在使用启动时自动检查点/恢复时,启动时间减少了 90% 。

这篇博文还解释了如何使用 Dockerfile 构建使用 CRaC 的 Docker 镜像,而不是大多数博文所建议的复杂 bash 脚本。然而,这也带来了一些挑战,比如使用自定义 Docker 构建器进行特权构建,正如博文中所述。

使用在启动时自动检查点/恢复创建的 Docker镜像需要付出代价。Docker 镜像将包含运行时特定的敏感信息,例如运行时连接数据库的凭据。因此,必须保护它们以防止未经授权的使用。

Spring Boot 对 CRaC 的支持并未完全覆盖 Spring 生态系统中的所有模块,因此必须采用一些解决方法,例如在使用 Spring Data JPA 时。

此外,在使用自动检查点/启动时恢复时,JVM HotSpot 引擎无法在检查点之前预热。如果处理的第一个请求的最佳执行时间很重要,那么自动检查点/启动时恢复可能不是最佳选择。