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:
JVM: Zulu 21.0.3.crac-zulu
Kubernates: Minikube
Docker
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 :-)