快速启动:基于CRaC实现Spring Boot 3恢复预热

在本教程中,我们将了解检查点协调恢复 (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("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 相同的问题,但限制更多,并且故障排除体验可能更昂贵。