#chiroito ’s blog

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

Infinispan / Red Hat Data Grid を監視する

Infinispan (Red Hat Data Grid) が提供している監視に使える機能についてまとめました。環境に合わせて適宜設定することをお勧めします。

この記事では Infinispan および Red Hat Data Grid のインストール先のディレクトリをINFINISPAN_HOMEとしています。

ログ出力

出力したいログの設定は${INFINISPAN_HOME}/server/conf/log4j2.xmlで定義します。設定ファイルの名前の通り、設定はLog4j 2の形式で設定します。

ログはデフォルトで{INFINISPAN_HOME}/server/logに出力されます。

メトリクス

Infinispan は OpenMetrics 形式でメトリクスを提供しています。このメトリクスはHTTPでアクセスできます。そのため、このURLを指定することでさまざまな監視ツールからメトリクスを収集できます。

Infinispan のメトリクスは JMX を使用して MBean にアクセスすることでも取得できますが、今の時代では使われることが少ないので省略します。必要な場合は参考資料をご覧ください。

メトリクスを有効にするには、${INFINISPAN_HOME}/server/conf/infinispan.xmlなどのInfinispanの設定ファイルでcache-container要素のstatistics属性にtrueを設定します。

<cache-container name="default" statistics="true"></cache-container>

この設定をしてInfinispanを起動するとHTTPでメトリクスが公開されます。

URL は以下の通りです。

http://<ホスト名>:<ポート番号>/metrics/

以下のようなメトリクスが出力されます。

# HELP base_memory_committedNonHeap_bytes Displays the amount of memory that is committed for the Java virtual machine to use.
# TYPE base_memory_committedNonHeap_bytes gauge
base_memory_committedNonHeap_bytes 9.5879168E7
# HELP base_memory_maxHeap_bytes Displays the maximum amount of memory in bytes that can be used for memory management.
# TYPE base_memory_maxHeap_bytes gauge
base_memory_maxHeap_bytes 1.70917888E10
# HELP base_gc_time_total Displays the approximate accumulated collection elapsed time in milliseconds. This attribute displays -1 if the collection elapsed time is undefined for this collector. The Java virtual machine implementation may use a high resolution timer to measure the elapsed time. This attribute may display the same value even if the collection count has been incremented if the collection elapsed time is very short.
# TYPE base_gc_time_total counter
base_gc_time_total_seconds{name="G1 Young Generation1"} 0.037
(略)

ヘルス

Infinispanはクラスタの状態とキャッシュマネージャの状態を REST API として提供しています。

クラスタの状態

クラスタの状態を取得するには以下の URL へアクセスします。

http://<ホスト名>:<ポート番号>/rest/v2/cache-managers/<キャッシュマネージャ名>/health

URL にはキャッシュマネージャ名を付与します。

キャッシュマネージャ名はデフォルトではdefaultになります。この場合、以下のURLになります。

http://<ホスト名>:<ポート番号>/rest/v2/cache-managers/default/health

出力される結果は以下の通りです。

{
  "cluster_health":{
    "cluster_name":"cluster",
    "health_status":"HEALTHY",
    "number_of_nodes":1,
    "node_names":["Development-59678"]
  },
  "cache_health":[
    {"status":"HEALTHY","cache_name":"javaChildCache"},
    {"status":"HEALTHY","cache_name":"___protobuf_metadata"},
    {"status":"HEALTHY","cache_name":"protoBufChildUngroupCache"},
    {"status":"HEALTHY","cache_name":"javaParentCache"},
    {"status":"HEALTHY","cache_name":"protoBufParentUngroupCache"},
    {"status":"HEALTHY","cache_name":"protoBufParentCache"},
    {"status":"HEALTHY","cache_name":"protoBufChildCache"},
    {"status":"HEALTHY","cache_name":"___script_cache"}
  ]
}

キャッシュマネージャの状態

キャッシュマネージャの状態を取得するには以下の URL へアクセスします。

http://<ホスト名>:<ポート番号>/rest/v2/cache-managers/<キャッシュマネージャ名>/health/status

URL にはキャッシュマネージャ名を付与します。

キャッシュマネージャ名はデフォルトではdefaultになります。この場合、以下のURLになります。

http://<ホスト名>:<ポート番号>/rest/v2/cache-managers/default/health/status

出力される結果は以下の通りです。

HEALTHY

JDK Flight Recorder

Java 11以降もしくはJava 8の場合はupdate 272以降の Java を使っている場合は JDK Flight Recorder を使いましょう。

Javaの実行時VM引数に-XX:StartFlightRecording=name=DefaultRecording,settings=default,disk=true,filename=DefaultRecording.jfr,dumponexit=trueを追加してください。

シェルから起動している場合

シェルから起動している場合は、${INFINISPAN_HOME}/bin/server.shもしくは${INFINISPAN_HOME}/bin/server.bat の最後 java コマンドを実行しているところに追加します。

server.sh

   eval \"$JAVA\" $JAVA_OPTS \
      -Dvisualvm.display.name=$PROCESS_NAME \
      -XX:StartFlightRecording=name=DefaultRecording,settings=default,disk=true,filename=DefaultRecording.jfr,dumponexit=true \
      -Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager \

server.bat

"%JAVA%" %JAVA_OPTS%  -showversion ^
   -Dvisualvm.display.name=%PROCESS_NAME% ^
   -XX:StartFlightRecording=name=DefaultRecording,settings=default,disk=true,filename=DefaultRecording.jfr,dumponexit=true ^
   "-Dinfinispan.server.home.path=%ISPN_HOME%" ^
(略)

参考資料

日本語訳:JEP 425 : Virtual Threads (Preview) Part 1

個人的に気になっている Project Loom で Virtual Threads がプレビューされたので仕様である JEP を翻訳してみました。 ボリュームが多いため、いくつかのパートに分けて公開していきます。スターやブコメが付くとモチベーションが上がるかもしれません。(上がらないかもしれません)

原文はこちら:JEP 425: Virtual Threads (Preview)

全パートはこちら

JEP 425 : Virtual Threads (Preview)

  • 著者:Ron Pressler, Alan Bateman
  • オーナー:Alan Bateman
  • タイプ:機能
  • スコープ:SE
  • ステータス:統合済み
  • リリース:19
  • コンポーネント:core-libs
  • 議論:loom dash dev at openjdk dot java dot net
  • 労力:XL
  • レビュワ:Alex Buckley, Brian Goetz, Chris Hegarty
  • 作成日:2021/11/15 16:43
  • 更新日:2022/05/21 16:58
  • イシュー:8277131

要約

Java プラットフォームに仮想スレッドを導入します。仮想スレッドは、高スループットの並列アプリケーションを書き、維持し、観察する労力を劇的に削減する軽量なスレッドです。このAPIはプレビュー版です。

ゴール

  • シンプルなスレッド単位でリクエストするスタイルで書かれたサーバアプリケーションを、ほぼ最適なハードウェア使用率でスケーリングできるようにします。
  • java.lang.Thread API を使用する既存のコードに、最小限の変更で仮想スレッドを採用できるようにします。
  • 既存の JDK ツールを使用して、仮想スレッドのトラブルシューティング、デバッグ、およびプロファイリングを容易に行えるようにします。

非ゴール

  • 従来のスレッドの実装を削除したり、既存のアプリケーションを仮想スレッドを使用するようにユーザに明示せずに移行させることが目的ではありません。
  • Javaの基本的な並行処理モデルを変更することが目的ではありません。
  • Java言語やJavaライブラリに新しいデータ並列化構造を提供することも目的ではありません。Stream APIは、大規模なデータセットを並列に処理するための好ましい方法であることに変わりはありません。

動機

Java開発者は、30年近くにわたり、並行処理サーバーアプリケーションの構成要素としてスレッドに依存してきました。Javaはマルチスレッドなので、複数のスレッドが一度に実行され、各メソッドの全ての処理はスレッドの中で実行されます。スレッドは、Javaの並行処理の単位であり、他の単位と同時に(そしてほとんど独立して)実行される逐次実行されるコードの一部分です。各スレッドは、ローカル変数を格納し、メソッド呼び出しを調整するためのスタックと、物事がうまくいかないときのコンテキストを提供します。例外は、同じスレッドのメソッドによってスローおよびキャッチされるので、開発者はスレッドのスタックトレースを使用して、何が起こったかを見つけられます。スレッドは、ツールの中心的な概念でもあります。デバッガはスレッドのメソッド内のステートメントをステップ実行し、プロファイラは複数のスレッドの挙動を可視化し、そのパフォーマンスを理解するのに役立ちます。

リクエスト毎にスレッドを使う方式

サーバーアプリケーションは一般に、互いに独立したユーザーリクエストを同時に処理するため、アプリケーションがリクエストの全期間にわたってそのリクエストにスレッドを割り当てて処理することは理にかなっています。このスレッド毎のリクエスト方式は、アプリケーションの並行性の単位を表すためにプラットフォームの並行性の単位を使用するので、理解しやすく、プログラミングしやすく、デバッグやプロファイルも簡単です。

サーバーアプリケーションの拡張性は,待ち時間,同時実行性,スループット を関連付けるリトルの法則によって支配されています。あるリクエスト処理時間(すなわち待ち時間)において、アプリケーションが同時に処理するリクエストの数(すなわち同時実行性)は、到着率(すなわちスループット)に比例して増加しなければなりません。たとえば、平均待ち時間が50msのアプリケーションが、10リクエストを同時に処理することによって、1秒間に200リクエストのスループットを達成したとします。そのアプリケーションが 1 秒あたり 2000 リクエストのスループットに拡張するためには、100 リクエストを同時に処理する必要があります。各リクエストがリクエストの間、スレッドで処理される場合、アプリケーションが維持するために、スループットが増加するにつれてスレッドの数は増加する必要があります。

残念ながら、JDKはスレッドをオペレーティングシステム(OS)スレッドのラッパーとして実装しているため、利用可能なスレッドの数は限られています。OSのスレッドはコストが高いので、あまり多く持つことができず、リクエスト毎のスレッドというスタイルには不向きな実装になっています。各リクエストがスレッド、つまりOSのスレッドを消費する場合、CPUやネットワーク接続などの他のリソースが枯渇する前に、スレッドの数が制限要因になることが多いのです。JDKの現在のスレッド実装は、アプリケーションのスループットを、ハードウェアがサポートできるレベルよりもかなり低い水準に制限しています。これは、スレッドがプールされている場合でも発生します。プールすることで、新しいスレッドを開始する際の高いコストを回避できますが、スレッドの総数が増えるわけではありません。

非同期式でスケーラビリティを向上

ハードウェアを最大限に活用したい開発者の中には、リクエスト毎のスレッドをあきらめ、スレッドを共有するスタイルを採用する人もいます。1つのスレッドで最初から最後までリクエストを処理するのではなく、リクエスト処理コードは、I/O処理が完了するのを待ってスレッドをプールに戻し、そのスレッドが他のリクエストに対応できるようにします。このようにスレッドをきめ細かく共有することで、コードが計算を行うときだけスレッドを保持し、I/Oを待つときには保持しないことで、多くのスレッドを消費せずに多くの同時処理を行うことができるのです。OSのスレッドの不足によるスループットの制限をなくす一方で、高い代償を払うことになります。それは、いわゆる非同期プログラミングスタイルで、I/O操作の完了を待たず、後でコールバックに完了を知らせるI/Oメソッドを別に採用する必要があることです。専用のスレッドがない場合、開発者はリクエスト処理ロジックを小さな段階に分解し、通常はラムダ式で記述し、APIを使って順次パイプラインに合成しなければなりません(例えば、CompletableFutureや、いわゆる「リアクティブ」フレームワークなどを参照ください)。そのため、ループやtry/catchブロックといった、言語の基本的な逐次合成演算子を使いません。

非同期式では、リクエストの各ステージは異なるスレッドで実行され、各スレッドは異なるリクエストに属するステージを相互に切り離して実行することがあります。このことは、プログラムの動作を理解する上で深い意味を持ちます。スタックトレースは有用なコンテキストを提供せず、デバッガはリクエスト処理ロジックを段階的に処理できず、プロファイラは操作のコストをその呼び出し元と関連付けることができません。ラムダ式の記述は、JavaのストリームAPIを使って短いパイプラインでデータを処理する場合には何とかなりますが、アプリケーションのリクエスト処理コードをすべてこの方法で書かなければならない場合には問題があります。なぜなら、アプリケーションの並行性の単位である非同期パイプラインは、もはやプラットフォームの並行性の単位ではなくなっているからです。

仮想スレッドによるリクエスト毎のスレッド方式の維持

アプリケーションをプラットフォームと調和させながら拡張するためには、スレッドをより効率的に実装することで、リクエスト毎のスレッドというスタイルを維持し、より多くのスレッドを使えるようにする努力が必要です。言語やランタイムによってスレッドスタックの使い方が異なるため、OSのスレッドをより効率的に実装できません。しかし、Javaランタイムは、OSのスレッドとの一対一の対応を絶つ方法でJavaのスレッドを実装することができます。OSが限られた物理RAMに大きな仮想アドレス空間をマッピングすることでメモリが豊富にあるように見せかけるのと同じように、Javaランタイムも少数のOSスレッドに多数の仮想スレッドをマッピングすることでスレッドが豊富にあるように見せかけることができるのです。

仮想スレッドは、特定のOSスレッドに縛られないjava.lang.Threadのインスタンスです。これに対してプラットフォームスレッドは、OSのスレッドの薄いラッパーとして、伝統的な方法で実装されたjava.lang.Threadのインスタンスです。

リクエスト毎のスレッド方式のアプリケーションコードは、リクエストの全期間にわたって仮想スレッドで実行できますが、仮想スレッドは、CPUで計算を実行する間のみOSスレッドを消費します。その結果、非同期スタイルと同じスケーラビリティを、透過的に達成できます。仮想スレッドで実行されているコードがjava.* APIでブロッキングI/Oオペレーションを呼び出すと、ランタイムはノンブロッキングOSコールを実行し、後で再開できるまで仮想スレッドを自動的にサスペンドさせるのです。Java開発者にとっては、仮想スレッドは単にスレッドの作成が安上がりで、ほとんど無限にあるようなものです。ハードウェアの使用率は最適に近く、高いレベルの並行処理が可能になり、その結果高いスループットが得られる一方、アプリケーションはJavaプラットフォームとそのツールのマルチスレッド設計と調和したままです。

仮想スレッドの意味するところ

仮想スレッドは安価で豊富なため、決してプールしてはいけません。アプリケーション・タスクごとに新しい仮想スレッドを作成する必要があります。ほとんどの仮想スレッドは短命でコールスタックも浅く、1回のHTTPクライアントコールや1回のJDBCクエリを実行する程度です。対照的に、プラットフォームスレッドは重量級で高価であるため、しばしばプールする必要があります。スレッドは長寿命で、コールスタックも深く、多くのタスクで共有される傾向があります。

要約すると、仮想スレッドは、ハードウェアを最適に利用しながら、Java プラットフォームの設計と調和した信頼性の高いリクエスト毎のスレッド方式を維持します。仮想スレッドを使うのに新しい概念を学ぶ必要はありませんが、今日の高いスレッド費用に対処するために開発された習慣を学ぶ必要はあるかもしれません。仮想スレッドは、アプリケーション開発者を助けるだけでなく、フレームワーク設計者がスケーラビリティに妥協することなくプラットフォームの設計に適合した使いやすいAPIを提供するのにも役立ちます。

続きはこちら:日本語訳:JEP 425 : Virtual Threads (Preview) Part 2 - #chiroito ’s blog

JFRのイベントをCSVに出力する

JDK Flight RecorderファイルをJDK Mission Controlで分析して異常を見つけたら、レポートを作成するためにExcelに取り込みたいなぁと思うことがあります。その場合はjfrコマンドとjqコマンドを使って任意のイベントをCSVに変換しましょう。

イベントを探す

CSVファイルを作る前に、イベントが持つ要素名を取得しないといけません。そのためにはまず、そのイベントの名前を探して、次にそのイベントが持つ要素名を取得します。

ここで使用するのはjfrコマンドのサブコマンドであるmetadataです。これらを以下のように使います。

jfr metadata <JFR file>  | grep @Name
jfr metadata --events <Event Name> <JFR file>

まず最初に、イベントの名前を探しましょう。イベントの名前はメタデータの@Nameアノテーションに記載されています。そのため、JFRファイルが持っているイベントの@Nameアノテーションを全て表示しましょう。たくさん出力されるので、grepを追加して引っかかりそうなキーワードでフィルタすることをお奨めします。

> jfr metadata <JFR file>  | grep @Name
@Name("jdk.TenuringDistribution")
@Name("jdk.ThreadAllocationStatistics")
@Name("jdk.ThreadCPULoad")
@Name("jdk.ThreadContextSwitchRate")
@Name("jdk.ThreadDump")
@Name("jdk.ThreadEnd")
@Name("jdk.ThreadPark")
@Name("jdk.ThreadSleep")
@Name("jdk.ThreadStart")
@Name("jdk.UnsignedIntFlag")
@Name("jdk.UnsignedIntFlagChanged")
@Name("jdk.UnsignedLongFlag")
@Name("jdk.UnsignedLongFlagChanged")
@Name("jdk.YoungGarbageCollection")

次に、そのイベントが持つメタデータを取得しましょう。metadataサブコマンドではJFRが持つ全てのイベントのメタデータを表示します。イベントの数は膨大にあるのでその出力結果からメタデータを探すのは大変です。そのため、先ほど取得したイベントの名前でフィルタを掛けて実行しましょう。今回はjdk.CPULoadというCPUの負荷情報のメタデータを取得します。

> jfr metadata --events jdk.CPULoad <JFR file>
@Name("jdk.CPULoad")
@Category({"Operating System", "Processor"})
@Label("CPU Load")
@Description("OS CPU Load")
class CPULoad extends jdk.jfr.Event {
  @Label("Start Time")
  @Timestamp("TICKS")
  long startTime;

  @Percentage
  @Label("JVM User")
  float jvmUser;

  @Percentage
  @Label("JVM System")
  float jvmSystem;

  @Percentage
  @Label("Machine Total")
  float machineTotal;
}

イベントをCSVへ出力

これでようやく本題であるイベントをCSVへ出力できます。以下のようなコマンドを使用してCSVへ出力します。

jfr print --json --events <イベント名> <JFR file>  | jq -r ".recording.events[].values | [<出力したい要素を羅列>] | @csv"

出力したい要素は先ほど取得したイベントのメタデータからフィールド名に該当する部分の先頭に.を付けて、各要素間はカンマ区切りで羅列します。CPULoadのJVMのユーザ領域とシステム領域を出力する場合は.jvmUser, .jvmSystemになります。

以下の例は、イベントの発生時間、JVMのユーザ領域の使用量、JVMのシステム領域の使用量、マシン全体の使用量を出力する例です。

jfr print --json --events jdk.CPULoad <JFR file>  | jq -r ".recording.events[].values | [.startTime, .jvmUser, .jvmSystem, .machineTotal] | @csv"
"2022-03-23T1:23:45.123456789+09:00",0.01234567890,0.01234567890,0.01234567
"2022-03-23T1:23:46.123456789+09:00",0.01234567890,0.01234567890,0.01234567
"2022-03-23T1:23:47.123456789+09:00",0.01234567890,0.01234567890,0.01234567

この結果をファイルにリダイレクトすることでCSVファイルが作成されます。

おまけ

JFRのイベントには期間(Duration)を持つイベントが多くあります。そのようなイベントの1つであるソケット読み込みの例も記載しておきます。メタデータの詳細が気になる場合は先述の手順で取得してください。

jfr print --json --events jdk.SocketRead <JFR file>  | jq -r ".recording.events[].values | [.startTime, .duration, .host, .address] | @csv"
"2022-03-23T1:23:45.123456789+09:00","PT0.012345678S","service-a","172.30.1.1"
"2022-03-23T1:23:45.123456789+09:00","PT0.012345678S","service-b","172.30.1.2"
"2022-03-23T1:23:45.123456789+09:00","PT0.012345678S","service-a","172.30.1.2"