The English version of quarkus.io is the official project site. Translated sites are community supported on a best-effort basis.

ネイティブリファレンスガイド

このガイドは、ネイティブ実行可能ファイルのビルドネイティブイメージでのSSLの使用ネイティブアプリケーションの作成 の各ガイドに付随するものです。このガイドでは、開発時や本番時に発生する可能性のある Quarkus のネイティブ実行可能ファイルの問題をデバッグするための詳細を説明しています。

このリファレンスガイドは、スタートガイド で開発されたアプリケーションを入力としています。このガイドでは、このアプリケーションをすばやくセットアップする方法について説明しています。

要件と前提条件

このガイドには、次の要件があります。

  • JDK 11 がインストールされ、JAVA_HOME が適切に設定されていること

  • Apache Maven 3.8.1+

  • 動作するコンテナーランタイム(Docker, podman)

このガイドでは、Linux 環境内で Quarkus ネイティブ実行可能ファイルをビルドして実行します。すべての環境で同種のエクスペリエンスを提供するために、ガイドはコンテナーランタイム環境に依存して、ネイティブ実行可能ファイルをビルドおよび実行します。以下の手順では例として Docker を使用していますが、podman などの他のコンテナーランタイムでも、よく似たコマンドを実行できます。

ネイティブ実行可能ファイルのビルドはコストのかかるプロセスであるため、コンテナーランタイムに十分な CPU とメモリーがあることを確認してください。最低でも 4 つの CPU と 4GB のメモリーが必要です。

最後に、このガイドでは、ネイティブ実行可能ファイルのビルド用に GraalVM の Mandrel distribution の使用を想定しています。これらはコンテナー内にビルドされるため、ホスト上に Mandrel をインストールする必要はありません。

プロジェクトのブートストラップ

新しい Quarkus プロジェクトを作成することから始めます。ターミナルを開き、以下のコマンドを実行します。

Linux および MacOS ユーザーの場合

CLI
quarkus create app org.acme:debugging-native \
    --extension=resteasy-reactive,container-image-docker
cd debugging-native

Gradleプロジェクトを作成するには、 --gradle または --gradle-kotlin-dsl オプションを追加します。

Quarkus CLIのインストール方法については、Quarkus CLIガイドをご参照ください。

Maven
mvn io.quarkus.platform:quarkus-maven-plugin:2.11.1.Final:create \
    -DprojectGroupId=org.acme \
    -DprojectArtifactId=debugging-native \
    -Dextensions="resteasy-reactive,container-image-docker"
cd debugging-native

Gradleプロジェクトを作成するには、 -DbuildTool=gradle または -DbuildTool=gradle-kotlin-dsl オプションを追加します。

Windows ユーザーの場合

  • cmd を使っている場合 (バックスラッシュは使わず、全て同じ行にしてください)

  • Powershell を使用する場合は、-D パラメーターを二重引用符で囲みます。例: "-DprojectArtifactId=debugging-native"

Quarkus プロパティーの設定

一部の Quarkus 設定オプションは、このガイド全体で常に使用されるため、コマンドラインの呼び出しを整理しやすくするために、これらのオプションを application.properties ファイルに追加することをお勧めします。では、さっそく以下のオプションをこのファイルに追加してください。

quarkus.native.container-build=true
quarkus.native.builder-image=quay.io/quarkus/ubi-quarkus-mandrel:22.1-java11
quarkus.container-image.build=true
quarkus.container-image.group=test

最初のデバッグ手順

最初のステップとして、プロジェクトディレクトリーに移動し、アプリケーションのネイティブ実行可能ファイルをビルドします。

./mvnw package -DskipTests -Dnative

アプリケーションを実行して、期待通りに動作することを確認します。一つの端末で以下を実行します。

docker run -i --rm -p 8080:8080 test/debugging-native:1.0.0-SNAPSHOT

別のターミナルで以下を実行します。

curl -w '\n' http://localhost:8080/hello

このセクションの残りの部分では、追加情報を使用してネイティブ実行可能ファイルをビルドする方法について説明しますが、最初に、実行中のアプリケーションを停止します。-Dquarkus.native.additional-build-args を使用してネイティブイメージビルドオプションを追加することで、ネイティブ実行可能ファイルのビルド中にこの情報を取得できます。

./mvnw package -DskipTests -Dnative \
    -Dquarkus.native.additional-build-args=--native-image-info

これを実行すると、次のような追加の出力行が得られます。

...
# Printing compilation-target information to: /project/reports/target_info_20220223_100915.txt
…
# Printing native-library information to: /project/reports/native_library_info_20220223_100925.txt

Note that /project is a folder within the container that is building the native executable. So, this is not a folder that you will find in the host environment. /project folder is mapped to target/debugging-native-1.0.0-SNAPSHOT-native-image-source-jar, so you will find the files under the reports folder in that directory.

ターゲット情報ファイルには、ターゲットプラットフォーム、実行ファイルのコンパイルに使用されたツールチェーン、使用されているCライブラリなどの情報が含まれています。

$ cat target/*/reports/target_info_*.txt
Building image for target platform: org.graalvm.nativeimage.Platform$LINUX_AMD64
Using native toolchain:
   Name: GNU project C and C++ compiler (gcc)
   Vendor: redhat
   Version: 8.5.0
   Target architecture: x86_64
   Path: /usr/bin/gcc
Using CLibrary: com.oracle.svm.core.posix.linux.libc.GLib

ネイティブライブラリ情報ファイルには、バイナリに追加されるスタティックライブラリと、実行ファイルに動的にリンクされるその他のライブラリの情報が含まれています。

$ cat target/*/reports/native_library_info_*.txt
Static libraries:
   ../opt/mandrel/lib/svm/clibraries/linux-amd64/liblibchelper.a
   ../opt/mandrel/lib/static/linux-amd64/glibc/libnet.a
   ../opt/mandrel/lib/static/linux-amd64/glibc/libextnet.a
   ../opt/mandrel/lib/static/linux-amd64/glibc/libnio.a
   ../opt/mandrel/lib/static/linux-amd64/glibc/libjava.a
   ../opt/mandrel/lib/static/linux-amd64/glibc/libfdlibm.a
   ../opt/mandrel/lib/static/linux-amd64/glibc/libsunec.a
   ../opt/mandrel/lib/static/linux-amd64/glibc/libzip.a
   ../opt/mandrel/lib/svm/clibraries/linux-amd64/libjvm.a
Other libraries: stdc++,pthread,dl,z,rt

ネイティブイメージビルドの追加引数として --verbose を渡すことで、さらに詳細な情報を得ることができます。このオプションは、Quarkusを介して高いレベルで渡されたオプションがネイティブ実行可能ファイルの生成に渡されているのか、あるいはサードパーティのjarにネイティブイメージの設定が埋め込まれていて、それがネイティブイメージの呼び出しに届いているのかを検出するのに非常に役立ちます。

./mvnw package -DskipTests -Dnative \
    -Dquarkus.native.additional-build-args=--verbose

--verbose で実行すると、ネイティブイメージのビルドプロセスが2つの連続したJavaプロセスであることが分かります。

  • 1番目は非常に短いJavaプロセスで、基本的な検証を行い、2つ目のプロセスのための引数を組み立てます(GraalVMの純正ディストリビューションでは、これはネイティブコードとして実行されます)。

  • 2番目のJavaプロセスでは、ネイティブ実行可能ファイル作成の主要部分が行われます。 --verbose オプションは、実際に実行されたJavaプロセスを表示します。出力を受けて、自分で実行することもできます。

また、複数のネイティブ・ビルド・オプションをコンマで区切って組み合わせることもできます。例:

./mvnw package -DskipTests -Dnative \
    -Dquarkus.native.additional-build-args=--native-image-info,--verbose

Remember that if an argument for -Dquarkus.native.additional-build-args includes the , symbol, it needs to be escaped to be processed correctly, e.g. \\,.

ネイティブ実行可能ファイルの検査

ネイティブ実行可能ファイルを指定すると、さまざまな Linux ツールを使用して実行可能ファイルを検査できます。さまざまな環境をサポートできるように、検査は Linux コンテナー内から実行されます。このガイドに必要なすべてのツールを使用して、Linux コンテナーイメージを作成しましょう。

FROM fedora:35

RUN dnf install -y \
binutils \
gdb \
git \
perf \
perl-open

ENV FG_HOME /opt/FlameGraph

RUN git clone https://github.com/brendangregg/FlameGraph $FG_HOME

WORKDIR /data

ENTRYPOINT /bin/bash

Linux 以外の環境で docker を使用する場合は、以下を実行し、この Dockerfile を使用してイメージを作成できます。

docker build -t fedora-tools:v1 .

次に、プロジェクトの root に移動し、先ほど作成した Docker コンテナーを以下のように実行します。

docker run -t -i --rm -v ${PWD}:/data -p 8080:8080 fedora-tools:v1

ldd は、実行可能ファイルの共有ライブラリの依存関係を表示します。

ldd ./target/debugging-native-1.0.0-SNAPSHOT-runner

strings は、バイナリ内のテキストメッセージを探すのに使用できます。

strings ./target/debugging-native-1.0.0-SNAPSHOT-runner | grep Hello

strings を使えば、指定されたバイナリのMandrel情報を得ることもできます。

strings ./target/debugging-native-1.0.0-SNAPSHOT-runner | grep core.VM

Finally, using readelf we can inspect different sections of the binary. For example, we can see how the heap and text sections take most of the binary:

readelf -SW ./target/debugging-native-1.0.0-SNAPSHOT-runner

Runtime containers produced by Quarkus to run native executables will not include the tools mentioned above. To explore a native executable within a runtime container, it’s best to run the container itself and then docker cp the executable locally, e.g.:

docker run -i --rm --name=mytest -p 8080:8080 test/debugging-native:1.0.0-SNAPSHOT
docker cp mytest:/work/application path/on/host/

From there, you can either inspect the executable directly or use a tools container like above.

ネイティブレポート

オプションとして、ネイティブビルドプロセスでは、バイナリに何が入っているかを示すレポートを生成することができます。

./mvnw package -DskipTests -Dnative \
    -Dquarkus.native.enable-reports

レポートは、target/debugging-native-1.0.0-SNAPSHOT-native-image-source-jar/reports/ の下に作成されます。これらのレポートは、メソッド/クラスが見つからない問題が発生した場合、または Mandrel によって禁止されたメソッドが発生した場合に、最も役立つリソースの一部になります。

コールツリーレポート

call_tree csv file reports are some of the default reports generated when the -Dquarkus.native.enable-reports option is passed in. These csv files can be imported into a graph database, such as Neo4j, to inspect them more easily and run queries against the call tree. This is useful for getting an approximation on why a method/class is included in the binary.

Let’s see this in action.

まず、Neo4jのインスタンスを起動します。

export NEO_PASS=...
docker run \
    --detach \
    --rm \
    --name testneo4j \
    -p7474:7474 -p7687:7687 \
    --env NEO4J_AUTH=neo4j/${NEO_PASS} \
    neo4j:latest

コンテナーが実行されると、 Neo4j ブラウザー にアクセスできます。ログインする際は、ユーザー名として neo4j を使用し、パスワードとして NEO_PASS の値を使用します。

CSVファイルをインポートするためには、CSVファイル内のデータをインポートし、グラフデータベースのノードとエッジを作成する以下のcypherスクリプトが必要です。

CREATE CONSTRAINT unique_vm_id ON (v:VM) ASSERT v.vmId IS UNIQUE;
CREATE CONSTRAINT unique_method_id ON (m:Method) ASSERT m.methodId IS UNIQUE;

LOAD CSV WITH HEADERS FROM 'file:///reports/call_tree_vm.csv' AS row
MERGE (v:VM {vmId: row.Id, name: row.Name})
RETURN count(v);

LOAD CSV WITH HEADERS FROM 'file:///reports/call_tree_methods.csv' AS row
MERGE (m:Method {methodId: row.Id, name: row.Name, type: row.Type, parameters: row.Parameters, return: row.Return, display: row.Display})
RETURN count(m);

LOAD CSV WITH HEADERS FROM 'file:///reports/call_tree_virtual_methods.csv' AS row
MERGE (m:Method {methodId: row.Id, name: row.Name, type: row.Type, parameters: row.Parameters, return: row.Return, display: row.Display})
RETURN count(m);

LOAD CSV WITH HEADERS FROM 'file:///reports/call_tree_entry_points.csv' AS row
MATCH (m:Method {methodId: row.Id})
MATCH (v:VM {vmId: '0'})
MERGE (v)-[:ENTRY]->(m)
RETURN count(*);

LOAD CSV WITH HEADERS FROM 'file:///reports/call_tree_direct_edges.csv' AS row
MATCH (m1:Method {methodId: row.StartId})
MATCH (m2:Method {methodId: row.EndId})
MERGE (m1)-[:DIRECT {bci: row.BytecodeIndexes}]->(m2)
RETURN count(*);

LOAD CSV WITH HEADERS FROM 'file:///reports/call_tree_override_by_edges.csv' AS row
MATCH (m1:Method {methodId: row.StartId})
MATCH (m2:Method {methodId: row.EndId})
MERGE (m1)-[:OVERRIDEN_BY]->(m2)
RETURN count(*);

LOAD CSV WITH HEADERS FROM 'file:///reports/call_tree_virtual_edges.csv' AS row
MATCH (m1:Method {methodId: row.StartId})
MATCH (m2:Method {methodId: row.EndId})
MERGE (m1)-[:VIRTUAL {bci: row.BytecodeIndexes}]->(m2)
RETURN count(*);

スクリプトの内容を import.cypher というファイルにコピー&ペーストします。

Mandrel 22.0.0 には、コンテナー内でレポートを生成する際に、インポートサイファーファイルで使用されるシンボリックリンクが正しく設定されないというバグが含まれています (詳細は こちら を参照)。これは、以下のスクリプトをファイルにコピーして実行することで回避できます。

set -e

project="debugging-native"

pushd target/*-native-image-source-jar/reports

rm -f call_tree_vm.csv
ln -s call_tree_vm_${project}-* call_tree_vm.csv

rm -f call_tree_direct_edges.csv
ln -s call_tree_direct_edges_${project}-* call_tree_direct_edges.csv

rm -f call_tree_entry_points.csv
ln -s call_tree_entry_points_${project}-* call_tree_entry_points.csv

rm -f call_tree_methods.csv
ln -s call_tree_methods_${project}-* call_tree_methods.csv

rm -f call_tree_virtual_edges.csv
ln -s call_tree_virtual_edges_${project}-* call_tree_virtual_edges.csv

rm -f call_tree_virtual_methods.csv
ln -s call_tree_virtual_methods_${project}-* call_tree_virtual_methods.csv

rm -f call_tree_override_by_edges.csv
ln -s call_tree_override_by_edges_${project}-* call_tree_override_by_edges.csv

popd

次に、インポートサイファースクリプトとCSVファイルをNeo4jのインポートフォルダにコピーします。

docker cp \
    target/*-native-image-source-jar/reports \
    testneo4j:/var/lib/neo4j/import

docker cp import.cypher testneo4j:/var/lib/neo4j

すべてのファイルをコピーしたら、インポートスクリプトを起動します。

docker exec testneo4j bin/cypher-shell -u neo4j -p ${NEO_PASS} -f import.cypher

インポートの完了 (ほんの数分で完了) 後に、Neo4j ブラウザー にアクセスすると、簡単なデータのサマリーをグラフで見ることができます。

インポート後の Neo4j データベース情報

上のデータでは、 ~60000のメソッドがあり、それらの間には ~200000のエッジがあることがわかります。ここでデモされているQuarkusアプリケーションは非常に基本的なものなので、調べられることは多くありませんが、グラフをより詳細に調べるために実行できるクエリの例をいくつか紹介します。典型的な例としては、あるメソッドを探すことから始めます。

match (m:Method) where m.name = "hello" return *

そこから、特定の型の特定のメソッドに絞ることができます。

match (m:Method) where m.name = "hello" and m.type =~ ".*GreetingResource" return *

探している特定のメソッドのノードを見つけたら、答えを得たい典型的な質問は、「なぜこのメソッドはコールツリーに含まれるのか」です。そのためには、終点のメソッドから始まる所定の深さの到着接続を探します。たとえば、あるメソッドを直接呼び出すメソッドは、以下のようにして見つけることができます。

match (m:Method) <- [*1..1] - (o) where m.name = "hello" and m.type =~ ".*GreetingResource" return *

そうすれば、深さ2の直接呼び出しを探すことができます。つまり、対象のメソッドを呼び出すメソッドを呼び出すメソッドを探すことになります。

match (m:Method) <- [*1..2] - (o) where m.name = "hello" and m.type =~ ".*GreetingResource" return *

階層を上がっていくことはできますが、残念ながらノードの数が多すぎる深度に到達すると、Neo4jブラウザはそれらすべてを可視化することができません。そのような場合は、代わりにcypher shellに対して直接クエリを実行することができます。

docker exec testneo4j bin/cypher-shell -u neo4j -p ${NEO_PASS} \
  "match (m:Method) <- [*1..10] - (o) where m.name = 'hello' and m.type =~ '.*GreetingResource' return *"

詳細については、上記で説明した手法を使用して、Quarkus Hibernate ORM クイックスタートについて検討している blog 記事 を参照してください。

使用されているパッケージ/クラス/メソッドのレポート

used_packages, used_classes, used_methods テキストファイルレポートは、アプリケーションの異なるバージョンを比較する際に便利です。例えば、イメージ作成に時間がかかるのはなぜか?また、なぜイメージが大きくなったのか?

更なるレポート

Mandrelは、 -Dquarkus.native.enable-reports オプションで有効になっているレポート以外にも、様々なレポートを作成することができます。これらはエキスパートオプションと呼ばれ、以下を実行することで詳細を知ることができます。

docker run quay.io/quarkus/ubi-quarkus-mandrel:22.1-java11 --expert-options-all

These expert options are not considered part of the GraalVM native image API, so they might change anytime.

これらのエキスパートオプションを使用するには、 -Dquarkus.native.additional-build-args パラメータにコンマで区切って追加します。

ビルド時と実行時の初期化

QuarkusはMandrelに対し、ビルド時に可能な限り初期化するよう指示し、実行時の起動を可能な限り高速化しています。これは、起動速度がアプリケーションの動作準備の早さに大きな影響を与えるコンテナ環境では重要です。また、ビルド時の初期化は、サポートされていない機能が実行時の初期化によって到達可能になることによる実行時の失敗のリスクを最小限にし、Quarkusの信頼性を高めています。

ビルド時に初期化されるコードの最も一般的な例は、静的変数とブロックです。Mandrelはこれらをデフォルトでは実行時に実行しますが、Quarkusでは先程の理由でビルド時に実行するように指示しています。

つまり、インラインで初期化されたスタティック変数や、スタティックブロックで初期化されたスタティック変数は、アプリケーションを再起動しても同じ値を維持します。これは、Javaとして実行した場合とは異なる動作です。

これの実際の動作を非常に基本的な例で確認するには、以下のような新しい TimestampResource をアプリケーションに追加します。

package org.acme;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

@Path("/timestamp")
public class TimestampResource {

    static long firstAccess = System.currentTimeMillis();

    @GET
    @Produces(MediaType.TEXT_PLAIN)
    public String timestamp() {
        return "First access " + firstAccess;
    }
}

次のようにバイナリを再ビルドします。

./mvnw package -DskipTests -Dnative

1 つのターミナルでアプリケーションを実行します (これを実行する前に、他のネイティブ実行可能コンテナーの実行を必ず停止してください)。

docker run -i --rm -p 8080:8080 test/debugging-native:1.0.0-SNAPSHOT

別のターミナルから GET リクエストを複数回送信してみましょう。

curl -w '\n' http://localhost:8080/timestamp # run this multiple times

現在の時刻がどのようにバイナリに焼き付けられているかを確認できます。この時刻は、バイナリのビルド時に計算されたものなので、アプリケーションの再起動が影響しません。

状況によっては、ビルド時の初期化により、ネイティブ実行可能ファイルをビルドするときにエラーが発生する可能性があります。1 つの例は、バイナリーにベイクされる JVM のヒープに存在することが禁じられている値が、ビルド時に計算される場合です。これが実際に動作することを確認するには、この REST リソースを追加してください。

package org.acme;

import javax.crypto.Cipher;
import javax.crypto.NoSuchPaddingException;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import java.nio.charset.StandardCharsets;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;

@Path("/encrypt-decrypt")
public class EncryptDecryptResource {

    static final KeyPairGenerator KEY_PAIR_GEN;
    static final Cipher CIPHER;

    static {
        try {
            KEY_PAIR_GEN = KeyPairGenerator.getInstance("RSA");
            KEY_PAIR_GEN.initialize(1024);

            CIPHER = Cipher.getInstance("RSA");
        } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
            throw new RuntimeException(e);
        }
    }

    @GET
    @Path("/{message}")
    public String encryptDecrypt(String message) throws Exception {
        KeyPair keyPair = KEY_PAIR_GEN.generateKeyPair();

        byte[] text = message.getBytes(StandardCharsets.UTF_8);

        // Encrypt with private key
        CIPHER.init(Cipher.ENCRYPT_MODE, keyPair.getPrivate());
        byte[] encrypted = CIPHER.doFinal(text);

        // Decrypt with public key
        CIPHER.init(Cipher.DECRYPT_MODE, keyPair.getPublic());
        byte[] unencrypted = CIPHER.doFinal(encrypted);

        return new String(unencrypted, StandardCharsets.UTF_8);
    }
}

アプリケーションを再ビルドしようとすると、エラーが発生します。

./mvnw package -DskipTests -Dnative
...
Error: Unsupported features in 2 methods
Detailed message:
Error: Detected an instance of Random/SplittableRandom class in the image heap. Instances created during image generation have cached seed values and don't behave as expected.  To see how this object got instantiated use --trace-object-instantiation=java.security.SecureRandom. The object was probably created by a class initializer and is reachable from a static field. You can request class initialization at image runtime by using the option --initialize-at-run-time=<class-name>. Or you can write your own initialization methods and call them explicitly from your main entry point.
Trace: Object was reached by
	reading field java.security.KeyPairGenerator$Delegate.initRandom of
		constant java.security.KeyPairGenerator$Delegate@58b0fe1b reached by
	reading field org.acme.EncryptDecryptResource.KEY_PAIR_GEN
Error: Detected an instance of Random/SplittableRandom class in the image heap. Instances created during image generation have cached seed values and don't behave as expected.  To see how this object got instantiated use --trace-object-instantiation=java.security.SecureRandom. The object was probably created by a class initializer and is reachable from a static field. You can request class initialization at image runtime by using the option --initialize-at-run-time=<class-name>. Or you can write your own initialization methods and call them explicitly from your main entry point.
Trace: Object was reached by
	reading field sun.security.rsa.RSAKeyPairGenerator.random of
		constant sun.security.rsa.RSAKeyPairGenerator$Legacy@3248a092 reached by
	reading field java.security.KeyPairGenerator$Delegate.spi of
		constant java.security.KeyPairGenerator$Delegate@58b0fe1b reached by
	reading field org.acme.EncryptDecryptResource.KEY_PAIR_GEN

したがって、上記のメッセージが示しているのは、アプリケーションが定数としてランダムであると想定される値をキャッシュしているということです。シードがイメージでベイク処理されているため、ランダムであるはずの何かがもはやランダムではないため、これは望ましくありません。上記のメッセージは、何が原因かを非常に明確に示していますが、他の状況では、原因はさらにわかりにくいかもしれません。次のステップとして、ネイティブ実行可能ファイルの生成にいくつかのフラグを追加して、より多くの情報を取得することにします。

メッセージにあるように、まずはオブジェクトのインスタンス化を追跡するためのオプションを追加してみましょう。

./mvnw package -DskipTests -Dnative \
    -Dquarkus.native.additional-build-args="--trace-object-instantiation=java.security.SecureRandom"
...
Error: Unsupported features in 2 methods
Detailed message:
Error: Detected an instance of Random/SplittableRandom class in the image heap. Instances created during image generation have cached seed values and don't behave as expected.  Object has been initialized by the com.sun.jndi.dns.DnsClient class initializer with a trace:
 	at java.security.SecureRandom.<init>(SecureRandom.java:218)
	at sun.security.jca.JCAUtil$CachedSecureRandomHolder.<clinit>(JCAUtil.java:59)
	at sun.security.jca.JCAUtil.getSecureRandom(JCAUtil.java:69)
	at com.sun.jndi.dns.DnsClient.<clinit>(DnsClient.java:82)
. Try avoiding to initialize the class that caused initialization of the object. The object was probably created by a class initializer and is reachable from a static field. You can request class initialization at image runtime by using the option --initialize-at-run-time=<class-name>. Or you can write your own initialization methods and call them explicitly from your main entry point.
Trace: Object was reached by
	reading field java.security.KeyPairGenerator$Delegate.initRandom of
		constant java.security.KeyPairGenerator$Delegate@4a5058f9 reached by
	reading field org.acme.EncryptDecryptResource.KEY_PAIR_GEN
Error: Detected an instance of Random/SplittableRandom class in the image heap. Instances created during image generation have cached seed values and don't behave as expected.  Object has been initialized by the com.sun.jndi.dns.DnsClient class initializer with a trace:
 	at java.security.SecureRandom.<init>(SecureRandom.java:218)
	at sun.security.jca.JCAUtil$CachedSecureRandomHolder.<clinit>(JCAUtil.java:59)
	at sun.security.jca.JCAUtil.getSecureRandom(JCAUtil.java:69)
	at com.sun.jndi.dns.DnsClient.<clinit>(DnsClient.java:82)
. Try avoiding to initialize the class that caused initialization of the object. The object was probably created by a class initializer and is reachable from a static field. You can request class initialization at image runtime by using the option --initialize-at-run-time=<class-name>. Or you can write your own initialization methods and call them explicitly from your main entry point.
Trace: Object was reached by
	reading field sun.security.rsa.RSAKeyPairGenerator.random of
		constant sun.security.rsa.RSAKeyPairGenerator$Legacy@71880cf1 reached by
	reading field java.security.KeyPairGenerator$Delegate.spi of
		constant java.security.KeyPairGenerator$Delegate@4a5058f9 reached by
	reading field org.acme.EncryptDecryptResource.KEY_PAIR_GEN

The error messages point to the code in the example, but it can be surprising that a reference to DnsClient appears. Why is that? The key is in what happens inside KeyPairGenerator.initialize() method call. It uses JCAUtil.getSecureRandom() which is why this is problematic, but sometimes the tracing options can show some stack traces that do not represent what happens in reality. The best option is to dig through the source code and use tracing output for guidance but not as full truth.

Moving the KEY_PAIR_GEN.initialize(1024); call to the run-time executed method encryptDecrypt is enough to solve this particular issue. Rebuild the application and verify that encrypt/decrypt endpoint works as expected by sending any message and check if the reply is the same as the incoming message:

$ ./mvnw package -DskipTests -Dnative
...
$ docker run -i --rm -p 8080:8080 test/debugging-native:1.0.0-SNAPSHOT
...
$ curl -w '\n' http://localhost:8080/encrypt-decrypt/hellomandrel
hellomandrel

どのクラスがどのように初期化されるかについての追加情報は、 -Dquarkus.native.additional-build-args を通じて -H:+PrintClassInitialization フラグを渡すことで得ることができます。

実行時動作のプロファイリング

シングルスレッド

この演習では、ネイティブ実行可能ファイルにコンパイルされたQuarkusアプリケーションの実行時動作をプロファイリングし、ボトルネックがどこにあるかを判断します。問題がアプリケーションのネイティブバージョンでのみ発生するために、純粋なJavaバージョンのプロファイリングができないシナリオを想定しています。

次のコードを使用して REST リソースを追加します (この例は Andrei Pangin’s Java Profiling presentation からご提供いただいています):

package org.acme;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

@Path("/string-builder")
public class StringBuilderResource {

    @GET
    @Produces(MediaType.TEXT_PLAIN)
    public String appendDelete() {
        StringBuilder sb = new StringBuilder();
        sb.append(new char[1_000_000]);

        do
        {
            sb.append(12345);
            sb.delete(0, 5);
        } while (Thread.currentThread().isAlive());

        return "Never happens";
    }
}

アプリケーションを再コンパイルし、バイナリを再ビルドして実行します。単純なcurlを試みても、期待通り完了しません。

$ ./mvnw package -DskipTests -Dnative
...
$ docker run -i --rm -p 8080:8080 test/debugging-native:1.0.0-SNAPSHOT
...
$ curl http://localhost:8080/string-builder # this will never complete

しかし、ここで私たちが答えようとしているのは、そのようなコードのボトルネックは何か?文字を追加することか?削除していることか?スレッドが生きているかどうかをチェックしていることか?です。

Linux のネイティブ実行可能ファイルを扱っているので、perf のようなツールを直接使用できます。perf を使用するには、プロジェクトの root に移動し、特権ユーザーとして以前に作成したツールコンテナーを起動します。

docker run --privileged -t -i --rm -v ${PWD}:/data -p 8080:8080 fedora-tools:v1

なお、 perf を使用してガイドのネイティブ実行可能ファイルをプロファイルするには、コンテナを特権的に実行するか、 --cap-add sys_admin を使用する必要があります。本番環境では特権コンテナは推奨され ません ので、このフラグの使用には注意が必要です。

コンテナが稼働したら、カーネルがプロファイリングの演習に対応できるようにしておく必要があります。

echo -1 | sudo tee /proc/sys/kernel/perf_event_paranoid
echo 0 | sudo tee /proc/sys/kernel/kptr_restrict

上記のカーネルの変更は、Linux 仮想マシンにも適用されます。ベアメタル Linux マシンで実行している場合は、perf_event_paranoid を微調整するだけで十分です。

次に、ツールコンテナー内から以下を実行します。

perf record -F 1009 -g -a ./target/debugging-native-1.0.0-SNAPSHOT-runner

The perf record command above takes 1009 samples per second. Increasing this value means more samples are gathered, which can end up affecting the runtime performance. This also increases the amount of data generated. The more data generated, the longer it takes to process it, but the more precision you get on what the application is doing. So, finding the right value is a balancing act.

perf record の実行中に、別のウィンドウを開き、エンドポイントにアクセスします。

curl http://localhost:8080/string-builder # this will never complete

数秒後、perf record プロセスを停止します。これにより、perf.data ファイルが生成されます。perf report を使用して perf データを検査することができますが、多くの場合、データをフレームグラフとして表示した方が、より良い結果を得ることができます。フレームグラフを生成するには、ツールコンテナー内にすでにインストールされている FlameGraph GitHub リポジトリー を使用します。

次に、perf record を介してキャプチャされたデータを使用してフレームグラフを生成します。

perf script -i perf.data | ${FG_HOME}/stackcollapse-perf.pl | ${FG_HOME}/flamegraph.pl > flamegraph.svg

The flame graph is a svg file that a web browser, such as Firefox, can easily display. After the above two commands complete one can open flamegraph.svg in their browser:

シンボルのない Perf フレームグラフ

メインとなるはずのものに大半の時間が費やされていることがわかりますが、呼び出している StringBuilderResource クラスや StringBuilder クラスの痕跡は見られません。バイナリーのシンボルテーブルを確認する必要があります。クラスと StringBuilder のシンボルを見つけることができますか? 意味のあるデータを取得するためにそれらが必要です。ツールコンテナー内から、シンボルテーブルをクエリーします。

objdump -t ./target/debugging-native-1.0.0-SNAPSHOT-runner | grep StringBuilder
[no output]

シンボルテーブルをクエリーすると、出力は表示されません。これが、フレームグラフにコールグラフが表示されない理由です。これは、ネイティブイメージが行う意図的な決定です。デフォルトでは、バイナリーからシンボルを削除します。

シンボルを取り戻すには、シンボルを削除しないように GraalVM に指示するバイナリーを再ビルドする必要があります。さらに、DWARF デバッグ情報を有効にして、スタックトレースにその情報を入力できるようにします。ツールコンテナーの外部から、以下を実行します。

./mvnw package -DskipTests -Dnative \
    -Dquarkus.native.debug.enabled \
    -Dquarkus.native.additional-build-args=-H:-DeleteLocalSymbols

次に、終了した場合はツールコンテナーに再度入り、objdump を使用してネイティブ実行可能ファイルを検査し、シンボルがどのように存在するようになったかを確認します。

$ objdump -t ./target/debugging-native-1.0.0-SNAPSHOT-runner | grep StringBuilder
000000000050a940 l     F .text	0000000000000091              .hidden ReflectionAccessorHolder_StringBuilderResource_appendDelete_9e06d4817d0208a0cce97ebcc0952534cac45a19_e22addf7d3eaa3ad14013ce01941dc25beba7621
000000000050a9e0 l     F .text	00000000000000bb              .hidden ReflectionAccessorHolder_StringBuilderResource_constructor_0f8140ea801718b80c05b979a515d8a67b8f3208_12baae06bcd6a1ef9432189004ae4e4e176dd5a4
...

そのパターンに一致するシンボルの長いリストが表示されるはずです。

次に、実行ファイルを perf で実行すると、 コールグラフが dwarf であることがわかります

perf record -F 1009 --call-graph dwarf -a ./target/debugging-native-1.0.0-SNAPSHOT-runner

もう一度curlコマンドを実行し、バイナリを停止し、フレームグラフを生成して開きます。

perf script -i perf.data | ${FG_HOME}/stackcollapse-perf.pl | ${FG_HOME}/flamegraph.pl > flamegraph.svg

フレームグラフを見ると、どこがボトルネックになっているかがわかります。それは、 StringBuilder.delete() が呼び出され、 System.arraycopy() を呼び出すときです。問題は、100 万文字を非常に小さな単位でシフトさせる必要があることです。

シンボルのある Perf フレームグラフ

マルチスレッド

Multithreaded programs might require special attention when trying to understand their runtime behaviour. To demonstrate this, add this MulticastResource code to your project (example courtesy of Andrei Pangin’s Java Profiling presentation):

package org.acme;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.DatagramChannel;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;

@Path("/multicast")
public class MulticastResource
{
    @GET
    @Produces(MediaType.TEXT_PLAIN)
    public String send() throws Exception {
        sendMulticasts();
        return "Multicast packets sent";
    }

    static void sendMulticasts() throws Exception {
        DatagramChannel ch = DatagramChannel.open();
        ch.bind(new InetSocketAddress(5555));
        ch.configureBlocking(false);

        ExecutorService pool =
            Executors.newCachedThreadPool(new ShortNameThreadFactory());
        for (int i = 0; i < 10; i++) {
            pool.submit(() -> {
                final ByteBuffer buf = ByteBuffer.allocateDirect(1000);
                final InetSocketAddress remoteAddr =
                    new InetSocketAddress("127.0.0.1", 5556);

                while (true) {
                    buf.clear();
                    ch.send(buf, remoteAddr);
                }
            });
        }

        System.out.println("Warming up...");
        Thread.sleep(3000);

        System.out.println("Benchmarking...");
        Thread.sleep(5000);
    }

    private static final class ShortNameThreadFactory implements ThreadFactory {

        private final AtomicInteger threadNumber = new AtomicInteger(1);
        private final String namePrefix = "thread-";

        public Thread newThread(Runnable r) {
            return new Thread(r, namePrefix + threadNumber.getAndIncrement());
        }
    }
}

デバッグ情報付きでネイティブ実行可能ファイルをビルドします。

./mvnw package -DskipTests -Dnative \
    -Dquarkus.native.debug.enabled \
    -Dquarkus.native.additional-build-args=-H:-DeleteLocalSymbols

ツールコンテナー内から (特権ユーザーとして)、perf を介してネイティブ実行可能ファイルを実行します。

perf record -F 1009 --call-graph dwarf -a ./target/debugging-native-1.0.0-SNAPSHOT-runner

エンドポイントを呼び出して、マルチキャストパケットを送信します。

curl -w '\n' http://localhost:8080/multicast

フレームグラフを作成して開いてください。

perf script -i perf.data | ${FG_HOME}/stackcollapse-perf.pl | ${FG_HOME}/flamegraph.pl > flamegraph.svg
別々のスレッドを持つマルチスレッド perf フレームグラフ

作成されたフレームグラフは奇妙に見えます。すべてのスレッドが同じ作業をしているにもかかわらず、各スレッドが独立して扱われています。これでは、プログラムのボトルネックを明確に把握することができません。

これは、 perf の観点から見ると、各スレッドが異なるコマンドであるために起こっています。 perf report を確認するとわかります。

perf report --stdio
# Children      Self  Command          Shared Object       Symbol
# ........  ........  ...............  ......................................  ......................................................................................
...
     6.95%     0.03%  thread-2         debugging-native-1.0.0-SNAPSHOT-runner  [.] MulticastResource_lambda$sendMulticasts$0_cb1f7b5dcaed7dd4e3f90d18bad517d67eae4d88
...
     4.60%     0.02%  thread-10        debugging-native-1.0.0-SNAPSHOT-runner  [.] MulticastResource_lambda$sendMulticasts$0_cb1f7b5dcaed7dd4e3f90d18bad517d67eae4d88
...

これは、すべてのスレッドが同じ名前になるように、perfの出力にいくつかの変更を加えることで回避できます。例えば、以下のようになります。

perf script | sed -E "s/thread-[0-9]*/thread/" | ${FG_HOME}/stackcollapse-perf.pl | ${FG_HOME}/flamegraph.pl > flamegraph.svg
スレッドが結合されたマルチスレッド perf フレームグラフ

フレームグラフを開くと、すべてのスレッドの作業が1つの領域に折りたたまれているのがわかります。そして、パフォーマンスに影響を与える可能性のあるロックがあることがはっきりとわかります。

ネイティブ・クラッシュのデバッグ

ネイティブ実行可能ファイルを使用することの欠点の 1 つは、標準の Java デバッガーを使用してデバッグできないことです。代わりに、GNU プロジェクトのデバッガーである gdb を使用してデバッグする必要があります。その方法を示すために、http://localhost:8080/crash にアクセスするときにセグメンテーション違反が原因でクラッシュするネイティブ Quarkus アプリケーションを生成します。これを実現するには、以下の REST リソースをプロジェクトに追加します。

package org.acme;

import sun.misc.Unsafe;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import java.lang.reflect.Field;

@Path("/crash")
public class CrashResource {

    @GET
    @Produces(MediaType.TEXT_PLAIN)
    public String hello() {
        Field theUnsafe = null;
        try {
            theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
            theUnsafe.setAccessible(true);
            Unsafe unsafe = (Unsafe) theUnsafe.get(null);
            unsafe.copyMemory(0, 128, 256);
        } catch (NoSuchFieldException | IllegalAccessException e) {
            e.printStackTrace();
        }
        return "Never happens";
    }
}

This code will try to copy 256 bytes from address 0x0 to 0x80 resulting in a Segmentation Fault. To verify this, compile and run the example application:

$ ./mvnw package -DskipTests -Dnative
...
$ docker run -i --rm -p 8080:8080 test/debugging-native:1.0.0-SNAPSHOT
...
$ curl http://localhost:8080/crash

これにより、次のような出力が得られます。

$ docker run -i --rm -p 8080:8080 test/debugging-native:1.0.0-SNAPSHOT
...
Segfault detected, aborting process. Use runtime option -R:-InstallSegfaultHandler if you don't want to use SubstrateSegfaultHandler.
...

上記の省略された出力には、問題の原因の手がかりが含まれていますが、この演習では情報が提供されなかったと仮定しています。gdb を使用してセグメンテーション違反をデバッグしてみましょう。これを行うには、プロジェクトの root に移動し、ツールコンテナーに入ります。

docker run -t -i --rm -v ${PWD}:/data -p 8080:8080 fedora-tools:v1 /bin/bash

続いて、gdb でアプリケーションを起動し、run を実行します。

gdb ./target/debugging-native-1.0.0-SNAPSHOT-runner
...
Reading symbols from ./target/debugging-native-1.0.0-SNAPSHOT-runner...
(No debugging symbols found in ./target/debugging-ntaive-1.0.0-SNAPSHOT-runner)
(gdb) run
Starting program: /data/target/debugging-native-1.0.0-SNAPSHOT-runner

次に、http://localhost:8080/crash へのアクセスを試みます。

curl http://localhost:8080/crash

これにより、 gdb に次のようなメッセージが表示されます。

Thread 4 "ecutor-thread-0" received signal SIGSEGV, Segmentation fault.
[Switching to Thread 0x7fe103dff640 (LWP 190)]
0x0000000000461f6e in ?? ()

このクラッシュの原因となったバックトレースの情報を得ようとすると、十分な情報が得られないことがわかります。

(gdb) bt
#0  0x0000000000418b5e in ?? ()
#1  0x00007ffff6f2d328 in ?? ()
#2  0x0000000000418a04 in ?? ()
#3  0x00007ffff44062a0 in ?? ()
#4  0x00000000010c3dd3 in ?? ()
#5  0x0000000000000100 in ?? ()
#6  0x0000000000000000 in ?? ()

これは、Quarkus アプリケーションを -Dquarkus.native.debug.enabled でコンパイルしなかったことが原因で、これにより、gdb の最初にある "No debugging symbols found in ./target/debugging-native-1.0.0-SNAPSHOT-runner" メッセージで示されているように、gdb はネイティブ実行ファイルのデバッグシンボルを見つけることができません。

-Dquarkus.native.debug.enabled でQuarkusアプリケーションを再コンパイルし、 gdb で再実行すると、クラッシュの原因を明らかにするバックトレースを得ることができます。さらに、 -H:-OmitInlinedMethodDebugLineInfo オプションを追加すると、インライン化されたメソッドがバックトレースから省略されるのを防ぐことができます。

./mvnw package -DskipTests -Dnative \
    -Dquarkus.native.debug.enabled \
    -Dquarkus.native.additional-build-args=-H:-OmitInlinedMethodDebugLineInfo
...
$ gdb ./target/debugging-native-1.0.0-SNAPSHOT-runner
Reading symbols from ./target/debugging-native-1.0.0-SNAPSHOT-runner...
(gdb) run
Starting program: /data/target/debugging-native-1.0.0-SNAPSHOT-runner
...
$ curl http://localhost:8080/crash

これにより、 gdb に次のようなメッセージが表示されます。

Thread 4 "ecutor-thread-0" received signal SIGSEGV, Segmentation fault.
[Switching to Thread 0x7fffeffff640 (LWP 362984)]
com.oracle.svm.core.UnmanagedMemoryUtil::copyLongsBackward(org.graalvm.word.Pointer *, org.graalvm.word.Pointer *, org.graalvm.word.UnsignedWord *) ()
	at com/oracle/svm/core/UnmanagedMemoryUtil.java:169
169    com/oracle/svm/core/UnmanagedMemoryUtil.java: No such file or directory.

gdb は、どのメソッドがクラッシュの原因となったのか、それがソースコードのどこにあるのかを教えてくれることがすでにわかりました。また、この状態に至ったコールグラフのバックトレースも得ることができます。

(gdb) bt
#0  com.oracle.svm.core.UnmanagedMemoryUtil::copyLongsBackward(org.graalvm.word.Pointer *, org.graalvm.word.Pointer *, org.graalvm.word.UnsignedWord *) () at com/oracle/svm/core/UnmanagedMemoryUtil.java:169
#1  0x0000000000461e14 in com.oracle.svm.core.UnmanagedMemoryUtil::copyBackward(org.graalvm.word.Pointer *, org.graalvm.word.Pointer *, org.graalvm.word.UnsignedWord *) () at com/oracle/svm/core/UnmanagedMemoryUtil.java:110
#2  0x0000000000461dc8 in com.oracle.svm.core.UnmanagedMemoryUtil::copy(org.graalvm.word.Pointer *, org.graalvm.word.Pointer *, org.graalvm.word.UnsignedWord *) () at com/oracle/svm/core/UnmanagedMemoryUtil.java:67
#3  0x000000000045d3c0 in com.oracle.svm.core.JavaMemoryUtil::unsafeCopyMemory(java.lang.Object *, long, java.lang.Object *, long, long) () at com/oracle/svm/core/JavaMemoryUtil.java:276
#4  0x00000000013277de in jdk.internal.misc.Unsafe::copyMemory0 () at com/oracle/svm/core/jdk/SunMiscSubstitutions.java:125
#5  jdk.internal.misc.Unsafe::copyMemory(java.lang.Object *, long, java.lang.Object *, long, long) () at jdk/internal/misc/Unsafe.java:788
#6  0x00000000013b1a3f in jdk.internal.misc.Unsafe::copyMemory () at jdk/internal/misc/Unsafe.java:799
#7  sun.misc.Unsafe::copyMemory () at sun/misc/Unsafe.java:585
#8  org.acme.CrashResource::hello(void) () at org/acme/CrashResource.java:22

同様に、他のスレッドのコールグラフのバックトレースを取得できます。

  1. まず、利用可能なスレッドを以下のように一覧表示できます。

    (gdb) info threads
      Id   Target Id                                             Frame
      1    Thread 0x7fcc62a07d00 (LWP 322) "debugging-nativ" 0x00007fcc62b8b77a in __futex_abstimed_wait_common () from /lib64/libc.so.6
      2    Thread 0x7fcc60eff640 (LWP 326) "gnal Dispatcher" 0x00007fcc62b8b77a in __futex_abstimed_wait_common () from /lib64/libc.so.6
    * 4    Thread 0x7fcc5b7fe640 (LWP 328) "ecutor-thread-0" com.oracle.svm.core.UnmanagedMemoryUtil::copyLongsBackward(org.graalvm.word.Pointer *, org.graalvm.word.Pointer *, org.graalvm.word.UnsignedWord *) () at com/oracle/svm/core/UnmanagedMemoryUtil.java:169
      5    Thread 0x7fcc5abff640 (LWP 329) "-thread-checker" 0x00007fcc62b8b77a in __futex_abstimed_wait_common () from /lib64/libc.so.6
      6    Thread 0x7fcc59dff640 (LWP 330) "ntloop-thread-0" 0x00007fcc62c12c9e in epoll_wait () from /lib64/libc.so.6
    ...
  2. 検査するスレッドを選択します (例: スレッド 1)。

    (gdb) thread 1
    [Switching to thread 1 (Thread 0x7ffff7a58d00 (LWP 1028851))]
    #0  __futex_abstimed_wait_common64 (private=0, cancel=true, abstime=0x0, op=393, expected=0, futex_word=0x2cd7adc) at futex-internal.c:57
    57	    return INTERNAL_SYSCALL_CANCEL (futex_time64, futex_word, op, expected,
  3. そして最後に、スタックトレースを出力します。

    (gdb) bt
    #0  __futex_abstimed_wait_common64 (private=0, cancel=true, abstime=0x0, op=393, expected=0, futex_word=0x2cd7adc) at futex-internal.c:57
    #1  __futex_abstimed_wait_common (futex_word=futex_word@entry=0x2cd7adc, expected=expected@entry=0, clockid=clockid@entry=0, abstime=abstime@entry=0x0, private=private@entry=0,
        cancel=cancel@entry=true) at futex-internal.c:87
    #2  0x00007ffff7bdd79f in __GI___futex_abstimed_wait_cancelable64 (futex_word=futex_word@entry=0x2cd7adc, expected=expected@entry=0, clockid=clockid@entry=0, abstime=abstime@entry=0x0,
        private=private@entry=0) at futex-internal.c:139
    #3  0x00007ffff7bdfeb0 in __pthread_cond_wait_common (abstime=0x0, clockid=0, mutex=0x2ca07b0, cond=0x2cd7ab0) at pthread_cond_wait.c:504
    #4  ___pthread_cond_wait (cond=0x2cd7ab0, mutex=0x2ca07b0) at pthread_cond_wait.c:619
    #5  0x00000000004e2014 in com.oracle.svm.core.posix.headers.Pthread::pthread_cond_wait () at com/oracle/svm/core/posix/thread/PosixJavaThreads.java:252
    #6  com.oracle.svm.core.posix.thread.PosixParkEvent::condWait(void) () at com/oracle/svm/core/posix/thread/PosixJavaThreads.java:252
    #7  0x0000000000547070 in com.oracle.svm.core.thread.JavaThreads::park(void) () at com/oracle/svm/core/thread/JavaThreads.java:764
    #8  0x0000000000fc5f44 in jdk.internal.misc.Unsafe::park(boolean, long) () at com/oracle/svm/core/thread/Target_jdk_internal_misc_Unsafe_JavaThreads.java:49
    #9  0x0000000000eac1ad in java.util.concurrent.locks.LockSupport::park(java.lang.Object *) () at java/util/concurrent/locks/LockSupport.java:194
    #10 0x0000000000ea5d68 in java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject::awaitUninterruptibly(void) ()
        at java/util/concurrent/locks/AbstractQueuedSynchronizer.java:2018
    #11 0x00000000008b6b30 in io.quarkus.runtime.ApplicationLifecycleManager::run(io.quarkus.runtime.Application *, java.lang.Class *, java.util.function.BiConsumer *, java.lang.String[] *) ()
        at io/quarkus/runtime/ApplicationLifecycleManager.java:144
    #12 0x00000000008bc055 in io.quarkus.runtime.Quarkus::run(java.lang.Class *, java.util.function.BiConsumer *, java.lang.String[] *) () at io/quarkus/runtime/Quarkus.java:67
    #13 0x000000000045c88b in io.quarkus.runtime.Quarkus::run () at io/quarkus/runtime/Quarkus.java:41
    #14 io.quarkus.runtime.Quarkus::run () at io/quarkus/runtime/Quarkus.java:120
    #15 0x000000000045c88b in io.quarkus.runner.GeneratedMain::main ()
    #16 com.oracle.svm.core.JavaMainWrapper::runCore () at com/oracle/svm/core/JavaMainWrapper.java:150
    #17 com.oracle.svm.core.JavaMainWrapper::run(int, org.graalvm.nativeimage.c.type.CCharPointerPointer *) () at com/oracle/svm/core/JavaMainWrapper.java:186
    #18 0x000000000048084d in com.oracle.svm.core.code.IsolateEnterStub::JavaMainWrapper_run_5087f5482cc9a6abc971913ece43acb471d2631b(int, org.graalvm.nativeimage.c.type.CCharPointerPointer *)
        () at com/oracle/svm/core/JavaMainWrapper.java:280

または、1 つのコマンドですべてのスレッドのバックトレースを一覧表示することもできます。

(gdb) スレッドはすべてのバックトレースを適用します

Thread 22 (Thread 0x7fffc8dff640 (LWP 1028872) "tloop-thread-15"):
#0  0x00007ffff7c64c2e in epoll_wait (epfd=8, events=0x2ca3880, maxevents=1024, timeout=-1) at ../sysdeps/unix/sysv/linux/epoll_wait.c:30
#1  0x000000000166e01c in Java_sun_nio_ch_EPoll_wait ()
#2  0x00000000011bfece in sun.nio.ch.EPoll::wait(int, long, int, int) () at com/oracle/svm/core/stack/JavaFrameAnchors.java:42
#3  0x00000000011c08d2 in sun.nio.ch.EPollSelectorImpl::doSelect(java.util.function.Consumer *, long) () at sun/nio/ch/EPollSelectorImpl.java:120
#4  0x00000000011d8977 in sun.nio.ch.SelectorImpl::lockAndDoSelect(java.util.function.Consumer *, long) () at sun/nio/ch/SelectorImpl.java:124
#5  0x0000000000705720 in sun.nio.ch.SelectorImpl::select () at sun/nio/ch/SelectorImpl.java:141
#6  io.netty.channel.nio.SelectedSelectionKeySetSelector::select(void) () at io/netty/channel/nio/SelectedSelectionKeySetSelector.java:68
#7  0x0000000000703c2e in io.netty.channel.nio.NioEventLoop::select(long) () at io/netty/channel/nio/NioEventLoop.java:813
#8  0x0000000000701a5f in io.netty.channel.nio.NioEventLoop::run(void) () at io/netty/channel/nio/NioEventLoop.java:460
#9  0x00000000008496df in io.netty.util.concurrent.SingleThreadEventExecutor$4::run(void) () at io/netty/util/concurrent/SingleThreadEventExecutor.java:986
#10 0x0000000000860762 in io.netty.util.internal.ThreadExecutorMap$2::run(void) () at io/netty/util/internal/ThreadExecutorMap.java:74
#11 0x0000000000840da4 in io.netty.util.concurrent.FastThreadLocalRunnable::run(void) () at io/netty/util/concurrent/FastThreadLocalRunnable.java:30
#12 0x0000000000b7dd04 in java.lang.Thread::run(void) () at java/lang/Thread.java:829
#13 0x0000000000547dcc in com.oracle.svm.core.thread.JavaThreads::threadStartRoutine(org.graalvm.nativeimage.ObjectHandle *) () at com/oracle/svm/core/thread/JavaThreads.java:597
#14 0x00000000004e15b1 in com.oracle.svm.core.posix.thread.PosixJavaThreads::pthreadStartRoutine(com.oracle.svm.core.thread.JavaThreads$ThreadStartData *) () at com/oracle/svm/core/posix/thread/PosixJavaThreads.java:194
#15 0x0000000000480984 in com.oracle.svm.core.code.IsolateEnterStub::PosixJavaThreads_pthreadStartRoutine_e1f4a8c0039f8337338252cd8734f63a79b5e3df(com.oracle.svm.core.thread.JavaThreads$ThreadStartData *) () at com/oracle/svm/core/posix/thread/PosixJavaThreads.java:182
#16 0x00007ffff7be0b1a in start_thread (arg=<optimized out>) at pthread_create.c:443
#17 0x00007ffff7c65650 in clone3 () at ../sysdeps/unix/sysv/linux/x86_64/clone3.S:81

Thread 21 (Thread 0x7fffc97fa640 (LWP 1028871) "tloop-thread-14"):
#0  0x00007ffff7c64c2e in epoll_wait (epfd=53, events=0x2cd0970, maxevents=1024, timeout=-1) at ../sysdeps/unix/sysv/linux/epoll_wait.c:30
#1  0x000000000166e01c in Java_sun_nio_ch_EPoll_wait ()
#2  0x00000000011bfece in sun.nio.ch.EPoll::wait(int, long, int, int) () at com/oracle/svm/core/stack/JavaFrameAnchors.java:42
#3  0x00000000011c08d2 in sun.nio.ch.EPollSelectorImpl::doSelect(java.util.function.Consumer *, long) () at sun/nio/ch/EPollSelectorImpl.java:120
#4  0x00000000011d8977 in sun.nio.ch.SelectorImpl::lockAndDoSelect(java.util.function.Consumer *, long) () at sun/nio/ch/SelectorImpl.java:124
#5  0x0000000000705720 in sun.nio.ch.SelectorImpl::select () at sun/nio/ch/SelectorImpl.java:141
#6  io.netty.channel.nio.SelectedSelectionKeySetSelector::select(void) () at io/netty/channel/nio/SelectedSelectionKeySetSelector.java:68
#7  0x0000000000703c2e in io.netty.channel.nio.NioEventLoop::select(long) () at io/netty/channel/nio/NioEventLoop.java:813
#8  0x0000000000701a5f in io.netty.channel.nio.NioEventLoop::run(void) () at io/netty/channel/nio/NioEventLoop.java:460
#9  0x00000000008496df in io.netty.util.concurrent.SingleThreadEventExecutor$4::run(void) () at io/netty/util/concurrent/SingleThreadEventExecutor.java:986
#10 0x0000000000860762 in io.netty.util.internal.ThreadExecutorMap$2::run(void) () at io/netty/util/internal/ThreadExecutorMap.java:74
#11 0x0000000000840da4 in io.netty.util.concurrent.FastThreadLocalRunnable::run(void) () at io/netty/util/concurrent/FastThreadLocalRunnable.java:30
#12 0x0000000000b7dd04 in java.lang.Thread::run(void) () at java/lang/Thread.java:829
#13 0x0000000000547dcc in com.oracle.svm.core.thread.JavaThreads::threadStartRoutine(org.graalvm.nativeimage.ObjectHandle *) () at com/oracle/svm/core/thread/JavaThreads.java:597
#14 0x00000000004e15b1 in com.oracle.svm.core.posix.thread.PosixJavaThreads::pthreadStartRoutine(com.oracle.svm.core.thread.JavaThreads$ThreadStartData *) () at com/oracle/svm/core/posix/thread/PosixJavaThreads.java:194
#15 0x0000000000480984 in com.oracle.svm.core.code.IsolateEnterStub::PosixJavaThreads_pthreadStartRoutine_e1f4a8c0039f8337338252cd8734f63a79b5e3df(com.oracle.svm.core.thread.JavaThreads$ThreadStartData *) () at com/oracle/svm/core/posix/thread/PosixJavaThreads.java:182
#16 0x00007ffff7be0b1a in start_thread (arg=<optimized out>) at pthread_create.c:443
#17 0x00007ffff7c65650 in clone3 () at ../sysdeps/unix/sysv/linux/x86_64/clone3.S:81

Thread 20 (Thread 0x7fffc9ffb640 (LWP 1028870) "tloop-thread-13"):
...

ただし、バックトレースを取得できるにもかかわらず、list コマンドを使用してソースコードをある点で引き続き一覧表示できないことに注意してください。

(gdb) list
164    in com/oracle/svm/core/UnmanagedMemoryUtil.java

This is because gdb is not aware of the location of the source files. We are running the executable outside the target directory. To fix this we can either rerun gdb from the target directory or, run directory target/debugging-native-1.0.0-SNAPSHOT-native-image-source-jar/sources e.g.:

(gdb) directory target/debugging-native-1.0.0-SNAPSHOT-native-image-source-jar/sources
Source directories searched: /data/target/debugging-native-1.0.0-SNAPSHOT-native-image-source-jar/sources:$cdir:$cwd
(gdb) list
164        	UnsignedWord offset = size;
165        	while (offset.aboveOrEqual(32)) {
166            	offset = offset.subtract(32);
167            	Pointer src = from.add(offset);
168            	Pointer dst = to.add(offset);
169            	long l24 = src.readLong(24);
170            	long l16 = src.readLong(16);
171            	long l8 = src.readLong(8);
172            	long l0 = src.readLong(0);
173            	dst.writeLong(24, l24);

169 行を調べて、何が問題なのか最初のヒントを得ることができます(この場合、アドレス 0x0000 を含む src からの最初の読み取りに失敗していることがわかります)。また、 gdbup コマンドを使ってスタックをさかのぼり、コードのどの部分がこのような状況を引き起こしたかを確認することができます。ネイティブ実行可能ファイルをデバッグするためのgdbの使い方については、 こちらをご覧ください。

よくある質問

ネイティブ実行可能ファイルを生成するプロセスが遅いのはなぜですか?

ネイティブ実行可能ファイルの生成は、複数のステップで構成されています。その中でも解析とコンパイルのステップは最もコストがかかるため、ネイティブ実行可能ファイルの生成にかかる時間の大半を占めます。

解析フェーズでは、プログラムのメインメソッドから静的なPoint-to 解析を開始し、到達可能なものを見つけ出します。新しいクラスが発見されると、設定に応じてこのプロセス中にその一部が初期化されます。次のステップでは、ヒープがスナップショットされ、どのタイプが実行時に利用可能である必要があるかのチェックが行われます。初期化とヒープのスナップショットにより、新しい型が発見されることがありますが、その場合はこのプロセスが繰り返されます。このプロセスは、到達可能なプログラムがこれ以上成長しないという固定点に達したときに停止します。

コンパイルのステップは非常に簡単で、到達可能なすべてのコードを単純にコンパイルします。

解析とコンパイルの段階でかかる時間は、アプリケーションの大きさによって異なります。アプリケーションが大きければ大きいほど、コンパイルにかかる時間は長くなります。ただし、指数関数的な効果をもたらす機能もあります。例えば、リフレクションアクセスのために型やメソッドを登録する場合、解析はその型やメソッドの背後にあるものを簡単に見ることができないため、解析ステップを完了するためにはより多くの仕事をしなければなりません。

I get an OutOfMemoryError (OOME) building native executables, what can I do?

Building native executables is not only time consuming, but it also takes a fair amount of memory. For example, building a sample native Quarkus JPA application such as the Hibernate quickstart, may use 6GB to 8GB resident set size in memory. A big chunk of this memory is Java heap, but extra memory is required for other aspects of the JVM that runs the native building process. It is still possible to build such applications in environments that have total memory close to the limits, but to do that it is necessary to shrink the maximum heap size of the GraalVM native image process. To do that, set a maximum heap size using the quarkus.native.native-image-xmx property. For example, we can instruct GraalVM to use 5GB of maximum heap size by passing in -Dquarkus.native.native-image-xmx=5g in the command line.

Building native executables this way might have the side effect of requiring more time to complete. This is due to garbage collection having to work harder for native image generation to have free space to do its job.

Note that typical applications are likely bigger than quickstarts, so the memory requirements will also likely be higher.

JVMモードと比較して、ネイティブ実行可能ファイルのランタイムパフォーマンスが劣るのはなぜですか?

As with most things in life there are some trade-offs involved when choosing native compilation over JVM mode. So depending on the application the runtime performance of a native application might be slower compared to JVM mode, though that’s not always the case.

JVMによるアプリケーションの実行には、実行中に蓄積されるプロファイル情報を利用したコードの実行時最適化が含まれます。これには、より多くのコードをインライン化したり、ホットコードをダイレクトパスに配置したり(つまり、より良い命令キャッシュのローカリティを確保する)、コールドパスにある多くのコードをカットしたりする機会が含まれます(JVMでは、多くのコードが何かが実行しようとするまでコンパイルされず、最適化解除や再コンパイルを引き起こすトラップに置き換えられます)。コールドパスを取り除くことで、コンパイルされる少量のホットコードの分岐の複雑さや組み合わせロジックが大幅に削減されるため、先行してコンパイルする場合よりも多くの最適化の機会が得られます。

一方、ネイティブ実行可能ファイルのコンパイルでは、オフラインでコードをコンパイルする際に、すべての可能な実行経路に対応しなければなりません。なぜならば、ホットパスやコールドパスがわからないため、罠を仕掛けて、それに当たったら再コンパイルするというようなトリックが使えないからです。同じ理由で、ホットパスを隣接して配置することでコードキャッシュの衝突を最小限に抑えるようなサイコロを積むこともできません。ネイティブ実行可能ファイルの生成は、閉じた世界の仮説により、いくつかのコードを削除することができますが、それだけでは、プロファイリングや実行時最適化解除&再コンパイルがJVM JITコンパイラに提供するすべての利点を補うことはできません。

ただし、JVMの速度が向上する可能性があるため、その代償として、リソース(CPUとメモリの両方)の使用量と起動時間が増加することに注意してください。

  1. JITが作動してコードを完全に最適化するまでに時間がかかります。

  2. JIT コンパイラは、アプリケーションが利用できるリソースを消費します。

  3. JVMは、より良い最適化をサポートするために、より多くのメタデータやコンパイラ/プロファイラのデータを保持しなければなりません。

1)の理由は、コードはしばらくの間、インタプリタ実行する必要があり、場合によっては、以下を担保する全ての潜在的な最適化が実現される前に、何度もコンパイルする必要があるからです。

  1. そのコードパスはコンパイルする価値があります。つまり、十分な回数実行されており、

  2. 意味のある最適化を行うための十分なプロファイリングデータがあります。

1)の意味するところは、小規模で短命なアプリケーションには、ネイティブ実行可能ファイルの方が適しているということです。コンパイルされたコードは最適化されていませんが、すぐに利用できます。

2)の理由は、JVMは基本的に実行時にアプリケーションと並行してコンパイラを実行しているからです。ネイティブ実行可能ファイルの場合、コンパイラは事前に実行されるため、アプリケーションと並行してコンパイラを実行する必要がありません。

3)にはいくつかの理由があります。JVMは閉じた世界を想定していません。そのため、新しいクラスのロードにより、コンパイル時の楽観的な仮定を修正する必要がある場合には、コードを再コンパイルできなければなりません。例えば、あるインターフェイスの実装が1つだけの場合、そのコードに直接コールジャンプすることができます。しかし、2つ目の実装クラスがロードされた場合には、レシーバのインスタンスのタイプをテストして、そのクラスに属するコードにジャンプするようにコールサイトを修正する必要があります。このような最適化をサポートするには、ネイティブ実行可能ファイルよりもクラスベースの詳細を記録しておく必要があります。これには、完全なクラスとインターフェイスの階層、どのメソッドが他のメソッドをオーバーライドするかの詳細、すべてのメソッドのバイトコードなどが含まれます。ネイティブ実行可能ファイルでは、クラス構造やバイトコードの詳細のほとんどは実行時には無視できます。

また、JVMはクラスベースや実行プロファイルの変更にも対応しなければならず、その結果、スレッドが以前のコールドパスを通ることになります。その時点で、JVMはコンパイルされたコードからインタープリタにジャンプし、以前のコールドパスを含む新しい実行プロファイルに対応するためにコードを再コンパイルしなければなりません。そのためには、コンパイルされたスタックフレームを1つまたは複数のインタープリタフレームに置き換えることができる実行時情報を保持する必要があります。また、実行されたもの、されなかったものを追跡するために、ランタイムの拡張可能なプロファイルカウンタを割り当て、更新する必要があります。

なぜネイティブ実行可能ファイルは大きいのですか?

これには様々な理由があります。

  1. ネイティブ実行可能ファイルには、アプリケーションのコードだけでなく、ライブラリのコードやJDKのコードも含まれています。そのため、ネイティブ実行可能ファイルのサイズは、アプリケーションのサイズに加えて、使用するライブラリのサイズとJDKのサイズを加えたものと比較するのが、より公平な比較となります。特にJDKの部分は、HelloWorldのようなシンプルなアプリケーションでも無視できません。イメージの中で何が引き出されているかを把握するために、ネイティブ実行可能ファイルをビルドする際に -H:+PrintUniverse を使用することができます。

  2. ネイティブ実行可能ファイルには、実行時に実際には使われないかもしれないのに、必ず含まれている機能があります。そのような機能の例として、ガベージコレクションがあります。コンパイル時には、アプリケーションが実行時にガベージコレクションを実行する必要があるかどうかはわかりません。そのため、ガベージコレクションは、必要がないにもかかわらず、常にネイティブ実行可能ファイルに含まれ、サイズが大きくなります。ネイティブ実行可能ファイルの生成は、どのコードパスが到達可能かを特定するために、静的なコード解析に依存していますが、静的なコード解析は不正確な場合があり、実際に必要なコードよりも多くのコードがイメージに入ってしまうことがあります。

この話題については、 GraalVMアップストリーム課題で興味深い議論が行われています。

バイナリの生成に使用したMandrelのバージョンは?

どのバージョンのMandrelを使ってバイナリを生成したかは、バイナリを以下のように検査すればわかります。

$ strings target/debugging-native-1.0.0-SNAPSHOT-runner | grep GraalVM
com.oracle.svm.core.VM=GraalVM 22.0.0.2-Final Java 11 Mandrel Distribution

ネイティブ実行可能ファイルでGCロギングを有効にするにはどうすればいいですか?

ネイティブ実行可能ファイルに -XX:PrintFlags= を付けて実行すると、ネイティブ実行可能ファイルに渡すことのできるフラグのリストが表示されます。様々なレベルのGCロギングのために、次のように使用することができます。

$ ./target/debugging-native-1.0.0-SNAPSHOT-runner -XX:PrintFlags=
...
  -XX:±PrintGC                                 Print summary GC information after each collection. Default: - (disabled).
  -XX:±PrintGCSummary                          Print summary GC information after application main method returns. Default: - (disabled).
  -XX:±PrintGCTimeStamps                       Print a time stamp at each collection, if +PrintGC or +VerboseGC. Default: - (disabled).
  -XX:±PrintGCTimes                            Print the time for each of the phases of each collection, if +VerboseGC. Default: - (disabled).
  -XX:±PrintHeapShape                          Print the shape of the heap before and after each collection, if +VerboseGC. Default: - (disabled).
...
  -XX:±TraceHeapChunks                         Trace heap chunks during collections, if +VerboseGC and +PrintHeapShape. Default: - (disabled).
  -XX:±VerboseGC                               Print more information about the heap before and after each collection. Default: - (disabled).

ネイティブ実行可能ファイルのヒープダンプを取得することはできますか? 例えば、メモリ不足になった場合などです。

Starting with GraalVM 22.2.0 it will be possible to heap dumps upon request, e.g. kill -SIGUSR1 <pid>. Support for dumping the heap dump upon an out of memory error will follow up.

Can I build and run this examples outside a container in Linux?

Quarkusのネイティブ実行可能ファイルのデバッグは、Linux環境で行うのが最適です。一部のデバッグ手順を実行するために必要なパッケージをインストールする場合や、 perf でカーネルのイベントを収集できるようにする場合を除き、ルートアクセスは必要ありません。macOSやWindows環境でのデバッグは、コンテナ環境でも機能します( FAQエントリを参照)。

これらのパッケージは、異なるデバッグセクションを実行するために、Linux環境で必要となるものです。

# dnf (rpm-based)
sudo dnf install binutils gdb perf perl-open
# Debian-based distributions:
sudo apt install binutils gdb perf

フレームグラフの生成に時間がかかったり、エラーが発生したりするのですが、どうすればいいですか?

Mandrelが作成したネイティブ実行可能ファイルをプロファイリングする方法は複数あります。すべての方法で、 -H:-DeleteLocalSymbols オプションを渡す必要があります。

このリファレンス・ガイドで紹介する方法は、DWARFのデバッグ情報を含むバイナリを生成し、 perf record を通して実行し、 perf script とフレーム・グラフ・ツールを使用してフレーム・グラフを生成します。しかし、このバイナリで行われる perf script の後処理ステップは、時間がかかったり、DWARF のエラーが表示されたりすることがあります。

フレームグラフを生成する別の方法として、ネイティブ実行可能ファイルを生成する際に、DWARFのデバッグ情報を生成する代わりに、 -H:+PreserveFramePointer を渡す方法があります。これは、フレームポインタに追加のレジスタを使用するようにバイナリに指示します。これにより、 perf は、実行時の動作をプロファイリングするためにスタックウォーキングを行うことができます。これらのフラグを使用してネイティブ実行可能ファイルを生成するには、以下のようにします。

./mvnw package -DskipTests -Dnative
    -Dquarkus.native.additional-build-args=-H:+PreserveFramePointer,-H:-DeleteLocalSymbols

実行時プロファイリング情報をネイティブ実行可能ファイルから取得するには、単純に次のようにします。

perf record -F 1009 -g -a ./target/debugging-native-1.0.0-SNAPSHOT-runner

実行時プロファイリング情報を生成する方法としては、フレームポインタを保持したバイナリを生成するよりも、デバッグ情報を使用することを推奨します。これは、ネイティブ実行可能ファイルのビルドプロセスにデバッグ情報を追加しても、実行時のパフォーマンスには何の影響もないのに対し、フレームポインタの保持は影響があるためです。

DWARFのデバッグ情報は、別のファイルに生成され、デフォルトのデプロイメントでは省略することもでき、プロファイリングやデバッグの目的で必要なときだけ転送して使用することができます。さらに、デバッグ情報があることで、 perf は関連するソースコード行も表示することができ、ネイティブ実行可能ファイル自体を肥大化させることはありません。そのためには、 perf report にソースコード行を表示するパラメータを追加して呼び出すだけです。

perf report --stdio -F+srcline
...
83.69%     0.00%  GreetingResource.java:20 ...
...
83.69%     0.00%  AbstractStringBuilder.java:1025 ...
...
83.69%     0.00%  ArraycopySnippets.java:95 ...

The performance penalty of preserving the frame pointer is due to using the extra register for stack walking, particularly in x86_64 compared to aarch64 where there are fewer registers available. Using this extra register reduces the number of registers that are available for other work, which can lead to performance penalties.

native-imageのバグを見つけたようなのですが、IDEでどのようにデバッグすればいいのでしょうか?

コンテナ内のプロセスをリモートデバッグすることは可能ですが、Mandrelをローカルにインストールしてシェルプロセスのパスに追加することで、native-imageをステップバイステップでデバッグする方が簡単かもしれません。

ネイティブ実行可能ファイルの生成は、2つのJavaプロセスが順次実行された結果です。最初のプロセスは非常に短く、主な仕事は2番目のプロセスのために物事を準備することです。2つ目のプロセスは、ほとんどの作業を行うものです。一方のプロセスをデバッグするための手順は、若干異なります。

まず、最もデバッグしたいと思われる2番目のプロセスのデバッグ方法について説明します。2番目のプロセスのスタートポイントは、 com.oracle.svm.hosted.NativeImageGeneratorRunner クラスです。このプロセスをデバッグするには、ビルド時の引数として --debug-attach=*:8000 を追加するだけです。

./mvnw package -DskipTests -Dnative \
    -Dquarkus.native.additional-build-args=--debug-attach=*:8000

1番目のプロセスのスタートポイントとなるのは、 com.oracle.svm.driver.NativeImages クラスです。GraalVM CEのディストリビューションでは、この最初のプロセスはバイナリなので、従来のようにJava IDEを使ってデバッグすることはできません。しかし、Mandrelのディストリビューション(またはローカルにビルドされたGraalVM CEインスタンス)では、これを通常のJavaプロセスとして保持しているため、 --vm.agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=*:8000 を追加のビルド引数として追加することで、このプロセスをリモートデバッグすることができます。

$ ./mvnw package -DskipTests -Dnative \
    -Dquarkus.native.additional-build-args=--vm.agentlib:jdwp=transport=dt_socket\\,server=y\\,suspend=y\\,address=*:8000

JFR/JMCを使って、ネイティブバイナリのデバッグやプロファイリングはできますか?

Java Flight Recorder(JFR)JDK Mission Control(JMC)は、GraalVM CE 21.2.0以降、プロファイル・ネイティブ・バイナリのデバッグに使用することができます。 しかし、GraalVMのJFRは現在、HotSpotと比較して機能が大幅に制限されています。カスタムイベントAPIは完全にサポートされていますが、多くのVMレベルの機能は利用できません。これらは将来のリリースで追加される予定です。現在の制限事項は以下の通りです。

  • 最小限の VM レベルのイベント

  • oldオブジェクトのサンプリングはありません

  • スタックトレースのトレースがありません

  • JDK17 のストリーミング API はありません

JFRを使用するには、アプリケーションのプロパティ -Dquarkus.native.enable-vm-inspection=true を追加します。例:

./mvnw package -DskipTests -Dnative -Dquarkus.native.container-build=true \
    -Dquarkus.native.builder-image=quay.io/quarkus/ubi-quarkus-mandrel:22.1-java11 \
    -Dquarkus.native.enable-vm-inspection=true

イメージのコンパイルが完了したら、ランタイムフラグ -XX:+FlightRecorder-XX:StartFlightRecording を使ってJFRを有効にし、起動します。例えば、以下のようになります。

./target/debugging-native-1.0.0-SNAPSHOT-runner \
    -XX:+FlightRecorder \
    -XX:StartFlightRecording="filename=recording.jfr"

For more details on using JFR, see here.

How can we troubleshoot performance problems only reproducible in production?

In this situation, switching to JVM mode would be the best thing to try first. If the performance issues continue after switching to JVM mode, you can use more established and mature tooling to figure out the root cause. If the performance issue is limited to native mode only, you might not be able to use perf, so JFR is the only way to gather any information in this situation. As JFR support for native expands, the ability to detect root causes of performance issues directly in production will improve.

What information helps most debug issues that happen either at build-time or run-time?

To fix classpath, class initialization or forbidden API errors at build time it’s best to use build time reports to understand the closed world universe. A complete picture of the universe, along with the relationships between the different classes and methods will help uncover and fix most of the issues.

To fix runtime native specific errors, it’s best to have debug info builds of the native executables around, so that gdb can be hooked up quickly to debug the issue. If you also add local symbols to the debug info builds, you will obtain precise profiling information as well.