IK.AM


Programming > Java > org > springframework > boot

Notes on Creating a Docker Image for a Spring Boot App with CRaC Support

Created on Fri Dec 01 2023 • Last Updated on Fri Dec 01 2023N/A Views

🏷️ Spring Boot | Spring MVC | Java | CRaC

Warning

This article was automatically translated by OpenAI (gpt-4o). It may be edited eventually, but please be aware that it may contain incorrect information at this time.

Spring Framework 6.1 / Spring Boot 3.2 has introduced initial support for CRaC (Coordinated Restore at Checkpoint).

This is a note on creating a Docker image to try out CRaC.

CRaC is also mentioned in the presentation Virtual Threads! Checkpoint Restore! Introduction to Notable Features of Spring Framework 6.1 / Spring Boot 3.2 given at JJUG CCC 2023 Fall.

First, create a template project.

curl https://start.spring.io/starter.zip \
  -s \
  -d type=maven-project \
  -d language=java \
  -d bootVersion=3.2.0 \
  -d baseDir=demo \
  -d groupId=com.example \
  -d artifactId=demo \
  -d name=demo \
  -d description=Demo \
  -d packageName=com.example.demo \
  -d packaging=jar \
  -d javaVersion=21 \
  -d dependencies=web,actuator \
  -o demo.zip
unzip demo.zip 
cd demo
git init

To add the CRaC API, add the following <dependency> to pom.xml. The version is managed by Spring Boot.

        <dependency>
            <groupId>org.crac</groupId>
            <artifactId>crac</artifactId>
        </dependency>

Add the following HelloController.

cat <<EOF > ./src/main/java/com/example/demo/HelloController.java
package com.example.demo;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {

    @GetMapping(path = "/")
    public String hello() {
        return "Hello World!";
    }

}
EOF

Although not directly related to CRaC, to check the Java version information at the /actuator/info endpoint, add the following settings to application.properties.

cat <<EOF > ./src/main/resources/application.properties
management.endpoints.web.exposure.include=health,info
management.info.java.enabled=true
management.info.os.enabled=true
EOF

The Dockerfile is available on Gist. Since it may be updated, use the latest version. Refer to the following link for the implementation.

https://gist.github.com/making/35cfa52862e93793bad2b37b7c0e5135

wget https://gist.githubusercontent.com/making/35cfa52862e93793bad2b37b7c0e5135/raw/Dockerfile
wget https://gist.githubusercontent.com/making/35cfa52862e93793bad2b37b7c0e5135/raw/entrypoint.sh

The Dockerfile is written to build from the source code rather than from a jar (so it can also be used with Tanzu Application Platform).

Therefore, even in an environment without JDK installed, you can build with the following command as long as Docker is available.

docker build -t demo .

Start the container with the following command. In this container, the app is started once, and then a Checkpoint is created. After the Checkpoint is created, the app will terminate. Then, the app is restored from the Checkpoint.

To use CRaC, you need to grant some permissions with --cap-add. You can specify where to save the Checkpoint with the environment variable CHECKPOINT_RESTORE_FILES_DIR.

On OrbStack, an error occurs when creating a Checkpoint, possibly because criu cannot be used.

docker run -p 8080:8080 \
  --cap-add CHECKPOINT_RESTORE \
  --cap-add NET_ADMIN \
  --cap-add SYS_PTRACE \
  --cap-add SYS_ADMIN \
  -v /tmp/crac:/var/crac \
  -e CHECKPOINT_RESTORE_FILES_DIR=/var/crac \
  --rm \
  demo

The following log will be output. The Checkpoint is created 10 seconds after the app starts. This time can be changed with the environment variable SLEEP_BEFORE_CHECKPOINT in entrypoint.sh.

Save checkpoint to /var/crac
Picked up JAVA_TOOL_OPTIONS:  -XX:+ExitOnOutOfMemoryError

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v3.2.0)

2023-12-01T07:47:31.527Z  INFO 10 --- [           main] com.example.demo.DemoApplication         : Starting DemoApplication v0.0.1-SNAPSHOT using Java 21.0.1 with PID 10 (/application/BOOT-INF/classes started by root in /application)
2023-12-01T07:47:31.531Z  INFO 10 --- [           main] com.example.demo.DemoApplication         : No active profile set, falling back to 1 default profile: "default"
2023-12-01T07:47:32.567Z  INFO 10 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port 8080 (http)
2023-12-01T07:47:32.576Z  INFO 10 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2023-12-01T07:47:32.576Z  INFO 10 --- [           main] o.apache.catalina.core.StandardEngine    : Starting Servlet engine: [Apache Tomcat/10.1.16]
2023-12-01T07:47:32.608Z  INFO 10 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2023-12-01T07:47:32.609Z  INFO 10 --- [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 992 ms
2023-12-01T07:47:33.111Z  INFO 10 --- [           main] o.s.b.a.e.web.EndpointLinksResolver      : Exposing 3 endpoint(s) beneath base path '/actuator'
2023-12-01T07:47:33.169Z  INFO 10 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port 8080 (http) with context path ''
2023-12-01T07:47:33.181Z  INFO 10 --- [           main] com.example.demo.DemoApplication         : Started DemoApplication in 2.067 seconds (process running for 2.355)
Picked up JAVA_TOOL_OPTIONS:  -XX:+ExitOnOutOfMemoryError
10:
2023-12-01T07:47:41.076Z  INFO 10 --- [Attach Listener] jdk.crac                                 : Starting checkpoint
CR: Checkpoint ...
/application/entrypoint.sh: line 18:    10 Killed                  java -XX:CRaCCheckpointTo=$CHECKPOINT_RESTORE_FILES_DIR org.springframework.boot.loader.launch.JarLauncher
2023-12-01T07:47:44.605Z  INFO 10 --- [Attach Listener] o.s.c.support.DefaultLifecycleProcessor  : Restarting Spring-managed lifecycle beans after JVM restore
2023-12-01T07:47:44.609Z  INFO 10 --- [Attach Listener] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port 8080 (http) with context path ''
2023-12-01T07:47:44.610Z  INFO 10 --- [Attach Listener] o.s.c.support.DefaultLifecycleProcessor  : Spring-managed lifecycle restart completed (restored JVM running for 35 ms)

You can access the app normally.

$ curl localhost:8080
Hello World!

When you access /actuator/info, you can check the Java information. The vendor.version includes "CRaC-CA", indicating that it is a Java version compatible with CRaC.

$ curl -s localhost:8080/actuator/info | jq .
{
  "java": {
    "version": "21.0.1",
    "vendor": {
      "name": "Azul Systems, Inc.",
      "version": "Zulu21.30+23-CRaC-CA"
    },
    "runtime": {
      "name": "OpenJDK Runtime Environment",
      "version": "21.0.1+12-LTS"
    },
    "jvm": {
      "name": "OpenJDK 64-Bit Server VM",
      "vendor": "Azul Systems, Inc.",
      "version": "21.0.1+12-LTS"
    }
  },
  "os": {
    "name": "Linux",
    "version": "5.15.0-89-generic",
    "arch": "amd64"
  }
}

When you check the directory specified by CHECKPOINT_RESTORE_FILES_DIR, the following files are created.

$ ls -lh /tmp/crac/
total 184M
-rw-r--r-- 1 root root 2.2K Dec  1 04:46 core-10.img
-rw-r--r-- 1 root root  795 Dec  1 04:46 core-12.img
-rw-r--r-- 1 root root  767 Dec  1 04:46 core-13.img
...
-rw-r--r-- 1 root root  795 Dec  1 04:46 core-89.img
-rw-r--r-- 1 root root  790 Dec  1 04:46 core-90.img
-rw-r--r-- 1 root root  782 Dec  1 04:46 core-91.img
-rw------- 1 root root 380K Dec  1 04:46 dump4.log
-rw-r--r-- 1 root root  524 Dec  1 04:46 fdinfo-2.img
-rw-r--r-- 1 root root 6.3K Dec  1 04:46 files.img
-rw-r--r-- 1 root root   18 Dec  1 04:46 fs-10.img
-rw-r--r-- 1 root root   36 Dec  1 04:46 ids-10.img
-rw-r--r-- 1 root root   46 Dec  1 04:46 inventory.img
-rw-r--r-- 1 root root  12K Dec  1 04:46 mm-10.img
-rw-r--r-- 1 root root 7.1K Dec  1 04:46 pagemap-10.img
-rw-r--r-- 1 root root 183M Dec  1 04:46 pages-1.img
-rw-r--r-- 1 root root   98 Dec  1 04:46 pstree.img
-rw-r--r-- 1 root root   12 Dec  1 04:46 seccomp.img
-rw-r--r-- 1 root root   54 Dec  1 04:46 stats-dump
-rw-r--r-- 1 root root   34 Dec  1 04:46 timens-0.img

Stop the container with Ctrl+C.

Start the container again with the following command.

docker run -p 8080:8080 \
  --cap-add CHECKPOINT_RESTORE \
  --cap-add NET_ADMIN \
  --cap-add SYS_PTRACE \
  --cap-add SYS_ADMIN \
  -v /tmp/crac:/var/crac \
  -e CHECKPOINT_RESTORE_FILES_DIR=/var/crac \
  --rm \
  demo

The following log will be output. Since it was restored from the Checkpoint, it started in 30ms.

Restore checkpoint from /var/crac
2023-12-01T07:53:41.374Z  INFO 10 --- [Attach Listener] o.s.c.support.DefaultLifecycleProcessor  : Restarting Spring-managed lifecycle beans after JVM restore
2023-12-01T07:53:41.379Z  INFO 10 --- [Attach Listener] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port 8080 (http) with context path ''
2023-12-01T07:53:41.381Z  INFO 10 --- [Attach Listener] o.s.c.support.DefaultLifecycleProcessor  : Spring-managed lifecycle restart completed (restored JVM running for 27 ms)

You can access the app.

$ curl localhost:8080
Hello World!

As long as you use the same Checkpoint, changes to the code will not be reflected in the app. In the above entrypoint.sh, if you set the environment variable CLEAN_CHECKPOINT to true, the folder will be cleaned once and the Checkpoint will be recreated.

Since the Checkpoint needs to be recreated when the code changes, in practice, CHECKPOINT_RESTORE_FILES_DIR should be dynamically set to correspond to the revision.

For samples using other Spring features, refer to
https://github.com/spring-projects/spring-checkpoint-restore-smoke-tests.
If you use HikariCP, you need to set spring.datasource.hikari.allow-pool-suspension=true, among other considerations.

Found a mistake? Update the entry.