Spring Boot 对 CRaC(Coordinated Restore at Checkpoint)的支持

上周 发布 的 Spring 6.1 和 SpringBoot 3.2 都全面支持 CRaC(Coordinated Restore at Checkpoint)。

CRaC(Coordinated Restore at Checkpoint),翻译过来应该是 “检查点协调恢复”,如果你想了解有关 CRaC 的更多信息,请参阅 这里

CRaC 是一个 OpenJDK 项目,可以对运行中的 JVM(Java 虚拟机)进行 “快照”,并将其状态(包括应用)存储到磁盘中。然后,在另一个时间点,可以将 JVM 从保存的检查点恢复到内存中。借助这个功能,你可以启动应用、预热并创建检查点(Checkpoint)。从保存的检查点恢复到内存主要依靠磁盘 I/O,这意味着恢复速度非常快(在毫秒级范围内)。

CRaC(Coordinated Restore at Checkpoint)

本文使用 SpringBoot Petclinic 项目来测试 SpringBoot 3.2 对 CRaC 的支持。

前提条件

要在 SpringBoot 3.2 中使用 CRaC,需要具备以下三个条件

  • 支持 CRaC 的 JVM
  • org.crac 的依赖
  • 存放检查点的文件夹

使用的 JDK(Java 开发包)是 Azul Zulu 21.0.1 + CRaC,可从 此处 获取。该 JDK 适用于 x64 和 aarch64 CPU 架构,以及 JDK 17 和 JDK 21。

可能还需要设置使用 CRIU 的权限,也就是说,在运行演示程序的 Linux 机器上,需要执行一次以下命令:

sudo chown root:root $JAVA_HOME/lib/criu
sudo chmod u+s $JAVA_HOME/lib/criu

petclinic 仓库克隆到本地,并添加 org.crac 依赖。

由于 CRaC 目前仅适用于 Linux,因此你无法找到支持 CRaC 的 MacOS 和 Windows JDK。这意味着,如果你使用的是 Mac 或 Windows 机器,你就无法使用 CRaC API 进行编码。为了解决这个问题,org.crac 库提供了与支持 CRaC 的 JDK 中相同的 API,但不是使用 jdk.crac 命名空间,而是在 org.crac 命名空间中。

有了这个支持后,即使在 MacOS 和 Windows 系统上,你也可以针对 CRaC API 编写代码,而不会遇到任何问题。只要你在 Linux 系统上运行启用了 CRaC 的 JDK,它就会使用 CRaC 功能。

你可以在 Maven 仓库找到 org.crac

Gradle:

implementation 'org.crac:crac:1.4.0'

Maven:

<dependency>
  <groupId>org.crac</groupId>
  <artifactId>crac</artifactId>
  <version>1.4.0</version>
</dependency>

在测试之前,需要确保有一个可以存放检查点的文件夹,例如项目文件夹中的 /tmp_checkpoint

不使用 CRaC 启动

克隆 petclinic 后,需要构建项目(如使用 gradlew clean build),然后就可以运行它了。

首先关注应用的启动时间。我在两个 JDK 版本(17 和 21)上都做了测试,首先,仅仅从 17 版本切换到 21 版本,petclinic 应用的启动时间就已经缩短了 500 毫秒!

因此,如果可能,你应该尽快升级 JDK,以获得更好的性能

执行以下操作启动应用:

java -jar spring-petclinic-3.2.0.jar

以下是在不使用 CRaC 的情况下启动应用的结果:

不使用 CRaC 的情况下启动应用的耗时

虽然快了 500 毫秒左右,但启动仍需要一些时间,接下来看看在 SpringBoot 3.2 中实现的另一种方法。

自动检查点

Spring 团队的工程师们有一个很好的想法,那就是在应用启动前自动创建一个检查点,从而缩短 Spring/SpringBoot 框架的启动时间。

下面是文档中的描述:

当设置 -Dspring.context.checkpoint=onRefresh JVM 系统属性时,将在 LifecycleProcessor.onRefresh 阶段启动时自动创建检查点。该阶段结束后,所有非延迟初始化的 Singleton 都已实例化,InitializingBean#afterPropertiesSet 回调也已调用;但生命周期尚未启动,ContextRefreshedEvent 尚未发布

按如下方式启动应用,启动自动检查点功能:

java -Dspring.context.checkpoint=onRefresh -XX:CRaCCheckpointTo=./tmp_checkpoint -jar spring-petclinic-3.2.0.jar

执行程序后,它会创建检查点,将检查点文件存储在 ./tmp_checkpoint 文件夹中,然后退出程序。

现在,你可以执行以下命令,从检查点恢复应用(这意味着再次启动它):

java -XX:CRaCRestoreFrom=./tmp_checkpoint

以下是从自动检查点恢复时与启动时间相关的结果:

使用自动 CRaC 的情况下启动应用的耗时

这很酷,无需修改代码就能获得比原始启动时间快一个数量级的启动时间。这也意味着检查点只包含框架代码,而不包含应用代码,因为应用代码尚未启动。

手动检查点

自动检查点已经大大缩短了启动时间,但如果使用手动检查点,甚至可以比它更快。

使用手动检查点时,你可以决定何时创建检查点。

为什么这很重要?你可能想在 10 分钟后或应用完全预热(大部分/全部代码已编译和优化)时创建一个检查点,等等。

创建手动检查点的程序与自动检查点类似,唯一的区别是你要从应用程序外部触发检查点,而不是让框架自动创建检查点。

开始之前,确保检查点文件夹为空。

首先,按以下步骤启动应用:

java -XX:CRaCCheckpointTo=./tmp_checkpoint -jar spring-petclinic-3.2.0.jar

现在,等应用完全启动后,再打开第二个 shell 窗口。在第二个 shell 窗口中,执行以下命令:

jcmd spring-petclinic-3.2.0.jar JDK.checkpoint

现在你应该看到,在启动 petclinic 应用的第一个 shell 窗口中,创建了一个检查点并关闭了应用。

你可以通过验证文件夹 ./tmp_checkpoint 是否包含检查点文件来检查应用是否已经创建了检查点。

现在可以关闭第二个 shell 窗口了。

要从该检查点恢复应用,需要执行与自动检查点相同的命令:

java -XX:CRaCRestoreFrom=./tmp_checkpoint

这个手动触发的检查点不仅包含框架代码,还包含应用程序代码,这意味着启动速度会更快,因为框架已经加载并启动了应用程序。结果如下:

使用手动 CRaC 的情况下启动应用的耗时

如你所见,petclinic 应用的启动时间又缩短了一个数量级,降至 75 毫秒!

Resource 资源

由于 Spring 6.1 和 SpringBoot 3.2 完全支持 CRaC,因此我们无需修改代码。这里的完全支持意味着,只要使用 Spring Resource,框架就会在检查点之前关闭 Resource,并在还原之后恢复 Resource

如果使用其他资源,则需要在相关类中实现 CRaC Resource 接口,并在 beforeCheckpoint() 方法中关闭资源(如打开的文件或 Socket 连接),然后在 afterRestore() 方法中重新打开资源。

总结

使用 CRaC 可以显著缩短 SpringBoot 3.2 应用的启动时间。如果你想在不改动代码的情况下尝试一下,只需使用 Spring 6.1 / SpringBoot 3.2 中的自动检查点功能,就能将启动时间缩短一个数量级。

如果想获得最快的启动时间,可以手动创建检查点,这样可以将启动时间缩短两个数量级。

CRaC 的好处在于它仍运行在正常的 JVM 上,而且在检查点/恢复后还能进一步优化代码。

为了获得这些结果,我在 petclinic 项目中添加了几行代码,如果你想要重现这些数据,可以克隆我的 petclinic 项目副本,位于我的 GitHub repository 中。


Ref:

  • https://foojay.io/today/springboot-3-2-crac/
  • https://mp.weixin.qq.com/s/sIxb1YVL4rUVSNbWuU4XOA