在本文中,您将了解如何利用CRaC来提高 Java 启动时间并如何在 Kubernetes 中配置。
OpenJDK 检查点协调恢复 (CRaC) 项目由 Azul 于 2020 年推出。Azul 是一个以名为 Azul Zulu 的 OpenJDK 发行版而闻名的组织。Azul 发布了 OpenJDK 17 发行版 ,内置了对 CRaC 的支持。其目标是大幅减少 Java 应用程序的启动时间和达到峰值性能的时间。Micronaut 和 Quarkus 框架已经支持 CRaC,而 Spring Framework 宣布于 2023 年 11 月提供支持。
CRaC 背后的想法是什么?事实上,这是一个非常简单的概念。CRaC 在应用程序运行时拍摄内存快照,然后在以后的执行中恢复它。它基于称为用户空间检查点/恢复 (CRIU) 的 Linux 功能。不幸的是,没有适用于 Windows 或 Mac 的 CRIU 等效工具,因此目前您只能在 Linux 上使用 CRaC。
假设我们已经安装了 Azul Zulu OpenJDK,我们有 Linux 和一个支持 CRaC 的应用程序:
第一步是使用-XX:CRaCCheckpointTo参数运行我们的应用程序。它启用 CRaC 并指示快照的位置:
$ java -XX:CRaCCheckpointTo=/crac-files -jar target/sample-app.jar |
一旦我们的应用程序运行,我们可以在另一个终端中运行以下命令:
$ jcmd target/sample-app.jar JDK.checkpoint |
该jcmd命令触发应用程序检查点创建。过了一会儿,我们的快照就准备好了。我们可以转到该/crac-files目录并查看文件列表。目录结构不会告诉我们太多信息,但有一个名为 的文件dump4.log包含操作日志。如果命令成功完成,我们就可以进入下一步。为了恢复我们的图像并从保存的状态运行应用程序,我们需要运行以下命令:
$ java -XX:CRaCRestoreFrom=/crac-files |
您的应用程序的启动速度应该比以前快得多。差异是显着的。您可能需要几毫秒来启动,而不是几秒钟。
GitHub 存储库。当前练习的示例应用程序 Spring Boot 可在callme-service目录中找到。
为 Spring Boot 启用 CRaC
Spring Boot 目前不支持 CRaC,如果我们运行标准 Spring Boot 应用程序,然后执行jcmd创建检查点的命令,您将看到类似于以下结果的内容:
jdk.crac.impl.CheckpointOpenSocketException: tcp6 localAddr :: localPort 8080 remoteAddr :: remotePort 0 at java.base/jdk.crac.Core.translateJVMExceptions(Core.java:80) at java.base/jdk.crac.Core.checkpointRestore1(Core.java:137) at java.base/jdk.crac.Core.checkpointRestore(Core.java:177) at java.base/jdk.crac.Core.lambda$checkpointRestoreInternal$0(Core.java:194) at java.base/java.lang.Thread.run(Thread.java:832) |
幸运的是,我们可以绕过这个问题。在Maven Central存储库中,有支持CRaC的Tomcat Embed版本。tomcat-embed-core我们可以包含该依赖项并替换Spring Web 项目使用的默认模块。这是解决方案:
<dependency> |
一旦我们替换了tomcat-embed-core依赖项,我们就应该重建应用程序。有一个自定义 Maven 配置文件可激活tomcat-embed-core示例应用程序代码中依赖项的替换。crac因此请记住在构建期间启用配置文件:
$ mvn clean package -Pcrac |
为K8s构建docker
第一步,我们需要准备 Java 应用程序的镜像。为此,我们将Dockerfile在应用程序的根目录中创建一个。我们将使用支持 CRaC 的最新版本的 Azul Java 17 作为基础镜像。我们的镜像将包含应用程序 uber JAR 文件和用于创建检查点的单个脚本。
FROM azul/zulu-openjdk:17-jdk-crac-latest |
下面是 entrypoint.sh 脚本的内容,它已被复制到 Dockerfile 中的目标映像中。
#!/bin/bash |
如你所见,我们在启动 Java 应用程序后运行了 jcmd 命令。
关于 CRaC,有一件重要的事情我们需要在此提及。
下面是 CRaC 文档中的片段:"只有当整个 Java 实例状态可以存储在映像中时,CRaC 实现才会创建检查点。打开的文件或套接字等资源是不能存储的,因此需要在创建检查点时释放它们。".因此,jcmd 命令将停止我们的 Java 进程,所以我们不应该在此之后杀死容器/脚本。如果我们在启动容器后以这种方式运行脚本,它会首先创建快照,然后在 10 秒后停止 pod。
$ docker build -t callme-service:1.1.0 . |
现在,让我们在 Kubernetes 的背景下考虑一下我们的场景。
首先,我们需要创建快照并将其状态保存在磁盘上。这是一个一次性活动。或者更准确地说,每次发布应用程序都是一次性活动。因此,我们甚至应该在创建(或更新)部署之前执行该操作。当然,我们需要提供存储并将其分配给创建快照的 pod 和使用 CRaC 从存储中还原应用程序的所有 pod。让我们从 PersistenceVolumeClaim 定义开始:
apiVersion: v1 |
下一步,我们将创建一个执行检查点操作的 Kubernetes 作业。
job.yaml |
- 它将运行我们已构建的映像 (1),
- 然后执行负责进行检查点的 entrypoint.sh 脚本 (2)。
- CRaC 检查点操作需要更高的权限,因此我们需要在 securityContext 部分允许它(3)。
- 我们还将把 crac-store PVC 挂载到作业的 /crac 路径下 (4)。
让我们将作业应用到 Kubernetes 集群:
$ kubectl apply -f job.yaml
Kubernetes 会启动与作业相关的单个 pod。一旦状态更改为 "已完成",就意味着检查点操作已经完成。
$ kubectl get po -n crac |
现在,我们可以继续部署应用程序了。
deployment-crac.yaml |
- 我们将运行应用程序的三个 pod (1)。
- 我们将使用与之前完全相同的映像 (2),
- 但这次我们将运行 java -XX:CRaCRestoreFrom=/crac 命令 (3),而不是 entrypoint.sh 脚本。
- 为了测量 pod 准备就绪所需的时间,我们将添加周期为最低的 redinessRrobe (4)。
这样,我们就能比较启用 CRaC 机制和未启用 CRaC 机制时应用程序的启动时间。
部署:
$ kubectl apply -f deployment-crac.yaml
最后,我们可以显示正在运行的 callme-service pod 列表。
$ kubectl get po -n crac |
总结
与 GraalVM 提供的本地编译相比,CRaC 可被视为实现 Java 快速启动和预热的另一种方法。GraalVM 还能解决内存占用大的问题。不过,这也是有代价的,因为使用 GraalVM 会遇到更多限制,而且故障排除过程可能会更痛苦。另一方面,使用 CRaC,我们需要创建快照映像并将其存储在持久卷上。因此,每次我们都需要将卷挂载到在 Kubernetes 上运行的 pod 上。总之,还是多一种选择比较好。