原文はこちら。
The original article was written by Erik Gahlin (Consulting Member of Technical Staff, Oracle).
https://egahlin.github.io/2021/05/17/remote-recording-stream.html
アプリケーション監視ツールは、以前からJMXを使用してネットワーク経由で継続的にデータを取得することができました。例えば、CPU負荷はOperatingSystemMXBean
から取得し、JDK Mission Controlで視覚化できます。JFRは、スタックトレースやタイムスタンプ付きの値など、構造化されたよりリッチなデータを提供しますが、JDK 16までは、これらの情報を発生時にネットワーク経由で転送する方法がありませんでした。
JDK 14で、ストリームイベントへのAPIのサポートが追加されました。コードスニペットでご紹介しましょう。
JEP 349: JFR Event Streaming
https://openjdk.java.net/jeps/349
1. Passive(プロセス内)
try (EventStream stream = EventStream.openRepository()) {
stream.onEvent("jdk.JavaMonitorEnter", System.out::println),
stream.start();
}
2. Passive(プロセス外)
Path path = Path.of("/repository/2021_05_16_09_48_31_60185");
try (EventStream stream = EventStream.openRepository(path) {
stream.onEvent("jdk.JavaMonitorEnter", System.out::println),
stream.start();
}
3. Active(プロセス内)
try (RecordingStream stream = new RecordingStream()) {
stream.enable("jdk.JavaMonitorEnter").withStackTrace();
stream.onEvent("jdk.JavaMonitorEnter", System.out::println),
stream.start();
}
ここでいうアクティブとは、3つ目のコードスニペットのenabledメソッドに見られるように、録画のライフサイクルやイベントの設定をストリームが制御できることを意味します。上記のAPIは多くのシナリオに対応していますが、以下のような場合には使えません。
- リモートホスト上のJavaプロセスを監視する
- リモートホストや別のプロセスで記録されている内容を制御する
JDK 11から、リモートで録画をコントロールしたりダウンロードしたりできるFlightRecordingMXBeanがOpenJDKに存在します。
Interface FlightRecorderMXBean
https://docs.oracle.com/en/java/javase/16/docs/api/jdk.management.jfr/jdk/management/jfr/FlightRecorderMXBean.html
これでJDK Mission Control が録画データを取得し、リモート・マシンでイベントを構成します。JDK 15およびそれ以前のリリースでは、クライアントがデータを読み取る前に録画を停止する必要がありました。
JDK Mission Control
https://www.oracle.com/java/technologies/jdk-mission-control.html
https://www.oracle.com/jp/java/technologies/jdk-mission-control.html
JDK 16ではこの制限がなくなり、JFRでMBeanServerConnectionを使用してリモートホストを監視できるようになりました。
Interface MBeanServerConnection
https://docs.oracle.com/en/java/javase/16/docs/api/java.management/javax/management/MBeanServerConnection.html
4. Active(プロセス外およびネットワーク越し)
String host = "com.example";
int port = 7091;
String url = "service:jmx:rmi:///jndi/rmi://" + host + ":" + port + "/jmxrmi";
JMXServiceURL u = new JMXServiceURL(url);
JMXConnector c = JMXConnectorFactory.connect(u);
MBeanServerConnection connection = c.getMBeanServerConnection();
try (RemoteRecordingStream stream = new RemoteRecordingStream(connection)) {
stream.enabled("jdk.JavaMonitorEnter").withStackTrace();
stream.onEvent("jdk.JavaMonitorEnter", System.out::println),
stream.start();
}
RemoteRecordingStream
の実装では、FlightRecorderMXBean::readStream(long)
メソッドからデータを読み取り、リモートホスト上でJVMが行うのと同様に、チャンク単位でローカルにディスクに書き込みます。その後、別のスレッドがディスク上のデータを解析し、onEvent
ハンドラにイベントをディスパッチします。これで1秒に1回新しいデータが読めるようになります。
readStreamメソッド(Interface FlightRecorderMXBean)
https://docs.oracle.com/en/java/javase/16/docs/api/jdk.management.jfr/jdk/management/jfr/FlightRecorderMXBean.html#readStream(long)
データセグメントが完了する前にパーサースレッドがデータを読まないようにするため、チャンクヘッダーには、ファイルのどの範囲までデータを読めるかを示すサイズフィールドがあります。新しいデータが到着してセグメントが完了すると、このフィールドが更新されます。読み込み中にサイズフィールドが変更されないようにするために、パーサーがワードティアリング(word tearing)を避けるためのプロトコルがあります。

チャンクファイルが読み込まれ、そのイベントをディスパッチすると、ファイルはクライアントから削除されます。代わりにデータを保持するには、setMaxAge(Duration)
とsetMaxSize(long)
の2つのポリシーを設定して、データを保持する期間を決定できます。
RemoteRecordingStream
クラスは、イベントのストリーミングだけではなく、ディスクリポジトリを別のホストに移行することもできます。監視対象のアプリケーションがクラッシュして、ホスト上のディスクリポジトリファイルが削除されても、RemoteRecordingStream
が動作するマシン上ではまだ利用可能です。今後、RemoteRecordingStream
にdump
メソッドを追加し、何か問題が発生した場合に簡単にファイルを抽出できるようにする予定です。
Streaming event metadata
他のプロセスからイベントをストリーミングする際の複雑な点は、イベントのメタデータにアクセスできないことです。インプロセスでのストリーミングにおいて、FlightRecorder::getEventTypes()
メソッドを呼び出して、登録されたすべてのイベントタイプのリストを取得できます。
getEventTypes
メソッド(Class FlightRecorder)
https://docs.oracle.com/en/java/javase/16/docs/api/jdk.jfr/jdk/jfr/FlightRecorder.html#getEventTypes()
イベントタイプの知識がないと、フィールド・レイアウトや、どのイベントを有効にしたり設定したりするかを決定できません。
このような状況を改善するために、EventStream
インターフェースに新しいメソッドが追加されました。
Interface EventStream
https://docs.oracle.com/en/java/javase/16/docs/api/jdk.jfr/jdk/jfr/consumer/EventStream.html
void onMetadata(MetadataEvent);
MetadataEvent
は、登録されたすべてのイベントタイプとのリストと、JDKに付属するdefault
とprofile
の2つの構成を持ちます。onEvent
ハンドラが呼び出される前にMetadataEvent
が送信されます。新しいイベントタイプが登録されたり登録解除されたりすると、MetadataEvent
が更新され、更新されたイベントが送信されます。
Class MetadataEvent
https://docs.oracle.com/en/java/javase/16/docs/api/jdk.jfr/jdk/jfr/consumer/MetadataEvent.html
RemoteRecordingStream
クラスの動作を確認するために、イベントをサブスクライブして標準出力に出すHealthReport.java
という小さな1個のファイルのプログラムがあります。
Class RemoteRecordingStream
https://docs.oracle.com/en/java/javase/16/docs/api/jdk.management.jfr/jdk/management/jfr/RemoteRecordingStream.html
Health Report
https://github.com/flight-recorder/health-report

Usage:
$ java HealthReport.java com.example:7091
Resources
JEP 328: Flight Recorder
https://openjdk.java.net/jeps/328
JEP 349: JFR Event Streaming
https://openjdk.java.net/jeps/349
[JDK-8253898] JFR: Remote Recording Stream
https://bugs.openjdk.java.net/browse/JDK-8253898
RemoteRecordingStreamクラス
https://docs.oracle.com/en/java/javase/16/docs/api/jdk.management.jfr/jdk/management/jfr/RemoteRecordingStream.html
MetadataEventクラス
https://docs.oracle.com/en/java/javase/16/docs/api/jdk.jfr/jdk/jfr/consumer/MetadataEvent.html
Health Report
https://github.com/flight-recorder/health-report