--- 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/)の初期サポートが行われました。
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`に次の``を追加します。バージョンはSpring Bootによって管理されています。 ```xml org.crac crac ``` 次の`HelloController`を追加します。 ```java cat < ./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 < ./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`が必要であるなど、注意が必要な場合があります。