使用CRaC加速Kubernetes中SpringBoot启动


在本文中,您将了解如何利用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>
  <groupId>io.github.crac.org.apache.tomcat.embed</groupId>
  <artifactId>tomcat-embed-core</artifactId>
  <version>10.1.7</version>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-web</artifactId>
  <exclusions>
    <exclusion>
      <groupId>org.apache.tomcat.embed</groupId>
      <artifactId>tomcat-embed-core</artifactId>
    </exclusion>
  </exclusions>
</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
COPY target/callme-service-1.1.0.jar /app/callme-service-1.1.0.jar
COPY src/scripts/entrypoint.sh /app/entrypoint.sh
RUN chmod 755 /app/entrypoint.sh

下面是 entrypoint.sh 脚本的内容,它已被复制到 Dockerfile 中的目标映像中。

#!/bin/bash

java -XX:CRaCCheckpointTo=/crac -jar /app/callme-service-1.1.0.jar&
sleep 10
jcmd /app/callme-service-1.1.0.jar JDK.checkpoint
sleep 10


如你所见,我们在启动 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
kind: PersistentVolumeClaim
metadata:
  name: crac-store
  namespace: crac
spec:
  accessModes:
    - ReadWriteOnce
  volumeMode: Filesystem
  resources:
    requests:
      storage: 10Gi


下一步,我们将创建一个执行检查点操作的 Kubernetes 作业。

job.yaml
apiVersion: batch/v1
kind: Job
metadata:
  name: callme-service-snapshot-job
  namespace: crac
spec:
  template:
    spec:
      containers:
        - name: callme-service
          image: callme-service:1.1.0 # (1)
          env:
            - name: VERSION
              value: "v1"
          command: [
"/bin/sh","-c", "/app/entrypoint.sh"] # (2)
          volumeMounts:
            - mountPath: /crac
              name: crac
          securityContext:
            privileged: true # (3)
      volumes:
        - persistentVolumeClaim:
            claimName: crac-store # (4)
          name: crac
      restartPolicy: Never
  backoffLimit: 3
  • 它将运行我们已构建的映像 (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
NAME                                READY   STATUS      RESTARTS   AGE
callme-service-snapshot-job-j7wkz   0/1     Completed   0          43s

现在,我们可以继续部署应用程序了。

deployment-crac.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: callme-service
spec:
  replicas: 3 # (1)
  selector:
    matchLabels:
      app: callme-service
  template:
    metadata:
      labels:
        app: callme-service
    spec:
      containers:
        - name: callme-service
          image: callme-service:1.1.0 # (2)
          imagePullPolicy: IfNotPresent
          ports:
            - containerPort: 8080
          env:
            - name: VERSION
              value: "v1"
          command: [
"java"] # (3)
          args: [
"-XX:CRaCRestoreFrom=/crac"]
          volumeMounts:
            - mountPath: /crac
              name: crac
          readinessProbe: # (4)
            initialDelaySeconds: 0
            periodSeconds: 1
            httpGet:
              path: /actuator/health/readiness
              port: 8080
          securityContext:
            privileged: true
          resources:
            limits: 
              cpu: '1'
      volumes:
        - name: crac
          persistentVolumeClaim:
            claimName: crac-store

  • 我们将运行应用程序的三个 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
NAME                                READY   STATUS      RESTARTS   AGE
callme-service-6fb68cbd5b-5wz6x     1/1     Running     0          2m38s
callme-service-6fb68cbd5b-pds8c     1/1     Running     0          3m3s
callme-service-6fb68cbd5b-zbf6h     1/1     Running     0          2m18s

总结
与 GraalVM 提供的本地编译相比,CRaC 可被视为实现 Java 快速启动和预热的另一种方法。GraalVM 还能解决内存占用大的问题。不过,这也是有代价的,因为使用 GraalVM 会遇到更多限制,而且故障排除过程可能会更痛苦。另一方面,使用 CRaC,我们需要创建快照映像并将其存储在持久卷上。因此,每次我们都需要将卷挂载到在 Kubernetes 上运行的 pod 上。总之,还是多一种选择比较好。