Building Class Data Sharing Archives with Apache Maven

原文はこちら。
The original article was written by Gunnar Morling (Principal Software Engineer, Red Hat).
https://www.morling.dev/blog/building-class-data-sharing-archives-with-apache-maven/

Ahead-of-timeコンパイル (AOT) は最近Javaエコシステムで大きな話題になっています。Javaコードをネイティブバイナリにコンパイルすることで、開発者とユーザーが大幅に改善された起動時間とメモリ利用量の削減というメリットを享受できます。GraalVMプロジェクトがAOTコンパイルされたJavaアプリケーションでの多大なる進展に寄与し、Project Leydenが将来のJavaプラットフォームでAOTの標準化を約束しています。

GraalVM
https://graalvm.org
Project Leyden
https://mail.openjdk.java.net/pipermail/discuss/2020-April/005429.html

このために、最近のJavaバージョンでJVM上で行われた大幅なパフォーマンスの改善、特に起動時間の高速化を見逃しがちです。

OpenJDK Startup – Late 2019 Edition
https://cl4es.github.io/2019/11/20/OpenJDK-Startup-Update.html
https://logico-jp.io/2019/12/22/openjdk-startup-late-2019-edition/

クラスのロード、リンク、バイトコードの検証に関連するさまざまな改善に加えて、クラスデータ共有(CDS)に関する実質的な作業が行われてきました。

クラスデータ共有 / Class Data Sharing
https://docs.oracle.com/javase/jp/14/vm/class-data-sharing.html#GUID-7EAA3411-8CF0-4D19-BD05-DF5E1780AA91
https://docs.oracle.com/en/java/javase/14/vm/class-data-sharing.html#GUID-7EAA3411-8CF0-4D19-BD05-DF5E1780AA91

起動時間の高速化は、開発時のターンアラウンドタイムの短縮、コールドスタートシナリオでのユーザーの最初の応答までの時間の短縮、クラウドのCPU時間で課金される場合のコスト削減など、多くの点でメリットがあります。

CDSを使うと、クラスのメタデータはアーカイブファイルに保存され、その後のアプリケーション起動時にメモリにマッピングされます。これは、実際のクラスファイルをロードするよりも高速で、結果として起動時間の短縮につながります。同じホスト上で複数のJVMプロセスを起動する場合、クラスメタデータの読み取り専用アーカイブをVM間で共有することができるため、全体的に消費されるメモリ量が少なくなります。

元々はOracle JDKの一部商用機能であったCDSは、JDK 10で完全にオープンソース化され、それ以降、一連のJava改善提案の中で段階的に改善されてきました。

JEP
JEP 310 : Application Class-Data Sharing (AppCDS)
https://openjdk.java.net/jeps/310
JDK 10
起動時間とメモリフットプリントを改善するため、既存の機能(CDS)を拡張してアプリケーションクラスを共有アーカイブに配置できるようにする
JEP 341 : Default CDS Archives
https://openjdk.java.net/jeps/341
JDK 12
JDKビルドプロセスを改善し、デフォルトのクラスリストを使って64ビットプラットフォームのクラスデータ共有(CDS)アーカイブを生成する
JEP 350 : Dynamic CDS Archives
https://openjdk.java.net/jeps/350
JDK 13
アプリケーションクラスデータ共有(AppCDS)を拡張して、Javaアプリケーション実行の終了時にクラスの動的アーカイブを可能にする。アーカイブされたクラスには、デフォルトのベースレイヤーCDSアーカイブには存在しない、全てのロードされたアプリケーションクラスとライブラリクラスが含まれる。

このブログ記事の残りの部分では、JEP 350で行われた改善に基づいて、(Mavenの)プロジェクトビルドの一部としてAppCDSアーカイブを自動的に作成する方法について説明します。つまり、Java 13 以降が前提条件です。現在の LTS リリース JDK 11 での CDS の使用と CDS 一般についての詳細は、Nicolai Parlog による CDS に関する優れたブログ記事を参照してください。

Improve Launch Times On Java 13 With Application Class-Data Sharing
https://nipafx.dev/java-application-class-data-sharing/

Manually Creating CDS Archives

最初に、AppCDSアーカイブを手動で作成して使用するために必要なものを見てみましょう(簡潔にするために、”AppCDS”と “CDS”を多少入れ替えて使用することに注意してください)。その後、Mavenプロジェクトのビルドでタスクを自動化する方法について説明します。

平凡な”Hello World”を超えたサンプルとして、Quarkusスタックを使用して、個人のToDo管理のための小さなWebアプリケーションを作成しました。一緒にやってみたい方は、リポジトリをクローンしてプロジェクトをビルドしてください。

Quarkus with AppCDS
https://github.com/gunnarmorling/quarkus-cds/
Quarkus – supersonic subatomic Java
https://quarkus.io/

git clone git@github.com:gunnarmorling/quarkus-cds.git
cd quarkus-cds
mvn clean verify -DskipTests=true

このアプリケーションではToDoを保存するためにPostgres Databaseを使います。Dockerを使って起動します。

cd compose
docker run -d -p 5432:5432 --name pgdemodb \
    -v $(pwd)/init.sql:/docker-entrypoint-initdb.d/init.sql \
    -e POSTGRES_USER=todouser \
    -e POSTGRES_PASSWORD=todopw \
    -e POSTGRES_DB=tododb postgres:11

続いて、アプリケーションを実行し、CDSアーカイブファイルを作成するため、-XX:ArchiveClassesAtExitオプションを指定して実行します。

java -XX:ArchiveClassesAtExit=target/app-cds.jsa \   # ①
    -jar target/todo-manager-1.0.0-SNAPSHOT-runner.jar
アプリケーション終了時に、オプションで指定した場所にCDSアーカイブ作成するように指示

ロードされたクラスのみがアーカイブに追加されます。JVMでのクラスロードは遅延実行されるので、関連するすべてのクラスがロードされるようにするためには、アプリケーション内で何らかの機能を呼び出す必要があります。そのためには、ブラウザでアプリケーションのAPIエンドポイントを開くか、curlhttpieなどで呼び出す必要があります。

http localhost:8080/api

Ctrl+Cを押してアプリケーションを停止します。これにより、target/app-cds.jsaにCDSアーカイブが作成されます。この例では約41MB のサイズになるはずです。また、アーカイブからスキップされたクラスに関するログメッセージを見ておいてください。

...
[190.220s][warning][cds] Skipping java/lang/invoke/LambdaForm$MH+0x0000000800bd0c40: Hidden or Unsafe anonymous class
[190.220s][warning][cds] Skipping java/lang/invoke/LambdaForm$DMH+0x0000000800fdc840: Hidden or Unsafe anonymous class
[190.220s][warning][cds] Pre JDK 6 class not supported by CDS: 46.0 antlr/TokenStreamIOException
...

ほとんどの場合、これはアーカイブできない隠れたクラスや匿名のクラスに関するものです。これについてできることはあまりありません(Lambda 式の使用量を減らすことを除けば…)。

古いクラスファイルのバージョンに関するヒントはより実用的です。クラスファイルフォーマット 50(= JDK 1.6)以降のクラスのみが CDS でサポートされています。このケースでは、Antlr 2.7.7.7のクラスは(Java 1.2で導入されたことを示す)クラスファイルフォーマット46を使用しているため、CDSアーカイブに追加することができません。これは、たとえサブクラスが新しいクラスファイル形式のバージョンを使っていたとしても同様です。

ANTLR 2.7.7
https://mvnrepository.com/artifact/antlr/antlr/2.7.7


したがって、依存関係の新しいバージョンにアップグレードできるかどうかを確認するのは良いアイデアです。これにより、CDS で利用可能なクラスが増え、起動時間を短縮できる可能性があるからです。

Using the CDS Archive

では、再度アプリケーションを実行しましょう。今度は以前作成したCDSアーカイブを利用します。

java -XX:SharedArchiveFile=target/app-cds.jsa \  # ①
    -Xlog:class+load:file=target/classload.log \  # ②
    -Xshare:on \  # ③
    -jar target/todo-manager-1.0.0-SNAPSHOT-runner.jar
CDSアーカイブファイルのパス
CDSアーカイブが想定通りに適用されているかどうかを確認するために利用できるクラスローディングのログファイル
JDK 12以後ではクラスデータ共有がデフォルトで有効化されているが、これを明示的に強制することで、何か問題が発生した場合(例えばビルドとアーカイブ利用の間のJavaバージョンの不一致など)に確実にエラーが発生するようにすることを確実にする

classload.logファイルを調べると、ほとんどのクラスのメタデータがCDSアーカイブ(”source: shared object file”)から取得されているのに対し、古いAntlrクラスのようないくつかのクラスは、対応するJARから通常通りロードされていることがわかります。

[0.016s][info][class,load] java.lang.Object source: shared objects file
[0.016s][info][class,load] java.io.Serializable source: shared objects file
[0.016s][info][class,load] java.lang.Comparable source: shared objects file
[0.016s][info][class,load] java.lang.CharSequence source: shared objects file
...
[2.555s][info][class,load] antlr.Parser source: file:/.../antlr.antlr-2.7.7.jar
...

アーカイブの作成時と全く同じ Java バージョンを仕様することが重要です。同じバージョンでないとエラーが発生することに注意してください。残念ながら、これはつまり、AppCDSアーカイブをクロスプラットフォームで構築できないことも意味しているのですが、クロスプラットフォームでビルドできると非常に便利ですね。例えば、macOSやWindows上でJavaアプリケーションを構築する際に、そのアプリケーションをLinuxコンテナでパッケージ化する必要がある場合、とか。もしそうする方法をご存知でしたら、以下のコメント欄で教えてください。

CDS and the Java Module System
Java 11以降、クラスパスからのクラスだけでなく、モジュール化されたJavaアプリケーションのモジュールパスからのクラスもCDSアーカイブに追加できます。ここで考慮すべき重要な詳細の一つは、 --upgrade-module-path--patch-module オプションが指定された場合、CDS が無効化されるか、または (-Xshare:on が指定された場合) 無効化されるということです。これは、CDS アーカイブのクラスメタデータと新しいモジュールのバージョンによってもたらされるクラスの不一致を避けるためです。

Creating CDS Archives in Your Maven Build

手動でCDSアーカイブを作成するのはあまり効率的ではありませんし、信頼性も低いので、プロジェクトのビルドの一部としてタスクを自動化する方法を見てみましょう。以下は Apache Maven を使用した場合に必要な設定を示していますが、もちろん Gradle や他のビルドシステムでも同じアプローチで実装できます。

基本的な考え方は、先ほどと同じ手順なのですが、Mavenビルドの一部として実行します。

  1. -XX:ArchiveClassesAtExitオプションでアプリケーションを起動します。
  2. 関連するすべてのクラスのロードを開始するためのアプリケーション機能を呼び出します。
  3. アプリケーションを停止します。

CDS アーカイブを通常のテスト実行の一部として、例えば JUnit 経由で作成するというのは、説得力のあるアイデアのように思われるかもしれません。しかしながら、これはうまくいきません。なぜなら、CDS アーカイブを使用する際のクラスパスは、CDSアーカイブ作成時はクラスパスからのエントリを見逃さないようにしなければならないからです。テスト実行時には、すべてのテストスコープされた依存関係がクラスパスの一部になるので、そのようにして作成された CDS アーカイブは、テスト依存関係のないアプリケーションを後で実行したときには使えなくなります。

ステップ1.と3.は、Process-Exec Mavenプラグインの助けを借りて、それぞれ統合前テストと統合後テストのビルドフェーズにバインドして自動化できます。最初は、より広く知られている Exec プラグインを使おうと考えていましたが、後のビルドフェーズでフォークされたプロセスを停止する方法がないため、これは実行不可能であることがわかりました。

process-exec-maven-plugin
https://github.com/bazaarvoice/maven-process-plugin
Exec plug-in
https://www.mojohaus.org/exec-maven-plugin/usage.html
Spawn non-blocking #18
https://github.com/mojohaus/exec-maven-plugin/issues/18

以下は設定内容です。

...
<plugin>
  <groupId>com.bazaarvoice.maven.plugins</groupId>
  <artifactId>process-exec-maven-plugin</artifactId>
  <version>0.9</version>
  <executions>
      <execution>  <!-- ① -->
        <id>app-cds-creation</id>
        <phase>pre-integration-test</phase>
        <goals>
          <goal>start</goal>
        </goals>
        <configuration>
          <name>todo-manager</name>
          <healthcheckUrl>http://localhost:8080/</healthcheckUrl>  <!-- ② -->
          <arguments>
            <argument>java</argument>  <!-- ③ -->
            <argument>-XX:ArchiveClassesAtExit=app-cds.jsa</argument>
            <argument>-jar</argument>
            <argument>
              ${project.build.directory}/${project.artifactId}-${project.version}-runner.jar
            </argument>
          </arguments>
        </configuration>
      </execution>
      <execution>  <!-- ④ -->
          <id>stop-all</id>
          <phase>post-integration-test</phase>
          <goals>
              <goal>stop-all</goal>
          </goals>
      </execution>
  </executions>
</plugin>
...
pre-integration-test ビルドフェーズでアプリケーションを始動
ヘルスチェックURLを使って次のビルドフェースに進む前にアプリケーションの始動を待機
Javaの呼出しを組み立てる
post-integration-test ビルドフェースでアプリケーションを停止

残っているのは、ステップ2の自動化で、必要なアプリケーションロジックを呼び出して、すべての関連するクラスのロードを開始することです。これは、Maven Surefireプラグインを使って実現できます。REST Assuredを使ったシンプルな統合テストでトリックを実行します。

Maven Surefire plug-in
https://maven.apache.org/surefire/maven-surefire-plugin/
REST-Assured
https://rest-assured.io/

public class ExampleResourceAppCds {
  @Test
  public void getAll() {
    given()
      .when()
        .get("/api")
      .then()
        .statusCode(200);
    }
}

名前が*AppCds.javaで終わるテストクラスのみをピックアップするという、プラグインの特定の実行を設定するだけで、実際の統合テストから切り離すことができます。

...
<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-failsafe-plugin</artifactId>
  <version>3.0.0-M4</version>
  <executions>
    <execution>
      <goals>
        <goal>integration-test</goal>
        <goal>verify</goal>
      </goals>
      <configuration>
        <includes>
          <include>**/*AppCds.java</include>
        </includes>
      </configuration>
    </execution>
  </executions>
</plugin>
...

必要なのはこれだけです。mvn clean verifyでプロジェクトをビルドすると、CDSアーカイブがtarget/app-cds.jsaに作成されます。完全なサンプルプロジェクトとビルド/実行の手順はGitHubで見ることができます。

Quarkus with AppCDS
https://github.com/gunnarmorling/quarkus-cds/

What Do You Gain?

CDS アーカイブを作成するのは良いことですが、それだけの価値があるのでしょうか?この質問に答えるために、パフォーマンスの測定に関するQuarkusのガイドに従って、「最初の応答までの時間」のメトリックをいくつか測定してみました。

How do we measure startup time
https://quarkus.io/guides/performance-measure#how-do-we-measure-startup-time

つまり、遅延初期化によって任意に微調整することができる無意味な “start-up complete “ステータスを待つのではなく、アプリケーションが起動後に最初に入ってくるリクエストを処理する準備が整うまでの時間を測定しています。

OpenJDK 1.8.0_252(AdoptOpenJDK ビルド)、OpenJDK 14.0.1 (アップストリームビルド、AppCDSなしおよび AppCDSあり)、OpenJDK 15-ea-b26 (アップストリームビルド、AppCDS 付き)で測定しました。正確な手順については、サンプルリポジトリの README ファイルを参照してください。

AdoptOpenJDK
https://adoptopenjdk.net/
JDK 14 Releases
http://jdk.java.net/14/
JDK 15.0.1 General-Availability Release
http://jdk.java.net/15/
Run Measurements
https://github.com/gunnarmorling/quarkus-cds/#run-measurements

以下は、それぞれ10回の実行で平均した数値です。

app cds time to first response

【2020/6/12更新】当初OpenJDK 14(AppCDSあり)の実行でクラスロードのロギングを有効にしていましたが、それが不必要なオーバーヘッドを追加していました(これを指摘してくれた Claes Redestad に感謝します!)。数値とチャートはそれに応じて更新しました。OpenJDK 15-ea の数値も追加しました。

最初のレスポンスまでの時間は、2.267sec、2.162sec、1.669sec、1.483sec、1.279secです。つまり、私のマシン(2014 MacBook Pro)では、この特定のワークロードでは、現行のJDKにアップグレードするだけで~100ms、AppCDSを使用することでさらに~500ms~700msの改善が見られます。

OpenJDK15ではさらに改善されます。本稿執筆時点での最新のEAビルド(b26)では、最初のレスポンスまでの時間がさらに200msec短縮されています。次のEAビルド27では、LambdaプロキシクラスがAppCDSのアーカイブに追加されるので、さらに改善されるはずです。

JDK-8198698 Support Lambda proxy classes in dynamic CDS archive
https://bugs.openjdk.java.net/browse/JDK-8198698

これらすべては間違いなく素晴らしい改善です。特に、基本的に無料で手に入れることができ、しかも実際のアプリケーションには変更を加える必要がありません。しかし、アプリケーションの配布物の追加サイズと対比して考慮する必要があります。例えば、リモートのコンテナレジストリからコンテナイメージとしてアプリケーションを入手する場合、追加の ~40MB のダウンロードには、アプリケーションの起動時に節約された時間よりも長い時間がかかるかもしれません。一般的に、これは特定のノードの最初の起動時にのみ影響しますが、その後、イメージはローカルにキャッシュされてしまいます。

パフォーマンスの数値については、いつものように、これらの数値は当てにならないものとして考え、ご自身のアプリケーションを使用し、ご自身の環境で測定してください。

異なるワークロードプロファイルへの対応
アプリケーションが異なる「作業モード」をサポートしている場合、例えば「オンライン」と「バッチ」のように、大きく異なるクラスのセットで動作する場合、特定のワークロードのために異なる CDS アーカイブを作成することを検討するかもしれません。これは、例えば、より微細なマイクロサービスではなく、大規模なモノリシックアプリケーションを扱う場合に、追加のサイズと起動時間の改善のバランスをとることができるかもしれません。

Wrap-Up

AppCDSは、アプリケーションの起動時間を短縮するための便利なツールをJava開発者に提供します。利用にあたりコードの変更は不要です。今回扱った例では、OpenJDK 14の場合、最初のレスポンスまでの時間の指標が約 30% 改善されたことがわかりました。他のユーザーからはさらに大きな改善の報告を受けています。

Starting Quarkus 40% faster with CDS
https://groups.google.com/d/msg/quarkus-dev/c10cGsXriI8/TJvn6QRTAwAJ

1つのホスト上の複数のJVM間でクラスメタデータを共有する場合における、CDSによるメモリの改善の可能性については触れませんでした。コンテナ化されたサーバアプリケーションでは、各JVMが独自のコンテナイメージにパッケージ化されているため、これが役割を果たすことはありません。しかし、デスクトップシステムでは違いが出るかもしれません。例えば、VSCodeや他のエディタが活用しているJava言語サーバの複数のインスタンスは、その恩恵を受ける可能性があります。

Language support for Java™ for Visual Studio Code
https://github.com/redhat-developer/vscode-java

つまり、素の起動時間が主な関心事である場合、例えばサーバーレスや関数ベースの設定では、GraalVM(または将来的にはProject Leyden)を使ったAOTコンパイルをチェックするべきです。これにより、起動時間を全く異なるレベルにまで短縮することができます。例えば、Todo管理アプリケーションは、GraalVM経由でネイティブイメージとして実行された場合、数10ミリ秒以内に最初のレスポンスを返すでしょう。

しかし、AOTは常に選択肢に入るとは限りませんし、常に意味があるわけではありません。例えば、JVMはネイティブバイナリよりも優れたレイテンシを提供する可能性があったり、外部依存関係はAOTコンパイルされたネイティブイメージで使用できない可能性があったり、そしておなじみのデバッグツールやJDK Flight Recorder、JMXなどのJVMの良さを享受したいと思う可能性があったりします。そのような場合、CDS はビルドプロセスにいくつかのステップを追加するだけで、起動時間を大幅に改善できます。

Monitoring REST APIs with Custom JDK Flight Recorder Events
https://www.morling.dev/blog/rest-api-monitoring-with-custom-jdk-flight-recorder-events/

OpenJDK のクラスデータ共有の他にも、起動時間を改善するための関連するテクニックがいくつかあります。

AppCDS の詳細については、Vladimir Plizga による長いながらも洞察に富んだ投稿があります。Volker Simonisも興味深い記事を書いています。javaコマンドのリファレンスドキュメントにあるCDSのドキュメントも見てみてください。

AppCDS for Spring Boot applications: first contact
https://medium.com/@toparvion/appcds-for-spring-boot-applications-first-contact-6216db6a4194
cl4cds
https://simonis.github.io/cl4cds/
アプリケーションデータ共有 / Application Class Data Sharing
https://docs.oracle.com/javase/jp/14/docs/specs/man/java.html#application-class-data-sharing
https://docs.oracle.com/en/java/javase/14/docs/specs/man/java.html#application-class-data-sharing

最後に、QuarkusチームはCDSアーカイブの標準サポートに取り組んでいます(訳注:1.6で実験的機能として利用できるようになりました)。これにより、必要なすべてのクラスのアーカイブの作成が、余計な設定なしに完全に自動化され、CDSによって約束された起動時間の改善の恩恵をより簡単に受けられるようになることでしょう。

Introduce the ability to generate AppCDS #9710
https://github.com/quarkusio/quarkus/pull/9710

コメントを残す

以下に詳細を記入するか、アイコンをクリックしてログインしてください。

WordPress.com ロゴ

WordPress.com アカウントを使ってコメントしています。 ログアウト /  変更 )

Google フォト

Google アカウントを使ってコメントしています。 ログアウト /  変更 )

Twitter 画像

Twitter アカウントを使ってコメントしています。 ログアウト /  変更 )

Facebook の写真

Facebook アカウントを使ってコメントしています。 ログアウト /  変更 )

%s と連携中