---
title: CRaCに対応したSpring BootアプリのDockerイメージを作るメモ
tags: ["Spring Boot", "Spring MVC", "Java", "CRaC"]
categories: ["Programming", "Java", "org", "springframework", "boot"]
date: 2023-12-01T10:45:18Z
updated: 2023-12-01T11:20:34Z
---

Spring Framework 6.1 / Spring Boot 3.2で[CRaC (Coordinated Restore at Checkpoint)](https://openjdk.org/projects/crac/)の初期サポートが行われました。<br>
CRaCを試すためのDockerイメージを作るメモです。

> JJUG CCC 2023 Fallで話した[Virtual Threads! Checkpoint Restore! Javaの進化に対応する Spring Framework 6.1 / Spring Boot 3.2の注目機能紹介](https://docs.google.com/presentation/d/17RdIx4ysrebxOTrGrBl2pr7yn9-T2krhyi9833C8pBE/edit#slide=id.p)でもCRaCについて触れています。

まずは雛形プロジェクトを作成します。

```
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
```

CRaCのAPIを追加するため、`pom.xml`に次の`<dependency>`を追加します。バージョンはSpring Bootによって管理されています。

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

次の`HelloController`を追加します。

```java
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
```

CRaCとは直接関係ありませんが、`/actuator/info`エンドポイントでJavaのバージョン情報を確認したいので、`application.properties` に次の設定を追加します。

```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
```

`Dockerfile`はGistに置いてあります。更新する可能性があるので、最新版を使用します。実装は次のリンク先を参照してください。

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
```

`Dockerfile`はjarからではなく、ソースコードからビルドするように記述されています。(Tanzu Application Platformでも使えるように)<br>
ですので、JDKがインストールされていない環境でもDockerさえあれば次のコマンドでビルド可能です。

```
docker build -t demo .
```

次のコマンドでコンテナを起動します。このコンテナでは一度アプリを起動した後に、Checkpointを作成します。Checkpointを作成が終わるとアプリは終了します。その後、CheckpointからアプリをRestoreします。<br>
CRaCを使うには`--cap-add`でいくつかの権限を与える必要があります。環境変数`CHECKPOINT_RESTORE_FILES_DIR`でCheckpointを保存する場所を指定できます。

> OrbStackではcriuが使えないのか、Checkpoint作成時にエラーが発生します。

```
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
```

次のようなログが出力されます。Checkpointは、アプリの起動から 10秒後に作成されます。 この時間は、`entrypoint.sh`の環境変数`SLEEP_BEFORE_CHECKPOINT`で変更できます。

```
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)
```

アプリには普通にアクセス可能です。

```
$ curl localhost:8080
Hello World!
```

`/actuator/info` にアクセスするとJavaの情報を確認できます。`vendor.version`に"CRaC-CA"が含まれており、CRaCに対応したJavaであることがわかります。

```
$ 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"
  }
}
```


`CHECKPOINT_RESTORE_FILES_DIR` で指定したディレクトリを確認すると次のようなファイルが作成されています。


```
$ 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
```

Ctrl+Cでコンテナを止めます。

再度次のコマンドでコンテナを起動します。

```
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
```

次のようなログが出力されます。CheckpointからRestoreしたので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)
```

アプリにアクセスできます。

```
$ curl localhost:8080
Hello World!
```

同じCheckpointを使う限り、コードを変更してもアプリに反映されません。上記の`entrypoint.sh`では環境変数`CLEAN_CHECKPOINT`を`true`に設定するとフォルダを一度一掃してからCheckpointを作り直します。

コードが変わるとCheckpointを作り直す必要があるので、実際には`CHECKPOINT_RESTORE_FILES_DIR`がrevisionと対応するように動的に設定しないといけないと思われます。


Springの他の機能を使う場合のサンプルは
https://github.com/spring-projects/spring-checkpoint-restore-smoke-tests
を参照してください。HikariCPを使う場合は`spring.datasource.hikari.allow-pool-suspension=true`が必要であるなど、注意が必要な場合があります。
