Vorstellung: Java Flight Recorder

05.07.2021

Java kann fast so schön wie Fliegen sein. Dank dem Java Flight Recorder und dem Java Mission Control können spannende Daten über den Flug einer Java Anwendung gesammelt werden.

Anwendungen verstehen und nachvollziehen zu können, ist beim Einsatz und der Entwicklung von Software ein wichtiger Aspekt. Neben den bestehenden Lösungen zum Logging, Monitoring und Debugging gibt es mit dem Java Flight Recorder einen frischen "Player", der in dem Java Ökosystem an Relevanz zu gewinnen scheint. Daher soll dieser Artikel eine grundlegende Übersicht über das JVM Werkzeug schaffen.

Der Java Flight Recorder

JFR steht für Java Flight Recorder. Dahinter verbergen sich Werkzeuge zum analysieren und besser verstehen von Java Anwendungen. JFR ist ein Bestandteil des JDKs und scheint unter anderem durch den Wechsel von einer kommerziellen Lizenz zu einer offeneren Lizenz relevanter mit aktuellen Java Versionen zu werden.

Das Prinzip hinter dem Werkzeug kann, wie der Name schon verrät, mit einem Flugschreiber verglichen werden. Eine Java Anwendung produziert dabei Events, welche dann bei Bedarf aufgezeichnet und ausgewertet werden können. Gegenüber klassischen Logging und Metrikenframeworks liegt die Besonderheit tatsächlich darin, Events gezielt über einen Zeitraum aufzeichnen zu können. Das Ergebnis ist eine Datei, die mit der Desktopanwendung Java Mission Control ausgewertet werden kann. Der Weg über die Datei hat den Vorteil, dass man eine Aufzeichnung starten kann und es dann nicht notwendig ist, während der Aufzeichnung eine bestehende Verbindung aufrechtzuhalten.

Die GraalVM hat seit Kurzem auch eine Unterstützung für den Java Flight Recorder.

JFR lebt von Events

Grundlegend ist das Konzept der Events. Events beinhalten nicht nur Domain spezifische Informationen über das Geschehnis, sondern bilden immer auch Metriken, wie z.B. den Zeitpunkt des Auftretens und die Dauer ab. Generell werden folgende drei Eventtypen unterschieden:

  • Instant Events werden direkt beim Auftreten erfasst
  • Duration Events bilden eine Zeitspanne ab und werden entsprechend erst beim Abschluss erfasst
  • Sample Events erfassen mit einer Regelmäßigkeit einen bestimmten Zustand

Neben vielen vordefinierten Events aus dem JDK kann man beliebige eigene Events definieren. Dazu erstellt man für jedes Event eine Klasse, die man von `jdk.jfr.Event` ableitet. Über Annotationen kann man dann Metainformationen, wie z.B. einen Namen oder eine Beschreibung, ergänzen. Über entsprechend annotierte Felder werden die möglichen Daten des Events definiert.

Hier ein Beispiel, wie solch ein Event definiert werden könnte, wenn z.B. ein User eine Datei von einem Server herunterlädt.

@Name(DownloadEvent.NAME)
@Label("Download")
@Category("Api")
@Description("Download of a file from the server")
@StackTrace(false)
public class DownloadEvent extends Event {

	static final String NAME = "de.doubleslash.jfr.demo.Download";

	@Label("User")
	public String user;

	@Label("Media Type")
	public String mediaType;

	@Label("Source")
	public String source;

	@Label("Length")
	@DataAmount
	public int length;

	@Label("Success")
	public boolean success;
}

Dank der Ableitung von `jdk.jfr.Event` kann man das Event wie ein Pojo verwenden. Das heißt man erzeugt über `new` eine Instanz des Events und erfasst es abschließend mit einem Aufruf der `commit()` Methode. Die Api ist aber noch ein wenig komplexer, um besonders effektiv und gezielt Events erfassen zu können.

  • `event.isEnabled()` ermöglicht es, relevanten Code (z.B. das Sammeln von Daten für das Event) nur dann auszuführen, wenn das Event überhaupt gefragt ist.
  • Über `event.begin();` und `event.end();` kann man die Zeitspanne von Duration Events messen.
  • `event.shouldCommit()` hat eine ähnliche Aufgabe wie `event.isEnabled()`. Es kann zwar sein, dass ein Event aktiv ist, aber nur dann erfasst werden soll, wenn es eine gewisse Zeitspanne überschreitet (z.B. zum Erfassen von besonders langsamen Abfragen).

Das folgende Beispiel veranschaulicht, wie die vorgestellten Methoden in Kombination eingesetzt werden können.

DownloadEvent downloadEvent = new DownloadEvent();
if(downloadEvent.isEnabled()) {			
	downloadEvent.user = user;
	downloadEvent.source = source;
	downloadEvent.mediaType = getMediaType(source);
	downloadEvent.begin();
}
// execute the domain logic
long downloadId = download();
if(downloadEvent.isEnabled()) {
	downloadEvent.end();
	downloadEvent.success = isSuccess(downloadId);
	downloadEvent.length = getDownloadLength(downloadId);
	downloadEvent.commit();
}

Events aufzeichnen und auswerten

Damit unsere Anwendung auftretende Events aufzeichnet, müssen wir den Flight Recorder starten. Dazu gibt es die folgenden Möglichkeiten.

Möglichkeit A: JVM Parameter beim Starten der Anwendung

-XX:+FlightRecorder -XX:StartFlightRecording=delay=10s,duration=10m,name=Default,filename=recording.jfr,settings=default
  • `delay` gibt an, wie lange der Start des Flight Recorders nach dem Start der Anwendung verzögert werden soll.
  • `duration` gibt an, wie lange der Flight Recorder aufzeichnen soll.
  • `name` gibt der Aufzeichnung einen Namen, der später hilfreich sein kann, verschiedene Aufzeichnungen zu unterscheiden.
  • `filename` gibt an, wie die Datei mit den Aufzeichnungen heißen soll. Die Datei wird im Verzeichnis angelegt, in dem der Prozess gestartet wird. Diese Datei beinhaltet nach dem Ausführen des Flight Recorders die aufgezeichneten Events.
  • `settings` wählt ein Template aus, das Voreinstellungen (Filterung der Events) für verschiedene Szenarien vorgibt. Die Templates werden aus dem $JAVA_HOME/lib/jfr Verzeichnis gespeist. Hinter dem Template "default" steht zum Beispiel entsprechend die Datei "default.jfc". Die Template Daten lassen sich entweder direkt im XML Format in der Textdatei editieren oder man kann auf die Java Mission Control Oberfläche zurückgreifen, welche einen umfangreichen Editor bereitstellt.

Da man schon beim Starten definieren muss, was aufgezeichnet werden soll, ist diese Variante eher für kurze Ausführungszyklen wie z.B. beim Entwickeln und Testen geeignet und eher nicht für einen langlaufenden Server.

Möglichkeit B: Flight Recorder über `jcmd` starten

`jcmd` ist ein Kommandozeilen Tool, das mit diversen JDK Distributionen mitgeliefert wird. Es erlaubt das Ausführen von Befehlen für in einer JVM laufende Prozesse.

Als Erstes muss die Id des Java Prozesses herausgefunden werden, indem wir den Flight Recorder starten wollen. Dazu rufen wir `jcmd` einfach ohne Parameter auf.

>jcmd
22867 org.eclipse.lemminx.XMLServerLauncher
2967 de.doubleslash.jfr.demo.Demo
3016 jdk.jcmd/sun.tools.jcmd.JCmd

In der ersten Spalte sehen wir die gesuchte Id.

Jetzt können wir über `jcmd` verschiedene JFR Befehle für unseren Prozess ausführen. Das Pattern dafür ist `jcmd <pid|MainClass> <command> [parameters]`.

Mögliche Befehle (command) sind:

  • `JFR.start`
  • `JFR.check`
  • `JFR.stop`
  • `JFR.dump`

Eine Referenz zu den Befehlen kann man auf der Oracle Webseite finden oder sich durch das Pattern `jcmd <pid|MainClass> help <command>` ausgeben lassen.

Hier z.B. die Ausgabe für den `JFR.start` Befehl:

>jcmd 3336 help JFR.start
3336:
JFR.start
Starts a new JFR recording

Impact: Medium: Depending on the settings for a recording, the impact can range from low to high.

Permission: java.lang.management.ManagementPermission(monitor)

Syntax : JFR.start [options]

Options: (options must be specified using the <key> or <key>=<value> syntax)
	name : [optional] Name that can be used to identify recording, e.g. \"My Recording\" (STRING, no default value)
	settings : [optional] Settings file(s), e.g. profile or default. See JAVA_HOME/lib/jfr (STRING SET, no default value)
	delay : [optional] Delay recording start with (s)econds, (m)inutes), (h)ours), or (d)ays, e.g. 5h. (NANOTIME, 0)
	duration : [optional] Duration of recording in (s)econds, (m)inutes, (h)ours, or (d)ays, e.g. 300s. (NANOTIME, 0)
	disk : [optional] Recording should be persisted to disk (BOOLEAN, no default value)
	filename : [optional] Resulting recording filename, e.g. \"/home/user/My Recording.jfr\" (STRING, no default value)
	maxage : [optional] Maximum time to keep recorded data (on disk) in (s)econds, (m)inutes, (h)ours, or (d)ays, e.g. 60m, or 0 for no limit (NANOTIME, 0)
	maxsize : [optional] Maximum amount of bytes to keep (on disk) in (k)B, (M)B or (G)B, e.g. 500M, or 0 for no limit (MEMORY SIZE, 0)
	flush-interval : [optional] Minimum time before flushing buffers, measured in (s)econds, e.g. 4 s, or 0 for flushing when a recording ends (NANOTIME, 1s)
	dumponexit : [optional] Dump running recording when JVM shuts down (BOOLEAN, no default value)
	path-to-gc-roots : [optional] Collect path to GC roots (BOOLEAN, false)

Diese Variante ermöglicht es, eine Ad-hoc Aufzeichnung zu starten. Das funktioniert vor allem dann, wenn man eine Terminalverbindung auf den Rechner mit der zu überwachenden Java Anwendung hat. Voraussetzung ist natürlich auch, dass das `jcmd` Programm auf dem entsprechenden System vorhanden ist.

Möglichkeit C: Java Mission Control verwenden

Das Java Mission Control (kurz: JMC) ist eine grafische Oberfläche für den Java Flight Recorder (kurz: JFR). Heruntergeladen werden kann das Programm zum Beispiel hier: https://adoptopenjdk.net/jmc.html

Da es sich um eine Eclipse Anwendung handelt, kann man das Programm auch als Erweiterung in Eclipse installieren. Dazu findet man unten auf der Download Seite entsprechende Archive, die man in Eclipse über Help > Install New Software... auswählen kann.

Das JMC erlaubt es, die oben aufgeführten Aktionen wie das Starten einer Aufnahme bequem aus einer Übersicht heraus auszuführen. Erst über die Oberfläche bekommt man eine gute Übersicht über die Möglichkeiten, den JFR zu steuern. So kann man zum Beispiel Bedingungen definieren, ab welchen Schwellenwerten eine volle Aufzeichnung gestartet werden soll, um so Randfälle ohne zu viel Overhead einfangen zu können.

Darüber hinaus ist das JMC die erste Wahl, wenn es darum geht, Aufzeichnungen, die mit dem JFR gemacht wurden, auszuwerten. Wie man es von anderen Desktopanwendungen gewohnt ist, wählt man über File > Open File... die Aufzeichnungsdatei (*.jfr) aus.

JMC Event Explorer
Abb. 1: JMC Event Explorer, Quelle: Eigene Darstellung (Screenshot)

Wie im Screenshot oben zu sehen, findet man hier je nach Aufzeichnung jede Menge JVM spezifische Metriken. Im Event Browser findet man auch die weiter oben selbst definierten Events wieder.

Möglichkeit D: JFR Event Streaming

In dem bisher beschriebenen Prinzip hinter dem Java Flight Recorder steckt der Nachteil, dass zwischen Aufzeichnung und Auswertung immer ein wenig Zeit liegt. Man kann also immer nur einen statischen Zeitabschnitt in der Vergangenheit betrachten. Diese Inflexibilität ist scheinbar auch den Entwicklern hinter Java aufgefallen und wurde mit "JEP 349: JFR Event Streaming" angegangen. Wie der Name schon vermuten lässt, ist der Gedanke dahinter, Events on-the-fly und kontinuierlich weiterzuverarbeiten. Damit wird JFR zum Beispiel auch für Anforderungen gerechnet, die Events für eine Monitoring Lösung einzusetzen. JFR Event Streaming ist ab Java 14 verfügbar und lässt sich über eine einfache API verwenden.

  • Als Erstes erstellt man über `new` eine Instanz von `RecordingStream`.
  • Dann aktiviert man die Events über `enable(...)`, an denen man ein Interesse hat. Hier hat man über eine Fluent-Api weitere Möglichkeiten, z.B. einen Threshold oder eine Periode einzustellen, um die Anzahl der Events zu reduzieren.
  • Im nächsten Schritt definiert man über `onEvent(...)` einen Callback, der die eingehenden Events verarbeitet. Der Callback erhält die Events in der Form von `jdk.jfr.consumer.RecordedEvent`, was eine generische Sicht auf ein Event liefert. Möchte man auf die nicht Standard-Eigenschaften des Events zugreifen, kann man je nach Datentyp die getter-Methoden wie `getString(...)`, `getInt(...)` usw. verwenden.
  • Zuletzt muss man den Stream starten. Mit `start()` startet man blockierend im aktuellen Thread. Alternativ kann man mit `startAsync()` die Events auch in einem separaten Thread konsumieren.
try (RecordingStream recordingStream = new RecordingStream()) {
	recordingStream.enable(DownloadEvent.NAME);
	recordingStream.onEvent(DownloadEvent.NAME, event -> {
		int length = event.getInt("length");
		Duration duration = event.getDuration();
		System.out.printf("Downloaded %s bytes in %s ms\n", length, duration.toMillis());				
	});
	recordingStream.start();
}
Fazit

Der Java Flight Recorder bietet eine interessante und frische Möglichkeit, das Verhalten einer Java Anwendung im Auge zu behalten. Die Schnittstellen sind sehr effektiv gehalten und fühlen sich stabil an. Das Java Mission Control ist ein mächtiges Werkzeug, in dem man sicher immer mal wieder neue und interessante Funktionen und Einblicke entdecken kann. JFR verbindet Logging und Metriken zu einem spannenden Werkzeug in der Java Welt.

 

Mehr zu Java Programmierungen erfahren

Zurück zur Übersicht

Kommentar verfassen

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

*Pflichtfelder

*