Spring Tanzu アプリケーションのモダナイゼーション

Spring アプリケーションの起動時間”改善”と Cloud Native Buildpacks のすゝめ

Java は他の開発言語と比較しても、開発効率性とパフォーマンスのバランスに長けており、それを理由に採用しているプロジェクトも多いと多います。ところが、アプリケーションの起動時間においてネガティブな印象を与えることが多く、一例としてサーバレスに代表されるような Function as a Service (FaaS) 環境での利用を避けられているなどの実態があります。Spring とそのコミュニティは長らくこの点に着目しており、様々な機能改善を行っております。特に、ソースコードへ影響を全く与えず、あくまで基盤側の設定のみで起動時間の改善が実現できるかがひとつの関心ごとになっています。

このブログでは、Spring アプリケーションの起動時間”改善”について執筆時点での最新状況をお届けします。また、結論だけ先にかいてしまうと、その恩恵にあやかるには Cloud Native Buildpacks を利用することをお勧めします。一見関連がないように見受けられるこの二つがなぜ結びつくのか、気になる方は、ぜひ読み進んでいただければと思います。

なお、このブログでは、GraalVM Native ImageJVM Checkpoint Restore は取り上げません。これらの二つは、Spring の起動時間を”劇的に改善” する代わりに、ソースコード上も多くの考慮点を及ぼす内容です。これらの機能についてはまた別の機会に取り上げたいと思います。

Spring アプリケーション起動時間”改善”の最前線

2023/12 に CDS with Spring Framework 6.1 というタイトルのブログが公開されました。そのブログで、紹介されたのが以下のグラフです。

この中の Executable JAR アプリケーションは以下のように Jar ファイルとしてパッケージとして起動した場合の時間です。

$ java -jar myapp.jar

これを基準として、アプリケーションの起動時間の改善として、以下の3つの手法が紹介されています。

  • Jarファイルの展開 (Unpacked)
  • Class Data Sharing  の利用 (CDS)
  • CDS と Ahead of Time Optimization の適用(CDS + Spring AOT)

ここからは、この三つを解説したいと思います。特に、CDS は Project Leyden の名の下、開発がされている注目のプロジェクトです。

Jar ファイルの展開

一点目が、Jarファイルを展開してから実行する方法です。Springのマニュアルに明確に書かれていることなので、すでにやっている現場も多いとは思います。

Spring Boot でコンパイルしたアプリケーションは Jar ファイルとしてパッケージすることが多いと思います。パッケージした Jar ファイルであれば先ほど紹介した方法 (-jar myapp.jar) で起動することができます。ただし、この起動方法に以下のような一手間を加えることで、起動時間を早くすることができます。 Jar ファイル一度展開(-xf による extract)した状態から起動するというものです。

$ jar -xf myapp.jar
$ java org.springframework.boot.loader.launch.JarLauncher

上のテスト結果から見てわかる通り、これを行うだけで、20% 近いパフォーマンス改善が得られています。もし今の開発現場で行っていなければ、この一手間はぜひ検討してください。

Class Data Sharing  の利用

Class Data Sharing (CDS) とは、 Java のアプリケーションの起動速度向上を狙ったものです。テスト結果にもあるように Executable Jar と比較しても 30% 近いパフォーマンス改善がなされています。

CDS という技術自体は J2SE 5.0 (2004年ごろ) からサポートされている機能です。CDS は Java のクラスのメタ情報をアーカイブファイルとしてキャッシュし、そのアーカイブファイルを使った起動ができる技術です。これによりクラスの読み込みが高速化され、起動時間に寄与することが期待されています。このとき自身のアプリケーションの独自アーカイブを参照できれば、さらなる起動の高速化が期待されます。

仕組み自体をしっていれば、Spring でも CDS を使うことは執筆時点でも可能です。アプリケーション起動時に -XX:ArchiveClassesAtExitというフラグを指定して起動をすれば、停止のタイミングでアーカイブファイルが作成されます。ところが、この方法だと再現性のある停止のタイミングを確保するが困難なため、アーカイブファイルにばらつきが発生します。結果として自動化しにくく、実環境での適用を阻害していました。また、CDS が効果的にアーカイブを作るためのディレクトリ構成があるものの、Spring の展開方法がそれに則していない問題もございました。そこで Spring およびそのコミュニティでは、より CDS の恩恵を受けやすくするために以下のような拡張が追加されました。特に Spring 3.3 によって大きく使いやすくなることが想定されています。

Spring Boot 3.3 からは以下の流れで Spring Boot のアプリケーションであれば CDS の効果を得ることができます。

  1. Spring Boot のアプリケーションを通常の方法でコンパイル
  2. jarmode=tools extract を使い Jar を展開
  3. spring.context.exit=onRefresh を指定してアプリケーションを一度起動し(その後自動的にアプリケーションが停止)、同時に -XX:ArchiveClassesAtExit を指定してアーカイブファイルを生成
  4. 作成されたアーカイブファイルとともにアプリケーションを起動

ポイントがこれが一貫性のある、単純な手順になったことです。CI ツールを使った自動化や Dockerfile などへの組み込みが容易になります。実際に、すでに Dockerfile を使い、CDS を有効にしたコンテナイメージを作成している例が登場していいます。この Dockerfile をみると、上記の解説された手順が再現されていることがわかります。

CDS と Ahead of Time Optimization の適用(CDS + Spring AOT)

ブログで公表されている最後のテスト結果では、AOTに対応したコードにspring.aot.enabled=trueのフラグを追加した上で、アーカイブの作成を行なっています。四つのテストで一番早い起動時間を記録しています。

AOT とは、GraalVM Native Image を使う上でも必要となる知識です。JVM 上のアプリケーションは動的な振る舞いが前提になっています。特に Spring の一つのコンセプトである Auto-Configuration は、稼働環境のプロパティなどに応じて動的に振る舞いをかえることができます。この特徴がゆえに多くのメリットを享受できるものの、起動時間や初回のロードの準備(Warm up)時間にペネルティをうけてしまいます。AOT では、コンパイル時にすべての必要なクラス情報を静的に定義することで、起動時間および準備時間の改善を狙ったものです。この技術と CDS を組みあわせることで、spring.context.exit=onRefreshのタイミングで生成されるアーカイブファイルにより多くのクラス情報が含まれ、さらに起動時間の改善されている点がテスト結果から伺えます。

なお、AOTの場合は、静的に解決できるようなコーディングまたはワークアラウンドが必要になってきます。いずれにせよ、CDS 単体とはことなり、多くの前提知識が必要な点は留意する必要がございます。

まとめ

以上 Spring アプリケーション起動の最前線について、解説させていただきました。多くのイノベーションがおきていることが確認できます。

ここに記載した通り、これらの恩恵をうけるには、それぞれの機能の理解や設定の変更などを実施しなくてはいけません。では、一般の開発者はこういった新しいイノベーションを細かく理解せずとも恩恵を受けるには、どうしたらいいのでしょうか?この次の章では、その恩恵に享受するための Cloud Native Buildpacks について改めて解説します。

Cloud Native Buildpacks のすゝめ

さて、前章では、Spring アプリケーション起動速度”改善”の最前線について解説しました。そして、これらの改善の恩恵をいち早く受けるために、Cloud Native Buildpacks の利用をお勧めしています。

Cloud Native Buildpacks とは、コンテナを作成支援の機能です。Spring のソースコードからコンテナイメージを作る場合は、Dockerfile を使わずに、以下のようなコマンドで作ることが推奨されています。

./mvnw spring-boot:build-image
(Gradle の場合は ./gradlew bootBuildImage )

Dockerfile でもコンテナイメージを作ることができますが、Cloud Native Buildpacks だと様々な機能が追加されています。その中にパフォーマンスを改善させる仕込みもされています。

Jar ファイルの展開を自動化

Cloud Native Buildpacks. は起動時時間”改善”の一点目で記載した、Jar ファイルが展開された状態でコンテナ化を行います。 /workspace 以下のディレクトリには Jar ファイルではなく展開されたものが配置されています。

% docker run --entrypoint=ls --rm docker.io/library/spring-petclinic:2.6.0-SNAPSHOT -Rl /workspace 
/workspace:
total 12
drwxr-xr-x 1 cnb cnb 4096 Jan  1  1980 BOOT-INF
drwxr-xr-x 3 cnb cnb 4096 Jan  1  1980 META-INF
drwxr-xr-x 3 cnb cnb 4096 Jan  1  1980 org

/workspace/BOOT-INF:
total 28
drwxr-xr-x 7 cnb cnb  4096 Jan  1  1980 classes
-rw-r--r-- 1 cnb cnb  5279 Jan  1  1980 classpath.idx
-rw-r--r-- 1 cnb cnb   212 Jan  1  1980 layers.idx
drwxr-xr-x 2 cnb cnb 12288 Jan  1  1980 lib

/workspace/BOOT-INF/classes:
total 40
-rw-r--r-- 1 cnb cnb  316 Jan  1  1980 application-mysql.properties
-rw-r--r-- 1 cnb cnb  297 Jan  1  1980 application-postgres.properties
-rw-r--r-- 1 cnb cnb  739 Jan  1  1980 application.properties
-rw-r--r-- 1 cnb cnb  709 Jan  1  1980 banner.txt
drwxr-xr-x 6 cnb cnb 4096 Jan  1  1980 db
-rw-r--r-- 1 cnb cnb  925 Jan  1  1980 git.properties
drwxr-xr-x 2 cnb cnb 4096 Jan  1  1980 messages
drwxr-xr-x 3 cnb cnb 4096 Jan  1  1980 org
drwxr-xr-x 3 cnb cnb 4096 Jan  1  1980 static
...

CDS への対応

起動時時間”改善”の二点目で記載した、CDS はBuildpack をつかうことで、それに対応したコンテナイメージが作成可能です。ビルド時に BP_JVM_CDS_ENABLED 環境変数を true にすることで CDS に対応したコンテナイメージが作成が行え、BP_SPRING_AOT_ENABLED をさらに true することで、AOTも有効にしたイメージをつくることができます。少ない手順で、CDS の恩恵が得られる強力な機能です。

Memory Calculator によるメモリ管理

起動時間”改善”.に、直接はかかわらないものの、もう一つ知っておくべき機能があります。それが推奨のパラメータを自動的に付与するMemory Calculator 機能です。Javaでは、メモリを効率的につかうための、メモリパラメータの理解が必要です。また、スケールアップが可能な環境では、メモリ割当量におうじて設定したパラメータを追従させる必要があります。Cloud Native Buildpacks の Memory Calculator はそういった点を自動的におこなってくれます。

Cloud Native Buildpacks で作成したコンテナを起動した際に以下のログに着目します。

% docker run --rm  docker.io/library/spring-petclinic:2.6.0-SNAPSHOT 
...
Calculating JVM memory based on 2486304K available memory
For more information on this calculation, see https://paketo.io/docs/reference/java-reference/#memory-calculator
Calculated JVM Memory Configuration: -XX:MaxDirectMemorySize=10M -Xmx1836696K -XX:MaxMetaspaceSize=137607K -XX:ReservedCodeCacheSize=240M -Xss1M (Total Memory: 2486304K, Thread Count: 250, Loaded Class Count: 21881, Headroom: 0%)
...

起動方法はそのままに環境のメモリ(Docker Desktop を 4GB > 8GB に変更)を変化させて起動すると以下のようなメッセージが出力されます。

% docker run --rm  docker.io/library/spring-petclinic:2.6.0-SNAPSHOT 
...
Calculating JVM memory based on 6058960K available memory
For more information on this calculation, see https://paketo.io/docs/reference/java-reference/#memory-calculator
Calculated JVM Memory Configuration: -XX:MaxDirectMemorySize=10M -Xmx5409352K -XX:MaxMetaspaceSize=137607K -XX:ReservedCodeCacheSize=240M -Xss1M (Total Memory: 6058960K, Thread Count: 250, Loaded Class Count: 21881, Headroom: 0%)
...

注目するべき点として Calculated JVM Memory Configuration 以降の値です。搭載メモリ量とロードするクラス数から自動で計算され、 -Xmx などの値は実際のJavaの起動オプションに追加されていきます。理解をしていれば、もちろん自身でも設定はできますが、搭載メモリ量まで把握して、毎回設定するのは骨が折れる作業と思われます。Cloud Native Buildpacks をつかうことで、メモリを効率的に利用するための恩恵を意識することなく得られます。

まとめ

以上 Cloud Native Buildpacks を利用することの恩恵を記載しました。繰り返しですが、これは仕組みさえわかってしまえば、Cloud Native Buildpacks は必要ないですが、仕組みを知らずとも利用できることが最大のメリットといえます。またコンテナフォーマットのため、高い可搬性も維持ができます。Kubernetes はもちろんこと、クラウドが提供するコンテナ基盤、Docker などのコンテナランタイムで同じように起動ができます。

Cloud Native Buildpacks は無料でももちろん使えるものですが、商用サポートが必要な場合、Tanzu Spring Runtimeのサポート対象ですので、ぜひご検討ください。

おわりに

このブログでは、Spring アプリケーション起動時間”改善”の最前線とともに、我々がそれに意識せずとも享受するための Cloud Native Buildpacks を改めておすすめさせていただきました。実際のプログラミングの開発の場面で活用して一歩上の Java ライフを体感いただければ幸いです。