GraalVM ネイティブイメージのJDK Flight Recorderサポート:これまでの歩み

Red Hat で Solution Architect として OpenJDK を担当している伊藤ちひろ(@chiroito)です。

この記事は、Red Hat Developerのブログ記事、JDK Flight Recorder support for GraalVM Native Image: The journey so far | Red Hat Developer の翻訳記事です。


https://developers.redhat.com/sites/default/files/styles/article_feature/public/ST-java1_1x%20%283%29_3.png?itok=y36CMCts

過去1年間、OracleとRed Hatのエンジニアは、GraalVM ネイティブイメージにJDK Flight Recorder(JFR)サポートを導入するために協力してきました。プロトタイプのプルリクエストは、2020 年後半と 2021 年前半に Red Hat と Oracle によって最初に導入されました。その後、すべての関係者の作業を組み込んだ新しいプルリクエストを寄稿する予定で、共有リポジトリで作業が続けられました。2021年6月には、GraalVM上のJDK Flight Recorderをサポートするためのインフラを導入する最初のプルリクエストをOpenJDK 11のアップストリームにマージしました。これは、GraalVMの21.2リリースで利用可能です。この記事では、その背後にある詳細な情報を共有します。ネイティブ・イメージとは何か、なぜJDK Flight Recorderの追加に取り組んだのか、私たちが直面した技術的課題のいくつか、そして私たちが次に何をしようとしているのか、です。

GraalVMネイティブイメージのパフォーマンスプロファイリング

GraalVMは、Javaアプリケーションのネイティブイメージを生成するためのサポートを含む高性能ランタイムです。この機能は、Javaのクラスファイルを受け取り、ターゲットプラットフォーム用のバイナリ実行ファイルを生成します。出来上がったネイティブ実行ファイルには、SubstrateVMと呼ばれるJavaで書かれたランタイムシステムが組み込まれており、メモリ管理やスレッドスケジューリングなどのコンポーネンツを含んでいます。これは、OpenJDKがHotSpot仮想マシン(VM)を使ってJavaアプリケーションを実行するのに似ています。ネイティブ実行ファイルは、ポインタ解析とヒープ生成を繰り返し、その後、事前コンパイルを経て生成されます。GraalVMによって生成されるネイティブ実行ファイルは、HotSpotのような従来のJVM上で実行されるJavaアプリケーションと比較して、理想的にはかなり速い起動時間を実現します。

より良いパフォーマンスを約束するバイナリ実行可能ファイルは、モニタリングとパフォーマンスプロファイリングのためのサポートツールのエコシステムをまだ必要とします。しかしながら、現時点では、OpenJDK上のJavaアプリケーションのために一般的に使用されるモニタリング機能のほとんどは、GraalVM上のGraalVM ネイティブイメージのために利用できません。アプリケーションの実行とランタイム実行レイヤーを調査するツールを持たずに、アプリケーションのパフォーマンス問題を診断することは困難です。この不備は、コンテナであろうとベアメタル上であろうと、本番にデプロイされるアプリケーションにとって重要です。

なぜ JDK Flight Recorder か?

JDK Flight Recorderは、Hotspot VMのプロファイリングシステムで、イベントベースのロギングシステムを通じてJVM内部データとカスタム、アプリケーション固有のデータを提供します。JDK Flight Recorderは、実稼働環境における重要なデータを最小限のパフォーマンスオーバーヘッドで収集するよう設計されています。従来、出力はJFRファイル(.jfr)であり、JDK Mission Controlなどのツールで読み込むことができました。JDK 14以降では、Java APIを通じてデータをストリーミングできます。

GraalVM ネイティブイメージに対するJDK Flight Recorderのサポートは、HotSpot VMの体験に似た強力なパフォーマンスプロファイリングツールをユーザーに提供することになります。理想的には、OpenJDK上のJavaアプリケーションのプロファイリングにJDK Flight Recorderを使用している開発者が、ネイティブ実行ファイルにも同じように使用できるようにすることです。ネイティブイメージ用のJFRは、HotSpotと同じJFRファイルとストリーミング機能でSubstrateVM内部データ、およびカスタムでアプリケーション固有のデータを提供するSubstrateVMのプロファイリングシステムとなります。

JFR for GraalVM ネイティブイメージは、SubstrateVMシステムの内部データを提供し、HotSpotで見られるデータの種類を多く模倣しています。これには、ガベージコレクション操作、スレッド状態、モニター状態、例外、セーフポイント、オブジェクト割り当て、ファイルまたはソケットI/Oイベントなどについての情報が含まれます。

内部イベントとカスタムイベントによるプロファイリング

HotSpotのJDK Flight Recorderやその他のプロファイリングツールは、さまざまな質問に答えるのに役立っています。GraalVM ネイティブイメージのJDK Flight Recorderは、同じことをするのが目的です。これらの質問には、以下のようなものがあります。

  • ガベージコレクタで消費される実行時間はどれくらいですか?
  • セーフポイント、Stop-the-World操作にどれだけの時間が費やされていますか?
  • 実行フローにボトルネックはあるか、それはどこにありますか?
  • 私のアプリケーションでホットなメソッドは何ですか?
  • I/O処理にはどれくらいの時間がかかっていますか?
  • デッドロックやロック待ちのオペレーションなど、同期の問題はどうでしょうか?

JDK Flight Recorderは、内部イベントとともに、開発者が独自のイベントをシステムに追加するためのAPIを備えています。これにより、ユーザーは、既存の低オーバーヘッドで本番環境でも使えるJFRインフラストラクチャの利点を生かした分析用にさらに多くのデータを追加できます。カスタムイベントは、ランタイム定数(クラス名、メソッド名、文字列など)の共通プールを利用でき、個別のカスタムイベントやメトリックを書き出すエージェントと比較して、全体的に出力コストを削減できます。

パフォーマンスの問題を解決

イベント単体では、性能問題を診断するのに十分ではありません。幸い、既存のJFRフォーマットでは、JDK Mission Controlのような可視化・解析ツールとすばやく統合できます。これらのツールを併用することで、ネイティブイメージのアプリケーションのパフォーマンス問題を解決できるのです。SubstrateVMのイベントのうち、HotSpotのイベントと一対一で一致するものは、既存の解析ツールに変更を加えることなく、すでに理解されていることでしょう。

注:JDK Flight Recorder for HotSpotには多数のリソースがあり、そのうちのいくつかは記事の最後にリストアップしています。JDK Flight Recorder全般について、またパフォーマンス問題の解決にもたらすメリットについて、ぜひご覧ください。

GraalVMネイティブイメージのためのJDKフライトレコーダサポートの実装

私たちの作業以前は、GraalVMネイティブイメージのJDK Flight Recorderサポートはなく、そのサポートを実装する際に多くの課題に遭遇しました。Java APIはアクセス可能ですが、JDK Flight Recorderシステムを管理する仕組みはありません。さらに、JDK Flight RecorderのJava APIを直接使用する既存のコード、例えば記録を開始するコードは、unsatisfied link errorで失敗します。この失敗は、JDK Flight RecorderのネイティブAPIが存在しないために起こります。この特定のエラーは、HotSpot ライブラリである libjvm.so にある JDK Flight Recorder のネイティブ コードが SubstrateVM システムに含まれていないために発生します。残念ながら、ネイティブコードはHotSpotの内部と深いつながりがあるため、取り込むのに適していません。

GraalVMネイティブイメージにJDKフライトレコーダーを追加するため、HotSpot VMのJDKフライトレコーダー用ネイティブインフラを、SubstrateVMのJavaコードで完全に再実装しました。しかし、C++で書かれたHotSpotのコードを、Javaで書かれたSubstrateVMの対応するコードに一対一で変換できませんでした。この項では、その理由のいくつかを説明します。

クラス変換

まず、HotSpotのイベントクラスは名目上は空で、実行時にクラス変換を行い、有効なイベント用のメソッドに実装を追加しています。メソッドは空の本体を持ち、finalとマークされ、extendsを防いでいます。イベントが変換されたときに実装がどのようになるかは、EventInstrumentationのコードを見てください。

変換されたイベントクラスは、イベントを発するためにSubstrateVMで必要とされますが、ネイティブイメージの場合、クラスの変換をランタイムで実行できません。OpenJDK JDK Flight Recorderには、Java Virtual Machine Tool Interface(JVMTI)経由でクラスを変換するためのretransformというブーリアン・フラグがあります。デフォルト値はtrueです。これを回避するには、例えば以下のコマンドを使用して、SubstrateVM解析のために実行されているJVMの値をfalseに設定できます。

$ mx native-image -J-XX:FlightRecorderOptions=retransform=false ...

その結果、SubstrateVMが見るイベントクラスには、イベントが有効でJDK Flight Recorderが実行されているかのように、変換された実装が含まれます。

もうひとつの解決策は、 jdk.jfr.internal.JVM の内部 API である retransformClasses を使用して、コンパイル前にイベントクラスを変換することです。この回避策も HotSpot JDK Flight Recorder の実装の詳細に依存していますが、変換するクラスをより細かく制御できます。

無停止型セーフティポイント

デフォルトでは、ネイティブコード(HotSpot)はセーフポイントのために割り込むことができませんが、Javaコード(SubstrateVM)は割り込めます。幸いなことに、SubstrateVMにはuninterruptibleアノテーションがあり、これを用いてセーフポイントのために中断されないメソッドブロックをマークできます。すべての「ネイティブ」部分をuninterruptibleとしてマークするのは、パフォーマンスが影響を受けるので、理想的ではありません。同様に、HotSpotのJDK Flight Recorderコードパスの中には、「can safepoint here」とコメントされている(そしてセーフポイントを許可するように適切にマークされている)ものもあるので、やはりSubstrateVMの実装には細心の注意を払って、整合性を保つ必要があります。

Uninterruptibleアノテーションの使用には、制限もあります。オブジェクトの割り当てができないのです。これは、オブジェクト指向のプログラミング言語であるJavaでおなじみのコードパターンを実装する際に、非常に問題となります。オブジェクトの割り当てを行うtoStringメソッドはUninterruptibleメソッドコールで使用できないため、ロギングさえも難しくなります。特に、同時実行の問題を扱う場合、単純なデバッガーでは、追いかけている問題と同じ実行フローを見られないため、デバッグが通常より少し難しくなってしまうのです。

さらに、ガベージコレクタのようなSubstrateVMの重要な部分には、中断可能とマークされているメソッドがあります。これらの場所でイベント書き込みインフラを起動できるようにする必要があるため、そのコンポーネントも中断できる必要があります。そのためには、Javaオブジェクトを割り当てないようにする必要があります。幸いなことに、SubstrateVMは、ガベージコレクションによって管理されないmalloc、つまりメモリを解放するためのメソッドを提供しています。しかし、これらのインスタンスはObjectを拡張していないため、ArrayListのような既存のJava APIを使ってデータを操作できません。これらのデータ構造が必要な場合は、再実装する必要があります。

JFR Recorder スレッド

データ書き込みのための重要な作業の一部は、HotSpotではJFR Recorderスレッドという単一のスレッドに与えられ、それ自体はJDK Flight Recorderシステムから除外されています。どのスレッドもガベージコレクションなどの仮想マシン操作に流用できるため、SubstrateVMではこの除外を真似できません。ガベージコレクタは、関連するJDK Flight Recorderイベントを発行する可能性があり、除外すると、そのような操作からイベントデータが欠落することになります。

イベントがディスクに書き込まれ、エポックが遷移し、メタデータと定数プールが書き込まれるチャンクのクローズが、ガベージコレクション操作のために一時停止されることさえ可能です。このアクションは、それからガベージコレクションイベントをバッファに書き込めます。ここでのタスクは、これらのガベージコレクションイベントとその定数プールデータのための一貫性を保つために、分析され、中断できないように適切にマークされる必要があります。先に説明したチャンクを閉じるための書き込み操作は、ガベージコレクションに充当される可能性を考慮して、中断できない部分を持つセーフポイント操作として慎重に書く必要があります。

まとめ

JDK Flight Recorderは、HotSpot VMの重要な機能で、実稼働環境での連続実行に適した低いオーバーヘッドで、JVMの実行に関する膨大な量のデータをユーザーに提供します。SubstrateVMでネイティブイメージアプリケーションを実行する際、開発者が同じツールにアクセスできるのは便利でしょう。

JFRインフラの初期マージは完了しましたが、HotSpotで可能なのと同様に、GraalVMで生成されたネイティブ実行ファイルへのビューをシステムが提供できるまでには、長い道のりが待っています。次は、ガベージコレクション、スレッド、例外など、SubstrateVMの有用な場所に対するイベントを追加する作業です。SubstrateVMには今のところRJMX APIが実装されていないため、JDK Flight Recorderをリモートで管理するAPIはありません。これと並行して、OpenJDKとHotSpotでは、JFRシステムの大幅な改良と強化がまだ行われています。異なる基礎となるOpenJDKのバージョンを適切にサポートするために、SubstrateVMの実装でAPIに影響を与える大きな変更を考慮する必要があります。現時点では、コードベースはOpenJDK 11のみを対象としています。

とりあえず、最新のGraalVMを試し、ビルド時のネイティブイメージプロセスに-H:+AllowVMInspectionというフラグを渡して、JDK Flight Recorderをテストできます。それが終わったら、実行時にアプリケーション・バイナリに -XX:+FlightRecorder -XX:StartFlightRecording="filename=recording.jfr" などのフラグを追加するとよいでしょう。

GraalVMと並んで、Quarkusのコミュニティ・ディストリビューションであるMandrelも、当然JFRのサポートを引き継ぎます。JDK Flight Recorder for GraalVM Native Imageの改善と改良を続けながら、QuarkusとMandrel環境でのネイティブイメージでのJFRの使用に関する発表と記事をお待ちください。

JDK Flight Recorderについてもっと知る

JDK Flight Recorderの詳細と、パフォーマンス問題の解決に使用するためのリソースは、以下のとおりです。

  • An introduction to Middleware Application Monitoring with Java Mission Control and Flight Recorder (Mario Torre and Marcus Hirt, FOSDEM 2019)
  • Get started with JDK Flight Recorder in OpenJDK 8u (Mario Torre, Red Hat Developer)
  • Using Java Flight Recorder with JDK 11 (Laszlo Csontos, DZone)
  • Troubleshoot Performance Issues Using JFR (Java Platform, Standard Edition Troubleshooting Guide, Oracle)
  • JDK Flight Recorder—a gem hidden in OpenJDK (BellSoft)
  • JDK11—Introduction to JDK Flight Recorder (Markus Grönlund, Oracle YouTube)
  • Monitoring REST APIs with Custom JDK Flight Recorder Events (Gunnar Morling)
  • Introduction to ContainerJFR: JDK Flight Recorder for containers (Andrew Azores, Red Hat Developer)

* 各記事は著者の見解によるものでありその所属組織を代表する公式なものではありません。その内容については非公式見解を含みます。