#chiroito ’s blog

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

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 を通常どおり実行するだけです。

HotRod ストアを使ったWildflyのセッションレプリケーション

これまでは、リモートストアを使用して HTTP セッションを Infinispan やその商用製品である Red Hat Data Grid へオフロードしていました。ですがその方法は Wildfly 14 (JBoss EAP 7.2)から古くなったようです。新しい方法は同バージョンから導入されたelytron サブシステムと統合する HotRod プロトコルをベースとするカスタムの最適化されたキャッシュストアを使います。

24.3.10. リモートストアを使用した HTTP セッションの Red Hat Data Grid への外部化

環境

  • Wildfly 19.1
  • Infinispan 9.4.18
  • Java EE 8

今回構築する構成は以下の様な構成です。1 つの Wildfly と 2 つの Infinispan を構築します。

f:id:chiroito:20200519172347p:plain

サンプルのソースコードはこちらにあります。

GitHub - chiroito/wildfly-session-sample

手順

Infinispan の設定は何も必要ありません。Wildfly の設定をしてアプリがそれを使えるように設定をすればセッションレプリケーションできます。

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

  1. 接続先となる Infinispan を定義する (下図の青)
  2. remote-cache-container と remote-cluster を定義する(下図の黄色)
  3. invalidation-cache を定義する (下図の赤)
  4. アプリでキャッシュを使うように設定する
  5. アプリをデプロイする

f:id:chiroito:20200521160717p:plain

0. Infinispan を起動する。

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

1つ目

bin\standalone.bat -c clustered.xml -Djboss.node.name=nodeA

2つ目

bin\standalone.bat -c clustered.xml -Djboss.node.name=nodeB -Djboss.socket.binding.port-offset=100

1. 接続先となる Infinispan を定義する

これからの作業には jboss-cliを使用します。これはwlidflyのインストール先のbin\jboss-cli.batです。jboss-cli.bat -cで起動したら以下を実行します。

batch
/socket-binding-group=standard-sockets/remote-destination-outbound-socket-binding=infinispan-server1:add(host=127.0.0.1, port=11222)
/socket-binding-group=standard-sockets/remote-destination-outbound-socket-binding=infinispan-server2:add(host=127.0.0.1, port=11322)
run-batch

2. remote-cache-container と remote-cluster を定義する

こちらも同様に Jboss-cli で以下を実行します。

batch
/subsystem=infinispan/remote-cache-container=foo:add(default-remote-cluster=bar)
/subsystem=infinispan/remote-cache-container=foo/remote-cluster=bar:add(socket-bindings=[infinispan-server1, infinispan-server2])
run-batch

3. invalidation-cache を定義する

以前の方法とはここが変りました。以前はリモートストアを使っていましたが、今回はHotRodストアを使います。

これまでの方法では以下の様にstoreremoteでした。

/subsystem=infinispan/cache-container=web/invalidation-cache=infinispan/store=remote

これからの方法では以下の様にstorehotrodになります。

/subsystem=infinispan/cache-container=web/invalidation-cache=infinispan/store=hotrod

こちらも同様に Jboss-cli で以下を実行します。

batch
/subsystem=infinispan/cache-container=web/invalidation-cache=baz:add()
/subsystem=infinispan/cache-container=web/invalidation-cache=baz/component=transaction:add(mode=BATCH)
/subsystem=infinispan/cache-container=web/invalidation-cache=baz/component=locking:add(isolation=REPEATABLE_READ)
/subsystem=infinispan/cache-container=web/invalidation-cache=baz/store=hotrod:add(remote-cache-container=foo, fetch-state=false, purge=false, passivation=false, shared=true)
run-batch

4. アプリでキャッシュを使うように設定する

アプリケーションは以下の 2 つの設定ファイルを変更します。

  • WEB-INF/web.xml
  • WEB-INF/jboss-web.xml

WEB-INF/web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">
    <distributable/>
</web-app>

WEB-INF/jboss-web.xml

<?xml version="1.0" encoding="UTF-8"?>
<jboss-web xmlns="http://www.jboss.com/xml/ns/javaee"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xsi:schemaLocation="http://www.jboss.com/xml/ns/javaee http://www.jboss.org/j2ee/schema/jboss-web_13_0.xsd"
           version="13.0">

    <replication-config>
        <cache-name>web.baz</cache-name>
    </replication-config>

</jboss-web>

5. アプリをデプロイする

アプリケーションをビルドして Wildfly へデプロイします。ビルドには Maven などのビルドツールを使い、デプロイはこれまで同様にjboss-cliを使います。

cd wildfly-session-sample
mvn clean package
jboss-cli.bat -c
deploy target\wildfly-session-sample.war

確認

環境構築がうまくいってアプリがちゃんとできていると Wildfly が以下の様なメッセージを出力します。

[2020-05-19 03:19:43,748] Artifact wildfly-session-sample:war: Artifact is being deployed, please wait...
(略)
15:19:44,225 WARN  [org.wildfly.clustering.web.undertow] (MSC service thread 1-7) WFLYCLWEBUT0004: Legacy <replication-config/> overriding attached distributable session management provider for wildfly-session-sample.war
15:19:44,246 INFO  [org.jboss.as.clustering.infinispan] (ServerService Thread Pool -- 102) WFLYCLINF0032: Getting remote cache named 'wildfly-session-sample.war'. If it does not exist a new cache will be created from configuration template named 'transactional'; null value uses default cache configuration on the Infinispan Server.
(略)
15:19:44,282 INFO  [org.infinispan.CLUSTER] (ServerService Thread Pool -- 102) [Context=wildfly-session-sample.war] ISPN100008: Updating cache members list [laptop-tlfueb3p], topology id 2
(略)
[2020-05-19 03:19:44,510] Artifact wildfly-session-sample:war: Artifact is deployed successfully
[2020-05-19 03:19:44,510] Artifact wildfly-session-sample:war: Deploy took 762 milliseconds

以下の URL へアクセスしてみてください。

以下の場合は各設定ファイルがきちんと書かれているか、デプロイしている war ファイル内のディレクトリ構成が正しいかを確認しましょう。私は IntelliJ IDEA の Enterprise Java で war ファイルを作ったところ後者に嵌まりました・・・

デプロイした時のログがこんな場合jboss-web.xmlの設定ができていません。

[2020-05-19 03:20:19,013] Artifact wildfly-session-sample:war: Artifact is being deployed, please wait...
(略)
15:20:19,613 INFO  [org.infinispan.configuration.cache.MemoryConfigurationBuilder] (MSC service thread 1-4) ISPN000152: Passivation configured without an eviction policy being selected. Only manually evicted entities will be passivated.
15:20:19,628 INFO  [org.infinispan.configuration.cache.MemoryConfigurationBuilder] (MSC service thread 1-4) ISPN000152: Passivation configured without an eviction policy being selected. Only manually evicted entities will be passivated.
(略)
[2020-05-19 03:20:19,905] Artifact wildfly-session-sample:war: Artifact is deployed successfully
[2020-05-19 03:20:19,905] Artifact wildfly-session-sample:war: Deploy took 892 milliseconds

このログの(略)に挟まれているログすら出ない場合は web.xmlの設定ができていません。

参考

24.3.9. HTTP セッションの Red Hat Data Grid への外部化