Spring Framework 6.1 / Spring Boot 3.2でCRaC (Coordinated Restore at Checkpoint)の初期サポートが行われました。
CRaCを試すためのDockerイメージを作るメモです。
JJUG CCC 2023 Fallで話したVirtual Threads! Checkpoint Restore! Javaの進化に対応する Spring Framework 6.1 / Spring Boot 3.2の注目機能紹介でも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によって管理されています。
<dependency>
<groupId>org.crac</groupId>
<artifactId>crac</artifactId>
</dependency>
次の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
CRaCとは直接関係ありませんが、/actuator/info
エンドポイントでJavaのバージョン情報を確認したいので、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
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でも使えるように)
ですので、JDKがインストールされていない環境でもDockerさえあれば次のコマンドでビルド可能です。
docker build -t demo .
次のコマンドでコンテナを起動します。このコンテナでは一度アプリを起動した後に、Checkpointを作成します。Checkpointを作成が終わるとアプリは終了します。その後、CheckpointからアプリをRestoreします。
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
が必要であるなど、注意が必要な場合があります。