#chiroito ’s blog

Java を中心とした趣味の技術について

JFR Under the hood : ローカルで動くJFR Event Streamingのリアルタイム性

結論

ローカルに作られるリポジトリからイベントを読み込みます。イベントは非同期でこのファイルに書込まれます。そのため、本当の意味でのリアルタイムにはイベントは処理されないため気を付けましょう。

JFR Event Streamingとは

Java 14 で追加された JFR Event Streaming、以下の様に書くと JVM の内部のイベントとかをストリーミングで処理してくれる素晴らしい機能です。

import jdk.jfr.Configuration;
import jdk.jfr.consumer.EventStream;
import jdk.jfr.consumer.RecordingStream;
import java.io.IOException;
import java.text.ParseException;
import java.time.Duration;
import java.util.concurrent.TimeUnit;

public class JfrEventStreamingLocalAsync {
    public static void main(String... args) {
        try (EventStream es = new RecordingStream(Configuration.getConfiguration("default"))) {
            es.onEvent("jdk.JVMInformation", System.out::println);
            es.startAsync();
            creatingEventsProcess();
            es.awaitTermination(Duration.ofSeconds(2));
        } catch (ParseException | IOException | InterruptedException e) {
            System.err.println("Couldn't start JFR Event Streaming");
        }
    }
}

この機能、ローカル実行とリモート実行ができ、それぞれでEventStreamインスタンスを作る方法が異なります。

リモート実行はEventStream.openRepository(Path)メソッドを使用してEventStreamインスタンスを作るので「あ~JFRのリポジトリから取るんだな~。ですよね~」と思ってました。

また、ローカル実行の場合はEventStreamクラスのサブクラスであるRecordingStreamコンストラクタを使用します。このコンストラクタにどの設定でJFRを開始するかを指定してインスタンスを作ります。ローカル実行の場合はstartする時に新たに JFR の記録を開始します。そのため、JFRが内部で持つバッファを見ていてイベントが追加された瞬間にリアルタイムにイベントが処理されるのだろうなぁと思っていました。

読む場所はどこか

「Javaからバッファを見るAPIが追加されてるだろうから見てみるか」と思ってソースを見たところ、EventDirectoryStreamとなっていました。これはリポジトリからイベントを読み込むクラスです。このクラスのコンストラクタは第2引数にリポジトリのパスを指定します。しかし、この引数はnullが渡されていました。

    public RecordingStream() {
        Utils.checkAccessFlightRecorder();
        AccessControlContext acc = AccessController.getContext();
        this.recording = new Recording();
        try {
            PlatformRecording pr = PrivateAccess.getInstance().getPlatformRecording(recording);
            this.directoryStream = new EventDirectoryStream(acc, null, SecuritySupport.PRIVILEGED, pr);
        } catch (IOException ioe) {
            this.recording.close();
            throw new IllegalStateException(ioe.getMessage());
        }
    }

https://github.com/openjdk/jdk/blob/6d137a36169956171bfd0afef91e8ce29b568e33/src/jdk.jfr/share/classes/jdk/jfr/consumer/RecordingStream.java#L88

「バッファから読む場合は null なのかな?」と思って読み進めてみると、パスを決めるために実行されるRepositoryFilesクラスのprivate boolean updatePaths() throws IOExceptionメソッドに以下の様なコメントが記載されていました。

// Always get the latest repository if 'jcmd JFR.configure
// repositorypath=...' has been executed
SafePath sf = Repository.getRepository().getRepositoryPath();

https://github.com/openjdk/jdk/blob/6d137a36169956171bfd0afef91e8ce29b568e33/src/jdk.jfr/share/classes/jdk/jfr/internal/consumer/RepositoryFiles.java#L166

どうやら JFR の内部 API である Repositoryクラスを使用して最新のリポジトリを取得しているようです。このクラスはシングルトンで、名前の通り JFR のリポジトリを管理しています。

まとめ

これまでのソースコードで、JFR Event Streaming をローカル実行すると、その JVM にある最新のリポジトリからイベントを読み込んでいるのが分かります。JVM によって記録されるイベントは非同期でこのファイルへ書込み、JFR Event Streaming は書込まれたイベントを都度読み込んで処理されます。そのため、イベントの処理はリアルタイムではなく少し遅れて処理されます。

Quarkus で JFR Event Streaming

Java 14 から JDK Flight Recorder で取得した情報を Java プロセス内で処理する JFR Event Streaming が導入されました。今回はこれを Quarkus で使ってみたいと思います。

今回のサンプルでは JFR では標準的な情報を取得するようにします。JFR Event Streaming は、JVMの情報を記録するイベントを受け取ったらそのイベントを標準出力に処理します。Java MP のアプリケーションスコープの開始処理と終了処理で JFR Event Streaming をそれぞれ開始/終了します。

開始処理では、JFCファイルというJFRの設定ファイル名を指定して設定を読み込み、ストリームを作成します。さらに、ストリームを流れるイベントの種類ごとに処理内容を指定し、ストリームを開始します。今回読み込むJFCファイルはdefault.jfcなので、defaultを指定します。また、今回受け取るイベントの名前はjdk.JVMInformationです。

終了処理では、ストリームがあれば閉じます。

import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.event.Observes;
import java.io.IOException;
import java.text.ParseException;

@ApplicationScoped
public class JfrStreamingBean {

    private EventStream es;

    void onStart(@Observes StartupEvent ev) {
        try {
            Configuration config = Configuration.getConfiguration("default");
            this.es = new RecordingStream(config);
            this.es.onEvent("jdk.JVMInformation", System.out::println);

            this.es.startAsync();
        } catch (ParseException | IOException e) {
            System.err.println("Couldn't start JFR Event Streaming");
        }
    }

    void onStop(@Observes ShutdownEvent ev) {
        if (this.es != null) {
            es.close();
        }
    }
}

このプログラムを実行してみます。Quarkus の通常の実行方法で実行します。今回は開発者モードで実行します。

mvnw.cmd quarkus:dev

Quarkus が実行したあとに、JVM情報のイベントであるjdk.JVMInformationというイベントが出力されます。

__  ____  __  _____   ___  __ ____  ______ 
 --/ __ \/ / / / _ | / _ \/ //_/ / / / __/ 
 -/ /_/ / /_/ / __ |/ , _/ ,< / /_/ /\ \   
--\___\_\____/_/ |_/_/|_/_/|_|\____/___/   
2020-05-28 18:16:55,594 INFO  [io.quarkus] (Quarkus Main Thread) quarkus-jfr-streaming 1.0-SNAPSHOT (powered by Quarkus 1.4.2.Final) started in 1.110s. Listening on: http://0.0.0.0:8080
2020-05-28 18:16:55,605 INFO  [io.quarkus] (Quarkus Main Thread) Profile dev activated. Live Coding activated.
2020-05-28 18:16:55,605 INFO  [io.quarkus] (Quarkus Main Thread) Installed features: [cdi, resteasy, smallrye-metrics]
jdk.JVMInformation {
  startTime = 18:16:55.549
  jvmName = "OpenJDK 64-Bit Server VM"
  jvmVersion = "OpenJDK 64-Bit Server VM (14+36-1461) for windows-amd64 JRE (14+36-1461), built on Feb  6 2020 19:03:18 by "mach5one" with MS VC++ 15.9 (VS2017)"
  jvmArguments = "-XX:TieredStopAtLevel=1 -Xverify:none -Xdebug -Xrunjdwp:transport=dt_socket,address=0.0.0.0:5005,server=y,suspend=n -Djava.util.logging.manager=org.jboss.logmanager.LogManager"
  jvmFlags = N/A
  javaArguments = "C:\Users\cito\develop\quarkus-sample\quarkus-jfr-streaming\target\quarkus-jfr-streaming-dev.jar"
  jvmStartTime = 18:16:54.186
  pid = 9092
}

Infinispan と Spring Session を使ったセッションレプリケーション

環境

  • SpringBoot 2.3.0
  • Infinispan 10.1.7

サンプルソースはこちら

GitHub - chiroito/spring-session-sample

手順

Java EE がインフラの設定だけで実現できることと比べ、SpringBoot はアプリ側を少し書き換えて設定しないと行けません。

手順は以下のとおりです。

  1. セッションを使ったアプリを書く
  2. プロジェクトに依存関係を追加
  3. アプリケーションにアノテーションを追加
  4. アプリに Infinispan へ接続する設定を追加

0. Infinispan を起動

今回は 2 ノードの Infinispan を起動します。 ダウンロードした Infinispan の zip を 2 回別の名前で解凍します。 普通に実行すると使用しているポートが被ってしまいます。それを回避するため引数に-Dinfinispan.socket.binding.port-offsetを付与します。それぞれのディレクトリで次の様に実行します。

1 つ目

bin\server.bat -Dinfinispan.node.name=nodeA

2 つ目

bin\server.bat -Dinfinispan.node.name=nodeB -Dinfinispan.socket.binding.port-offset=100

1. セッションを使ったアプリを書く

アプリはセッションを使ったものを普通に書くだけです。Infinispan にセッションをレプリケーションするかどうかで特に違いはありません。以下はセッション情報となるカウンターとそれを使ったエンドポイントのアプリです。

Counter.java

import org.springframework.stereotype.Component;
import org.springframework.web.context.annotation.SessionScope;

import java.io.Serializable;

@SessionScope
@Component
public class Counter implements Serializable {

    private static final long serialVersionUID = 1L;

    private int value;

    public int getValue() {
        return value;
    }

    public void countUp(){
        this.value++;
    }
}

CountEndpoint.java

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/session")
public class CountEndpoint {

    private final Counter counter;

    public CountEndpoint(Counter counter) {
        this.counter = counter;
    }

    @GetMapping("/count")
    public String count(){

        counter.countUp();
        return "counter=" + counter.getValue();
    }

    @GetMapping("/read")
    public String read(){

        return "counter=" + counter.getValue();
    }
}

2. プロジェクトに依存関係を追加

プロジェクトの依存関係に Spring Session の実装への依存関係を追加しないといけません。今回はセッションの格納先に Infinispan を使用するので Infinispan への依存関係を追加します。

<dependency>
    <groupId>org.infinispan</groupId>
    <artifactId>infinispan-bom</artifactId>
    <version>10.1.7.Final</version>
    <type>pom</type>
    <scope>import</scope>
</dependency>
<dependency>
    <groupId>org.infinispan</groupId>
    <artifactId>infinispan-spring-boot-starter</artifactId>
    <version>2.2.4.Final</version>
</dependency>
<dependency>
    <groupId>org.infinispan</groupId>
    <artifactId>infinispan-spring-boot-starter-remote</artifactId>
    <version>2.2.4.Final</version>
</dependency>

3. アプリケーションにアノテーションを追加

キャッシュを使えるようにするのと Infinispan のクラスタにセッションを入れられるようにするため、SpringBootのアプリケーションには 2 つのアノテーションを追加します。追加するアノテーションは@EnableCaching@EnableInfinispanRemoteHttpSessionです。

@EnableCaching
@EnableInfinispanRemoteHttpSession
@SpringBootApplication
public class InfinispanSessionApplication {

    public static void main(String[] args) {
        SpringApplication.run(InfinispanSessionApplication.class, args);
    }
}

実行

最後に実行しましょう。SpringBoot を通常どおり実行するだけです。