19 апр. 2024

Speed up the Java app startup time, part -3 : deploying on Kubernates

It's the third and last part of the series. In this blog post, I am going to deploy the Spring Boot application with Java CRAC support on Kubernates. The post is mainly based on Piotr Mińkowski's post "Speed Up Java Startup on Kubernetes with CRaC", so all the credit goes to him.

Whats I am going to use:

  1. JVM: Zulu 21.0.3.crac-zulu

  2. Kubernates: Minikube

  3. Docker

  4. Spring boot 3.2 application.

Here is my server setup for the experiment.

I am going to use a separate VM on promox server; however, you can run everything on your local machine. In the case of using kubenates+docker on a single machine, you don't need Nginx or a reverse proxy server to expose the application URL.

The source code of the application is available on Github. Actually, we will use the Spring Boot 3.2 application from the previous part of the series. For deploying on Kubernates cluster, I added a few deployment config files, a single Docker file, and a bash script to create the checkpoint.

Let's jump on the scenario that I am going to use. It's very straight-forward. We apply a kubernates job, which will run the application, then create a checkpoint/ snapshot, which will be stored on the Kubernates persistent volumes. Later, we deploy a few pods from the checkpoint.

Now it's time to get to work ;-).

Step 1. Download the sample project from the Git Hub repository. The project contains a REST service which will return 3 Customers ID and Names using the given URL, "/customers"

Step 2. Download and install the Azul Zulu Build of OpenJDK with CRaC support if you haven't done it before. Note that JDK should be installed with sudo. Don't forget to add the JDK to your class path.

Step 3. Build the project as shown below:

mvn clean package

Step 4. Create a Docker image. Run the following command from the project root directory:

docker build -t spring-boot-crac:0.0.1 . 

If everything goes fine, the above command will create a Docker image. This image will be our base image to create Kubernate pods.

The Docker image will also have a copy of the entrypoint.sh bash script to create the checkpoint for the application.

#!/bin/bash

java -XX:CRaCCheckpointTo=/crac -jar /app/spring-boot-crac-0.0.1.jar&
sleep 10
jcmd /app/spring-boot-crac-0.0.1.jar JDK.checkpoint
sleep 10

echo checkpoint process completed.

Step 5. Create a Kubernetes "Persistent Volume Claims". Run the following command from the root directory of the project:

kubectl create namespace crac
kubectl apply -f ./k8s/PersistenceVolumeClaim.yaml

The content of the deployment file is as follows:

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: crac-store
  namespace: crac
spec:
  accessModes:
    - ReadWriteOnce
  volumeMode: Filesystem
  resources:
    requests:
      storage: 10Gi

It's a basic Kubernetes deployment that will create 10G of persistent volume to store the application checkpoint files.

Step 6. Create a "Job" to create the snapshot of the Spring Boot application.

kubectl apply -f ./k8s/job.yaml

The job will create a "spring-boot-crac-job" and run once.

apiVersion: batch/v1
kind: Job
metadata:
  name: spring-boot-crac-job
  namespace: crac
spec:
  template:
    spec:
      containers:
        - name: spring-boot-crac
          image: spring-boot-crac:0.0.1
          env:
            - name: VERSION
              value: "v1"
          command: ["/bin/sh","-c", "/app/entrypoint.sh"]
          volumeMounts:
            - mountPath: /crac
              name: crac
          securityContext:
            privileged: true
      volumes:
        - persistentVolumeClaim:
            claimName: crac-store
          name: crac
      restartPolicy: Never
  backoffLimit: 3

Under the hood, the job will execute the "entrypoint.sh" bash script, which will create the checkpoint of the application.

If you go through the log of the job, you should have the following information:

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v3.2.0)
2024-04-18T08:36:08.252Z  INFO 7 --- [           main] c.blu.reactive.crac.SpringBootCRaCTest   : Starting SpringBootCRaCTest v0.0.1 using Java 21.0.1 with PID 7 (/app/spring-boot-crac-0.0.1.jar started by root in /)
2024-04-18T08:36:08.254Z  INFO 7 --- [           main] c.blu.reactive.crac.SpringBootCRaCTest   : No active profile set, falling back to 1 default profile: "default"
2024-04-18T08:36:08.837Z  INFO 7 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port 8080 (http)
2024-04-18T08:36:08.843Z  INFO 7 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2024-04-18T08:36:08.843Z  INFO 7 --- [           main] o.apache.catalina.core.StandardEngine    : Starting Servlet engine: [Apache Tomcat/10.1.16]
2024-04-18T08:36:08.864Z  INFO 7 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2024-04-18T08:36:08.865Z  INFO 7 --- [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 569 ms
2024-04-18T08:36:09.070Z  INFO 7 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port 8080 (http) with context path ''
2024-04-18T08:36:09.080Z  INFO 7 --- [           main] c.blu.reactive.crac.SpringBootCRaCTest   : Started SpringBootCRaCTest in 1.084 seconds (process running for 1.36)
7:
2024-04-18T08:36:17.912Z  INFO 7 --- [Attach Listener] jdk.crac                                 : Starting checkpoint
CR: Checkpoint ...
/app/entrypoint.sh: line 6:     7 Killed                  java -XX:CRaCCheckpointTo=/crac -Djdk.crac.collect-fd-stacktraces=true -jar /app/spring-boot-crac-0.0.1.jar
checkpoint process completed.

At these moments, we are ready to create pod's as much as needed from this checkpoint.

Step 7. Deploy pod's based on the snapshot from the persistence store. Run the following command:

kubectl apply -f ./k8s/deployment-crac.yaml

The above command should create 2 pods and one service to expose the application URL.

The full script of the deployment is as follows:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: spring-boot-crac
  namespace: crac
spec:
  replicas: 2
  selector:
    matchLabels:
      app: spring-boot-crac
  template:
    metadata:
      labels:
        app: spring-boot-crac
    spec:
      containers:
        - name: spring-boot-crac
          image: spring-boot-crac:0.0.1
          imagePullPolicy: IfNotPresent
          ports:
            - containerPort: 8080
          env:
            - name: VERSION
              value: "v1"
          command: ["java"]
          args: ["-XX:CRaCRestoreFrom=/crac"]
          volumeMounts:
            - mountPath: /crac
              name: crac
          securityContext:
            privileged: true
          resources:
            limits: 
              cpu: '1'
      volumes:
        - name: crac
          persistentVolumeClaim:
            claimName: crac-store
---
apiVersion: v1
kind: Service
metadata:
  name: spring-boot-crac
  namespace: crac
  labels:
    app: spring-boot-crac
spec:
  type: ClusterIP
  ports:
  - port: 8080
    name: http
  selector:
    app: spring-boot-crac

The deployment configuration uses the docker image named "spring-boot-crac:0.0.1" and uses the Java command to create 2 pods from the checkpoint allocated on the persistent volume.

If you are curious to know the startup time of the pod, please check the log file.


2024-04-18T08:37:18.504Z  INFO 7 --- [Attach Listener] o.s.c.support.DefaultLifecycleProcessor  : Restarting Spring-managed lifecycle beans after JVM restore
2024-04-18T08:37:18.508Z  INFO 7 --- [Attach Listener] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port 8080 (http) with context path ''
2024-04-18T08:37:18.509Z  INFO 7 --- [Attach Listener] o.s.c.support.DefaultLifecycleProcessor  : Spring-managed lifecycle restart completed (restored JVM running for 20 ms)

In my case, it's only 20 ms. So, Java CRaC is a great feature which can improve Java application startup time and can be used in production. Happy Java CRaCing :-)