Project Leyden を Quarkus に統合した方法
Quarkus は、それぞれ異なるトレードオフに最適化された JVM モードとネイティブイメージモードの両方を長年サポートしてきました。ネイティブイメージは、比類のない起動時間とフットプリントを実現します。JVM モードは、ピークスループット、動的な機能、成熟したツールエコシステムを提供します。
Quarkus はネイティブイメージとの連携を改善し続けており、最近では Project Leyden のおかげで、この方程式の JVM 側を強化することができました。
以前の ブログ記事 で、Project Leyden と、それが Java アプリケーションの起動パフォーマンスに新たな視点を与えてくれたことへの熱意を共有しました。
この投稿では、Project Leyden を Quarkus に統合するまでの道のり、そしてこの統合をユーザーにとって効率的かつ使いやすいものにするためにどのように取り組んだかを紹介します。
謝辞
まず、Project Leyden に関する取り組みと、Leyden を理解し、Quarkus に統合するための最善の方法を決定する上で非常に役立った多くの議論について、IBM の OpenJDK チームに感謝いたします。具体的には(アルファベット順で)、Maria Arias de Reyna Dominguez 氏、Andrew Dinn 氏、および Ashutosh Mehra 氏です。
より広く、Project Leyden に貢献しているすべての方々に感謝いたします。
Project Leyden はまだ進化途上にあり、Java 27 以降に向けてどのような準備をしているのか楽しみにしています。
最後に、Sanne Grinovero 氏には、適切なタイミングで Leyden チームに紹介していただき、私たちの仕事に対する洞察と貴重なフィードバックをいただいたことに深く感謝いたします。
Project Leyden とは?
GraalVM と Java における AOT の普及
Java の世界では、Java アプリケーションをネイティブ実行可能ファイルにコンパイルする GraalVM ネイティブイメージの登場により、Ahead-of-Time (AOT) コンパイルが注目を集めるようになりました。
これは Java エコシステムに大きな影響を与え、Java アプリケーションの起動パフォーマンスにとって画期的なものでした。
-
起動時間とメモリフットプリントが大幅に削減されます。
-
コードはすでにネイティブコードにコンパイルされているため、ウォームアップフェーズはありません。パフォーマンスは実質的に最初からピークに達します。
-
結果として生成される実行可能ファイルは小さく、より小さなコンテナーイメージにつながります。
-
Resident Set Size (RSS) メモリ使用量は、通常の JVM アプリケーションと比較してはるかに小さくなります。
しかし、これにはいくつかの欠点があります。
-
すべての Java 機能とライブラリが OpenJDK と同じようにそのままサポートされているわけではありません。
-
これはクローズドワールドの仮定に依存し、ほとんどのランタイムダイナミズムを排除します(少なくとも現在の形式では。GraalVM 側の作業によって将来的に変更される可能性はあります)。注記: Quarkus は同じ仮定を置いているため、これは Quarkus にとって問題ではありませんでしたが、サードパーティライブラリにとっては問題となる可能性があります。
-
特定の制約が導入され、最も一般的なのは明示的なリフレクション設定を提供する必要があることです。これらの要件は開発およびテスト中に労力を追加しますが、Quarkus がサポートするエクステンションの場合、それらは最小限に抑えられます。
-
ネイティブイメージのビルドは著しく遅くなります。これは通常、内部開発ループに影響を与えませんが(ネイティブ固有の問題をデバッグする場合を除く)、CI リソースに顕著な影響を与える可能性があります。
-
ネイティブ実行可能ファイルのデバッグは困難です。改善されてはいますが、GDB の使用とビルドにおけるデバッグシンボルの存在が必要であるため、JVM でのデバッグよりもはるかに困難です。
-
そして、すべてが AOT コンパイルされるため、実行時にコードを最適化する JIT コンパイラが存在せず、場合によっては最適なピークパフォーマンスが得られないことがあります。
全体として、ネイティブイメージは起動時間とフットプリントが主要な目標である場合に優れた選択肢です。
しかし、すべてのアプリケーションがこれらの側面を最適化するわけではありません。一部のアプリケーションは、ピークスループット、開発速度、デバッグ容易性、既存ツールとの互換性を優先します。ここで Leyden が興味深いものになります。これは、従来の JVM モデルに近いままで、起動とフットプリントの改善を目指します。
Project Leyden の AOT へのアプローチ
Project Leyden は GraalVM ネイティブイメージと同じ根本的な問題に対処しますが、異なるアプローチを取っています。
-
これは依然として「純粋な」Java であり、OpenJDK の一部です。アプリケーションは JVM 上で実行され、特別な設定なしに Java の機能とライブラリに完全にアクセスできます。
-
トレーニングフェーズ中に、アプリケーションの動作を記録し、ロードされたクラスやリンクされたクラスなどの情報を収集します。より新しいバージョンでは、JIT 出力の一部も記録し始め、各 JVM バージョンでスコープと最適化の可能性を拡大しています。
-
この情報は AOT キャッシュファイルに保存されます。
-
起動時に、この AOT キャッシュファイルを使用して JVM を設定します。
-
アプリケーションは依然として JVM 上で実行されるため、JIT コンパイラ、好みのガーベージコレクタ、およびその他のすべての JVM 最適化の恩恵を受け続け、通常 JVM に関連付けられている高いスループットを維持します。
AOT キャッシュはまさにその通り、キャッシュです。キャッシュにクラスが存在しない場合は、通常通りロードおよびリンクされます。
Project Leyden は、クラスのロードとリンクを最適化することで起動時間を短縮します。JVM にプロファイリング情報を提供することでウォームアップ時間を短縮し、コンパイルされたコード自体がキャッシュに保存されるようになれば、さらに短縮されるでしょう。
とはいえ、Project Leyden も魔法ではありません。現在の形式では以下のようになります。
-
起動時間とウォームアップ時間の改善は著しいですが、GraalVM/Mandrel ネイティブイメージほど実質的ではありません。
-
アプリケーションをトレーニングし、AOT キャッシュを生成する必要があります。実際には、これは難しくなく、Quarkus では可能な限りシームレスにしました。
-
アプリケーションと一緒に JVM を配布する必要があります。
-
また、AOT キャッシュも配布する必要があり、これはコンテナーイメージがネイティブ実行可能ファイルよりも大幅に大きくなることを意味します。
-
現時点ではメモリフットプリントの恩恵は得られません。ほとんどの場合、通常の JVM と同程度です。
私たちの見解では、Project Leyden はパフォーマンスと互換性の魅力的なバランスを非常に合理的なコストで提供し、JVM 上で実行される多くの Java アプリケーションのデフォルトのデプロイメントワークフローの一部となる可能性があります。
Quarkus における Project Leyden
Quarkus は、広範なビルド時処理と最適化を備えた、非常に特化したフレームワークです。Project Leyden もまた、独自の制約と要件を持つ特化されたテクノロジーです。
私たちの目標は、Quarkus の長所を維持しつつ、Leyden の利点を最大限に引き出す形で Project Leyden を Quarkus に統合することでした。
Quarkus 3.32 には、エンドツーエンドの Leyden 統合の最初のバージョンが含まれています。その仕組みを詳しく見ていきましょう。
AOT JAR パッケージング
お使いの Quarkus アプリケーションが target/quarkus-app/ ディレクトリーにパッケージされているのをご覧になっているかもしれませんが、常にそうだったわけではありません。Quarkus の初期の頃は、他のほとんどの Java フレームワークと同様に、依存関係を lib/ ディレクトリーに配置して、target/ に従来のランナー JAR をビルドしていました。
Quarkus の現在のデフォルトパッケージングは fast-jar と呼ばれています。その主要な目標の 1 つは、クラスローディングを最適化することです。
これを達成するために、独自のクラスローダーを導入しました。このクラスローダーは、たとえば、package name → jar file マッピングをメモリーに保持します。また、META-INF/services のような特定の場所の完全なディレクトリーインデックスを維持するなど、追加の最適化も含まれています。これらはこのパッケージングが行うことのほんの一部ですが、いくつかの文脈を提供できるはずです。
このパッケージングは従来の JVM 環境では非常に優れたパフォーマンスを発揮しますが、Project Leyden とはうまく連携しません。
Project Leyden は保守的なアプローチをとっており、標準の JDK クラスローダーによってロードされたクラスのみをキャッシュします。つまり、AOT キャッシュを最大限に活用したい場合は、カスタムクラスローダーは許可されません。
|
いずれかの時点で、この制限が Leyden チームによって解除されることを願っています。もしそうなれば、 |
正確には、AOT キャッシュを最大限に活用したい場合、クラス をロードするためにカスタムクラスローダーを使用することはできません。ただし、リソース のロードのみを目的としてカスタムクラスローダーを使用することはできます。
これが、Project Leyden 専用のパッケージングである aot-jar を開発した理由です。AOT が有効になっている場合、これは自動的に選択され、移行が完全に透過的になります。これは fast-jar パッケージングと同じファイルレイアウトを使用します。
すべての クラス のローディングを JDK クラスローダーに委譲することに加えて、このパッケージングにはいくつかの特定の特性があります。
-
META-INF/servicesからサービスディスクリプターを収集し、それらを統合してメモリーに保持します。 -
Quarkus 設定ファイル (例:
application.properties) にも同じアプローチを適用します。 -
特定のディレクトリー (例: ルートディレクトリー、
META-INF、およびMETA-INF/services) の完全なインデックスを維持します。 -
事前初期化フェーズを導入し、その間に選択された要素が並行して事前ロードされます (例: タイムゾーンデータベース)。
このパッケージング形式は新しいものであるため、実際の使用状況からフィードバックを収集したいと考えています。
|
はっきりさせておきますが、Project Leyden が使用されていない場合、 これは |
トレーニングと AOT キャッシュの生成
AOT キャッシュは、それを生成するために使用されるトレーニングデータの質に依存します。トレーニングフェーズ中、JVM はロードおよびリンクされたクラスを記録し、メソッドプロファイリングデータを収集します。トレーニングワークロードがより代表的であるほど、結果として得られるキャッシュはより効果的です。
私たちは、Quarkus ユーザーにとってトレーニングが可能な限りシームレスであることを望みました。既存の統合テストインフラストラクチャーを活用するのが自然な選択でした。つまり、すでに @QuarkusIntegrationTest テストがある場合、それらをトレーニングワークロードとして利用できます。
AOT トレーニングが有効な場合、以下のフローになります。
-
Quarkus は
aot-jarパッケージングでアプリケーションをビルドします。 -
アプリケーションは AOT トレーニングが有効な状態で起動します。
-
統合テストがアプリケーションに対して実行され、そのエンドポイントと機能を検証します。
-
JVM はテスト実行全体を通してプロファイリングデータをキャプチャーします。
-
記録されたデータから最適化された
app.aotキャッシュファイルが生成されます。
これは実用的かつ効果的です。あなたはすでにアプリケーションを検証するために統合テストを作成しており、それらはAOTキャッシュ用の比較的現実的なトレーニングデータも生成します。
|
|
ビルドシステムとの統合
私たちは、Quarkus で Leyden を有効にするのが、1 つのフラグで済む操作であることを望みました。
quarkus.package.jar.aot.enabled=true を設定するだけで十分です。Quarkus は自動的に aot-jar パッケージングに切り替わり、AOT パイプライン全体を設定します。
Maven では、完全なフローは単一のコマンドです。
./mvnw verify -Dquarkus.package.jar.aot.enabled=true -DskipITs=false
これにより、アプリケーションがビルドされ、AOT トレーニングを伴う統合テストが実行され、target/quarkus-app/ に app.aot キャッシュファイルが生成されます。
|
私たちのツールで生成されたプロジェクトを使用している場合、統合テストはデフォルトで無効になっています。 |
AOT キャッシュを使用してアプリケーションを実行するのも同様に簡単です。
cd target/quarkus-app
java -XX:AOTCache=app.aot -jar quarkus-run.jar
また、AOT をコンテナーイメージエクステンション (Jib、Docker、Podman) と統合しました。AOT を有効にしてコンテナーイメージをビルドする際、Quarkus は次の処理を行います。
-
aot-jarパッケージングでアプリケーションをビルドします。 -
ベースコンテナーイメージを作成します。
-
AOT キャッシュをトレーニングするために、コンテナーに対して統合テストを実行します。
-
AOT キャッシュを含み、起動時にそれを使用するように事前設定された最終的なコンテナーイメージ (バージョンに
-aotサフィックスが付加されます) を生成します。
このエンドツーエンドの統合は、単一のコマンドでソースコードから AOT 最適化されたコンテナーイメージへの移行を意味します。
いくつかの数値
Project Leyden の利点は理論上説明しましたが、実際にはどのように機能するのでしょうか?
それを調べるために、2 つの異なる Quarkus アプリケーションについていくつかの数値を収集しました。
-
quarkus create appを実行したときに得られるシンプルな REST アプリケーション -
非常に大規模な REST CRUD アプリケーション: 1,000 エンティティー、1,000 リポジトリー、1,000 サービス、1,000 REST エンドポイント… 合計で
9,000個の .java ファイル。これは極端なケースなので、自宅では試さないでください!
コンテナーイメージのサイズは、Red Hat の UBI 9 Minimal をベースとし、JDK 25 を使用する当社のデフォルトイメージを使用して測定されました。
生の値
|
これらの数値は、起動時にロードされたすべてのクラスを完全に記録して取得されました。これらは隔離されたラボ環境ではなく、私たちのラップトップで測定されました。近いうちに、ラボでより包括的なベンチマークセットを実行する予定です。 結果はいくつかの理由で異なる場合があります。
いずれにせよ、予期せぬ結果が見られた場合は、ぜひフィードバックをお寄せください。ご連絡いただければ、有用なプロファイリング情報を収集する方法をご案内します。 |
|
RSS は常駐セットサイズ (Resident Set Size) メモリー使用量を指します。 |
小規模 REST アプリケーション
| 起動時間 | 差分 | コンテナーイメージサイズ | 差分 | RSS | 差分 | |
|---|---|---|---|---|---|---|
デフォルト fast-jar |
|
|
|
|
|
|
Project Leyden および aot-jar |
|
|
|
|
|
|
Mandrel ネイティブ実行可能ファイル |
|
|
|
|
|
|
AOT キャッシュファイルのサイズは 39 MB で、Quarkus REST アプリケーションで期待できる最小値です。
大規模 REST CRUD アプリケーション
| 起動時間 | 差分 | コンテナーイメージサイズ | 差分 | RSS | 差分 | |
|---|---|---|---|---|---|---|
デフォルト fast-jar |
|
|
|
|
|
|
Project Leyden および aot-jar |
|
|
|
|
|
|
Mandrel ネイティブ実行可能ファイル |
|
|
|
|
|
|
AOT キャッシュファイルのサイズは 198 MB ですが、アプリケーションに 9,000 個の .java ファイルが含まれていることを考えると、驚くにはあたりません。
起動時間
Leyden は、小規模アプリケーションと非常に大規模なアプリケーションの両方で起動時間を大幅に改善します。ネイティブ実行可能ファイルの起動速度には及ばないものの、その結果は依然として素晴らしいものです。
ここで少し立ち止まりましょう。JVM で Quarkus REST アプリケーションをわずか 80 ms で起動できました。
確かに、単一のエンドポイントを持つシンプルな REST アプリケーションですが、それでもフル機能の REST 実装に依存しています。そして、JVM で動作します。
非常に大規模なアプリケーションでも、結果は同様に印象的です。起動時間を 71% 削減できました。
コンテナーイメージサイズ
驚くことではありませんが、Container Image に AOT キャッシュファイルを含める必要もあるため、Leyden はイメージサイズを増加させます。
Quarkus REST アプリケーションの場合、最小サイズは約 40 MB です。アプリケーションが大きくなるにつれてキャッシュサイズも大きくなります。
一方、ネイティブ化すると、完全な JVM を含める必要がなくなるため、Container Image サイズが大幅に削減されます。
メモリフットプリント
RSS の観点では、Project Leyden は状況を大きく変えませんが、ネイティブ実行可能ファイルは大幅に低いメモリフットプリントを提供します。
メモリ使用量の削減は、今のところ Project Leyden の主要な目標ではありませんが、将来的に進化する可能性があります。
一歩下がって考えてみましょう
この時点で、「すべてをネイティブにしないのはなぜだろう?」と考えているかもしれません。私たちが概説した すべての理由から、あなたにとって意味のある場合にネイティブを使用してください。
スループットの問題もあります。適切なセットアップがなかったため、私たちはスループットの数値を独自に収集していませんが、一部の同僚が隔離された環境でテストを実行しており、最近 新しいベンチマーク結果 を公開しました。ネイティブ実行可能ファイルは、少なくとも Mandrel と GraalVM CE で今日オープンソースとして利用できるものについては、通常、スループットが低くなる傾向があります。
ここが Project Leyden が真価を発揮する点です。通常の JVM と比較して起動時間が大幅に高速になりながらも、JVM で実行することのすべての利点を保持します。
Quarkus における Leyden の次なる展開は?
Project Leyden は積極的に進化しており、私たちはその開発を密接に追跡しています。特に OpenJDK Leyden リポジトリの premain ブランチを通じて、実験的な機能が主流の JDK リリースに統合される前に開発されています。
改善が期待されるいくつかの領域があります。
-
JIT コンパイルされたコードを AOT キャッシュに保存することで、ウォームアップ時間がさらに短縮されます。
-
カスタムクラスローダーの潜在的なサポートにより、より最適化された
fast-jarパッケージングに戻れるようになります。
Quarkus 側では、以下の点にも取り組んでいます。
-
Windows を含む追加プラットフォームでのテストと検証。
Project Leyden が成熟するにつれて、私たちは新しい機能を Quarkus に統合し続けます。Java 27 以降がもたらすものに期待しています。
まとめ
Project Leyden を Quarkus に統合するための実験と作業を1ヶ月間行った後、私たちはこのテクノロジーの結果と可能性に非常に興奮しています。そして、Quarkus 3.32 に統合できたことを大変嬉しく思っており、皆様はすでにそれを使って試すことができます。
Project Leyden は継続的に改善されており、Java プラットフォームの将来において重要な位置を占める可能性が高いです。最高の開発プラットフォームを提供するという Quarkus の探求の一環として、IBM の OpenJDK チームの協力を得て、私たちはアップストリーム( git@github.com:openjdk/leyden.git の premain ブランチ)での進捗状況を積極的に監視しており、Quarkus と Leyden のエクスペリエンスをそれに応じて形成し続ける予定です。
AOT サポートにより、Quarkus はそのランタイムの柔軟性を拡張します。プレーンな JVM、Leyden 最適化 JVM、そしてネイティブイメージです。
これらのモードは互いに置き換わるのではなく、トレードオフのスペクトルを形成します。ネイティブイメージは起動と最小フットプリントにおいて比類がありません。Leyden は JVM の起動の利点を強化します。
どれが最適かは、あなたの優先事項、制約、およびアプリケーションのワークロードによって異なります。
私たちのコミットメントは変わっていません。開発者にそのワークロードに最適なオプションを提供し、各パスを改善し続けることです。
参加のお誘い
私達は皆様からのフィードバックに重きを置いています。バグ報告、改善要望を是非お願いします。一緒に素晴らしいものを作り上げていきましょう!
Quarkusユーザーの場合でも、単に興味を持っているだけの場合でも、恥ずかしがらずにコミュニティに参加して下さい!:
-
GitHub でフィードバック
-
コードを作成し、 プルリクエスト を送信
-
Stack Overflow で質問