Candy, Vitamin or Painkiller

He's half man and half machine rides the metal monster

CRaCを用いたJavaアプリケーションの起動高速化

検索エンジンプロダクトを一緒に開発してた同窓会 Advent Calendar 2023 の9日目です。

Javaアプリケーションの起動を高速化する技術として、Application CDSGraalVM Native ImageによるAOTコンパイルなどがあります。

CRaC(Coordinated Restore at Checkpoint) もまた、Javaアプリケーションの起動高速化を目的としたOpenJDKのプロジェクトです。CRaCはLinuxCRIUを利用し、実行中のJavaアプリケーションの状態をスナップショットとして保存します。スナップショットからはアプリケーションを高速に復元することができます。

今回はSpring Bootアプリケーションのスナップショットを作成し、スナップショットからアプリケーションを起動してみます。

CRaCをサポートするJDKのインストール

今回は Liberica JDK with CRaC を利用します。
私は SDKMAN でインストールしました。

$ sdk install java 17.0.9.crac-librca
$ sdk use java 17.0.9.crac-librca

確認します。

$ sdk current java

Using java version 17.0.9.crac-librca

CRaCののドキュメント に従い、実行ユーザがCRIUを利用できるように lib/criuパーミッションを変更します。

$ sudo chown root:root jdk/lib/criu
$ sudo chmod u+s jdk/lib/criu

helpが表示できれば準備OKです。

$ $JAVA_HOME/lib/criu --help

環境によってはSELinuxのインストールが必要かもしれません。

$ $JAVA_HOME/lib/criu --help
/home/todokr/.sdkman/candidates/java/17.0.9.crac-librca/lib/criu: error while loading shared libraries: libselinux.so.1: cannot open shared object file: No such file or directory
$ ldd $JAVA_HOME/lib/criu
...
libselinux.so.1 => not found
...

Spring Boot アプリケーションの用意

spring-boot-starter-web をベースにプロジェクトを作成します。
依存ライブラリに org.crac:crac を追加し、SpringがCRaCを利用できるようにします。

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("org.crac:crac")
    ...
}

アプリケーションの初回起動と復元の差を際立たせるため、mainメソッド内にsleepを入れておきました。

@SpringBootApplication
class SpringBootWithCracApplication

fun main(args: Array<String>) {
    println("Starting app...")
    Thread.sleep(3000)
    runApplication<SpringBootWithCracApplication>(*args)
}

コードの全体はこちらのリポジトリ にあります。

初回起動とスナップショット作成

アプリケーションをビルドして起動します。CRaCCheckpointTo にはスナップショットを保存するディレクトリを指定します。

$ ./gradlew build
$ java -XX:CRaCCheckpointTo=checkpoint -jar ./build/libs/spring-boot-with-crac-0.0.1-SNAPSHOT.jar
Starting app...
...
[main] c.e.s.SpringBootWithCracApplicationKt    : Started SpringBootWithCracApplicationKt in 0.997 seconds (process running for 4.2)

初回起動はsleepの3秒込みで 4.2 秒かかりました。

jcmd でスナップショットを作成します。スナップショット作成後、アプリケーションは停止します。

$ jcmd spring-boot-with-crac JDK.checkpoint
1710359:
CR: Checkpoint ...

CRaCCheckpointTo に指定したディレクトリ内にファイルがモリモリと作成されています。
これらは先ほどのアプリケーションのJVMメモリがダンプされたものです。

$ ls -l ./checkpoint
core-1712983.img  core-1712998.img  core-1713046.img  dump4.log
core-1712984.img  core-1712999.img  core-1713047.img  fdinfo-2.img
...
core-1712994.img  core-1713042.img  core-1713068.img  stats-dump

スナップショットからのアプリケーション起動

スナップショットから起動してみます。

$ java -XX:CRaCRestoreFrom=checkpoint
[Attach Listener] o.s.c.support.DefaultLifecycleProcessor  : Restarting Spring-managed lifecycle beans after JVM restore
[Attach Listener] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port 8080 (http) with context path ''
[Attach Listener] o.s.c.support.DefaultLifecycleProcessor  : Spring-managed lifecycle restart completed (restored JVM running for 60 ms)

アプリケーションが正常に起動したことがわかります。
ログにあるように、アプリケーションの起動にかかった時間はわずか 60ms でした。

プロダクションでの利用にはCI/CDのや各種リソースのクローズ/再作成、スナップショットにシリアライズされた機密情報の管理など、考慮が必要な事項が多々ありますが、今後の発展が非常に楽しみな技術に思えました。