上周 发布 的 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,这意味着恢复速度非常快(在毫秒级范围内)。
本文使用 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 启动 {#不使用-crac-启动}
克隆 petclinic
后,需要构建项目(如使用 gradlew clean build
),然后就可以运行它了。
首先关注应用的启动时间。我在两个 JDK 版本(17 和 21)上都做了测试,首先,仅仅从 17 版本切换到 21 版本,petclinic 应用的启动时间就已经缩短了 500 毫秒!
因此,如果可能,你应该尽快升级 JDK,以获得更好的性能。
执行以下操作启动应用:
java -jar spring-petclinic-3.2.0.jar
以下是在不使用 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
以下是从自动检查点恢复时与启动时间相关的结果:
这很酷,无需修改代码就能获得比原始启动时间快一个数量级的启动时间。这也意味着检查点只包含框架代码,而不包含应用代码,因为应用代码尚未启动。
手动检查点 {#手动检查点}
自动检查点已经大大缩短了启动时间,但如果使用手动检查点,甚至可以比它更快。
使用手动检查点时,你可以决定何时创建检查点。
为什么这很重要?你可能想在 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
这个手动触发的检查点不仅包含框架代码,还包含应用程序代码,这意味着启动速度会更快,因为框架已经加载并启动了应用程序。结果如下:
如你所见,petclinic 应用的启动时间又缩短了一个数量级,降至 75 毫秒!
Resource 资源 {#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