連載「リアルタイム Java での開発」の 3 回目で最終回となるこの記事では、基本的なリアルタイム・アプリケーションを設計、作成、検証、分析する方法を説明します。内容は以下のとおりです。
- アプリケーションの時間的な要件とパフォーマンス要件
- 従来の非リアルタイム Java がアプリケーションに適さない理由
- 適用するリアルタイム Java プログラミング手法の選択
- 確定性を高めるために考慮しなければならない事項
- アプリケーションの確定性に対するテストと検証
- リアルタイムの確定性に関する問題をデバッグするためのツールと手法
- 予測可能性を向上させる方法
これらの話題に集中できるように、これまでアプリケーションの設計とコードはできるだけ複雑にならないようにしてきました。この記事で使われているソース・コードの完全なものはダウンロードすることができます。
私たちに課せられている任務は、産業用機器から短い待ち時間で温度測定値を読み取る Java アプリケーションを、Linux® オペレーティング・システム上で動作するアプリケーションとして作成することです。Java 言語の生産性と移植性を活かすため、コードはなるべく標準の非リアルタイム Java に近いものにしておかなければなりません。このアプリケーションでは、機器の温度測定値を読み取って使用側プログラムに送信し、そのプログラムが機器に対するフィードと冷却速度を制御することで、機器の効率性を維持します。要件となるのは、使用側プログラムが温度測定値を 5 ミリ秒以内に取得できるようにすることです。わずかな遅延でさえも、フィードや冷却速度が最適ではなくなり、機器の効率性を損なわせることになります。
使用側プログラムに一定間隔で、しかも極めてわずかな待ち時間でデータを送信するというパラダイムは、リアルタイム・システムでは一般的です。例えば、株価やレーダー信号などのデータがこのパラダイムに該当することは簡単に想像がつきます。第 2 回でその理由を列挙したように、このようなアプリケーションを従来の Java コードで作成して、しかもリアルタイムの時間的要件を満たすには無理があります。表 1 に、これらの理由を要約し、それぞれに対するリアルタイム Java でのソリューションを記載します (ただし、すべての実装がこの表と同じソリューションを提供するわけではありません)。
表 1. 従来の Java とリアルタイム Java との比較
| 従来の Java での問題 | リアルタイム Java でのソリューション |
|---|---|
| クラス・ロードの遅延 | 必要なクラスをプリロードすることによって遅延を回避します。 |
| JIT (Just-In-Time) コンパイルの遅延 | AOT (Ahead-Of-Time) コンパイルまたは非同期 JIT コンパイルによって遅延を回避します。 |
| オペレーティング・システム・レベルでのスレッド優先度に対するサポートが極めて制限されていること | RTSJ (Real Time Specification for Java) では、28 以上の優先度レベルを使用できます。 |
| デフォルト以外のスレッド化ポリシーの制御がサポートされていないこと | RTSJ はプログラマーがオプションを選択できます (例えば、ファーストイン・ファーストアウト (FIFO) など)。 |
| ガーベッジ・コレクション (GC) による大幅な遅延 | RTSJ は NHRT ( GC による、アプリケーション・スレッドの遅延を短縮するために、現在はリアルタイム・ガーベッジ・コレクターを使用できるようになっています。 |
| オペレーティング・システムのカーネル・スレッドと優先順位の逆転による遅延 | リアルタイム Linux ディストリビューションに提供されているようなリアルタイム・カーネルは、長時間実行されるカーネル・スレッドを使用しないこと、完全にプリエンプト可能であること、そして優先順位の逆転を回避することを目的に設計されています (第 1 回で説明)。 |
適用するリアルタイム Java プログラミング・モデルの選択
デモ・アプリケーションのコードをできるだけ標準の非リアルタイム Java に近づけておかなければならないという要件があることから、適用する RTSJ プログラミング・モデルの選択肢のうち、1 つはすぐに除外されます。それは、GC による遅延を回避するために NHRT およびメモリー・スコープを使用するという選択肢です。
RealtimeThread を使用することで、NHRT プログラミングの複雑さに対処することなく、リアルタイム Java プログラミングの拡張機能にアクセスすることができます。NHRT を使用するとしたら、プログラマーは通常の自動 GC に依存せずに、自分でメモリーを制御しなければならなくなります。実際のところ、永続メモリーには限りがあり、結局は枯渇してしまうため、NHRT では永続メモリーではなくメモリー・スコープを使用しなければなりません。また、どのクラスが「NHRT セーフ」であるかについても制約があります。つまり、メイン・ヒープでのオブジェクトを使用したり、参照したりすることのないクラスのことです。NHRT ではヒープの使用も、ヒープへの参照も認めていません。
プログラミングが単純化されるだけでなく、さらに以下についても期待することができます。
- パフォーマンスの改善。ヒープ・オブジェクトは、永続オブジェクトやスコープ・オブジェクトよりも短時間で作成および参照することができます。非ヒープ・オブジェクトには、メモリー・バリアやその他のオーバーヘッドが必要だからです。
- デバッグの容易化。NHRT がヒープ・メモリーを参照しないようにするためには、メモリー・アクセス例外をデバッグしなければなりません。また、永続ヒープまたはメモリー・スコープのいずれかから NHRT に対するメモリー不足のエラーがトリガーされる可能性もあります。
Linux でのリアルタイム・プログラムには、2 つの IBM® JVM Java 6 パッケージを使用することができます。この 2 つは、名前は似ているものの機能は異なり、パフォーマンスと確定性のトレード・オフにも違いがあります (「参考文献」を参照)。この記事のデモ・プログラムを実行するために必要なのは、IBM WebSphere® Real Time for RT Linux です。このパッケージは、RealtimeThread や、温度の読み取りスレッドを実装するために必要な周期タイマー・クラスなどの RTSJ 機能を提供するとともに、GC による中断を最小限に抑えます。また、ハード・リアルタイム・アプリケーションに必要な確定性をもたらすために、リアルタイム Linux カーネルと特定のハードウェア上で動作します (もう一方のパッケージ (IBM WebSphere Real Time for Linux) はソフト・リアルタイム・アプリケーションをサポートします。こちらのパッケージのほうがスループットとスケーラビリティーの点では勝っていますが、RTSJ プログラミング・ライブラリーは組み込まれていません。組み込まれているのは、GC による中断を約 3 ミリ秒まで抑えることを目的とした Metronome ガーベッジ・コレクターです)。
NHRT ではなく RealtimeThread を使用する場合の主なマイナス面は、リアルタイム・ガーベッジ・コレクターによる (短時間ではあるものの) 中断によってスレッドが影響されることです。しかし WebSphere Real Time の Metronome ガーベッジ・コレクターの場合、GC による中断が原因でアプリケーション・コードの実行が停止している時間は 500 マイクロ秒ほどでしかありません。また GC サイクルの間、アプリケーションが 70 パーセント以上の時間、実行されることも保証されます。つまり 10 ミリ秒の時間枠では、約 500 マイクロ秒の GC クォンタによってアプリケーションの実行が中断されるのは最大 6 回ということになります。このモデルについては、後で説明します。
このアプリケーションの要件は、温度データを 5 ミリ秒以上の間隔を空けずに提供することです。設計では、GC サイクルが実行されたとしてもこの要件を満たせるように、温度を 2 ミリ秒ごとに読み取るようにします。
リアルタイム・サービス品質の概念は、確定性と純粋なパフォーマンス要件との対比を浮き彫りにします。リアルタイム・アプリケーションの場合、一般に興味の対象となるのはスループット率ではなく、システムの振る舞いの正確さ、そして適時性です。実のところ、リアルタイム Java 実装はそれに匹敵する標準 Java 実装よりも処理速度に劣ることがあります。これは RTSJ をサポートするためのオーバーヘッド (メモリー・バリアなど) が必要になること、そして非確定的な振る舞いをもたらすランタイム最適化を無効にしなければならないことがあるためです。このように、定義しているのがハード・リアルタイム・システムなのか、あるいはソフト・リアルタイム・システムなのかによって要件は異なってきます。
ハード・リアルタイム・システムであるならば、システムは 5 ミリ秒以内にすべての温度データを使用側スレッドが使えるようにしなければなりません。5 ミリ秒を少しでも超える遅延は、システム障害の要因となります。一方、ソフト・リアルタイム・システムの場合は、5 ミリ秒以内に完全にすべての温度データを使用側スレッドが使えるようにしなければならないわけではありません。例えば 5 ミリ秒以内に 99.9 パーセントの温度測定値が使えるようになり、200 ミリ秒以内に 99.999 パーセントが送信されるシステムであれば許容されます。
リアルタイム・アプリケーションでは、単純な機能テストまたはパフォーマンス・テストを行うだけでは要件を満たしていることを証明できません。何回かのテストでは正常に機能するシステムでも、時間が経つうちに劣化してきたり、その出力が変わってきたりすることがあります。実行時間または周期スケジューリングに有効な値の範囲は、長時間の実行で大量のサンプルをとるまでは明らかになりません。
例えば、前述のソフト・リアルタイム要件から考えると、少なくとも 100,000 回テストするまでは、200 ミリ秒以内に実行の 99.999 パーセントを完了するという目標に達していることを示すことはできません。ハード・リアルタイム要件の場合、システムの開発者はその責任として、システムが要件 (すべての温度データを 5 ミリ秒以内に使用側スレッドが使用できるようにすること) を満たしていると認められるだけの十分なテストを実行しなければなりません。そのための方法としてよく使用されるのは、長期間にわたるテストと併せ、パフォーマンス・データの分布について統計学的に検討することです。データ・サンプルに最も一致する分布曲線を作成するためのツールがあるので、モデル化したデータを使用して、極度な外れ値の数とその可能性を予測することはできます。一般に開発者は、システムのテストにはシステムをデプロイするときほどの時間を割けません。そのため、観測された最悪の外れ値に対処することが、要件を満たすシステムにする実際的な方法となります。
簡単のため、ここでは温度データを提供するスレッドによって使用されるコードだけを検討します。データの使用側スレッドについては詳しく説明しません。これらのスレッドは、同じコンピューター上の別のプロセスで実行されるものとして考えてください。
初期実装では、WebSphere Real Time AOT コンパイル・ツールの admincache を使用する以外には、最大限の確定性を得るための手段は講じません。ここでは、確定性のないその他の分野を識別するための手法とツール、そしてその問題を解決する方法に重点を置きます。
機器の温度センサー (ここでは乱数発生器によってシミュレートされています) を 2 ミリ秒ごとにポーリングするために使用するのは、Reader プロセスです。周期的にアクションを実行するための RTSJ のソリューションは javax.realtime.PeriodicTimer クラスなので、Reader では PeriodicTimer を使って温度センサーをポーリングします。このプロセスでは待ち時間を最小限にするために BoundAsyncEventHandler として実装します。Reader は読み取り操作を行うごとに XML のスニペットを作成し、この XML スニペットを、Writer プロセスに接続されたネットワーク・ソケットに書き込みます。センサーを読み取ってデータを書き込むサイクルに費やされた時間が 2 ミリ秒を超えると、エラーがレポートされます。
Writer プロセスは、Reader プロセスと同じコンピューター上にある別の JVM で実行され、ネットワーク・ソケットで温度測定値をリッスンします。Writer は javax.realtime.RealtimeThread を使ってネットワーク・ソケットをリッスンすることによって、FIFO スケジューリング・モデルときめ細かな優先度制御を利用します。XML スニペットをアンパックし、温度測定値を抽出すると、Writer はその値をログ・ファイルに書き込みます。使用側に対してデータを迅速に使用可能にするために Writer にも制限が設けられており、Writer は 3 ミリ秒という時間期限のなかでディスクに測定値を書き込まなければなりません。つまり、温度測定値が取得されてからデータが書き込まれるまでの合計時間が 5 ミリ秒を超えた場合にも、エラーがレポートされるということです。
図 1 に、このアプリケーションのデータ・フローを示します。
図 1. データ・フロー
注意する点として、時間制約が厳しい状況で、このようなわずかな量のデータに XML を使用するのは好ましい設計プラクティスではありません。このデモ・アプリケーションは、WebSphere Real Time のパフォーマンスについて調べる上で有用な例として作成されているだけなので、リモートから温度をモニタリングするアプリケーションの手本であるとは考えないでください。
WebSphere Real Time 環境が用意されていれば、以下の手順に従って、このデモを実行することができます。
- デモのソースを、お使いの WebSphere Real Time マシンの任意の場所に解凍します。
- 以下のコマンドを実行して、デモをコンパイルします。
PATH to WRT/bin/javac -Xrealtime *.java
- ポート番号とログ・ファイル名を渡して Writer プロセスを開始します。以下は一例です。
sdk/jre/bin/java -Xrealtime Writer 8080 readings.txt
- Writer プロセスのホスト名とポート番号を渡して Reader プロセスを開始します。以下は一例です。
PATH to WRT/jre/bin/java -Xrealtime Reader localhost 8080
このコードは、Reader スレッドと Writer スレッドそれぞれでの遅延をレポートします。Writer は温度測定値を読み取ってからディスクに書き込むまでにかかった合計の転送時間もレポートします。ハード・リアルタイムの目標を達成するには、この転送時間が常に 5 ミリ秒以内に収まっていることが必須です。ソフト・リアルタイムの場合、5 ミリ秒以内に 99.9 パーセントが転送されれば、要件の最初の部分を満たします。そして 200 ミリ秒以内に 99.999 パーセントが転送されれば、要件の 2 番目の部分を満たします。
このアプリケーションを実行した IBM テスト・システムでは、ソフト・リアルタイムの目標は達成できましたが、ハード・リアルタイムの目標については達成することができませんでした。次のセクションでは、これらの失敗を調査するために使用できるツールと手法について説明します。
リアルタイム・プログラミングでは多くの場合、通常の Java 分析に使用される時間制約よりも厳しい制約が適用されます。通常の Java プログラミングではミリ秒の 10 倍以上を扱いますが、それとは対照的に、リアルタイム Java ではナノ秒からミリ秒の範囲を扱うのが一般です。デモ・アプリケーションが失敗した原因を突き止めるには、マイクロ秒および数ミリ秒の範囲で調査をしなければなりません。
私たちは、デモが最もよく失敗するのは繰り返し実行されるデモの中でも早い段階で実行されたデモであることに気付きました。これはリアルタイム・システムでも、その他のシステムでもよく見られる現象です。つまりプログラムが起動してからすぐの何回かの実行では、完了するまでの時間が長くなる傾向があります。その理由のいくつかはすでに触れたとおり、クラス・ロードと JIT コンパイルです。
最も単純なレベルの作業としては、-verbose:class コマンドライン・オプションを指定してアプリケーションを実行し、すべてのクラス・ロード・イベントに関する情報を出力するという方法があります。しかし、外れ値をその他のアクティビティー (クラス・ロードなど) と相関させるには、異常なイベントと、原因として疑われるアクティビティーの両方で正確なタイムスタンプが必要となります。一般的な目的のために、JVMTI (Java Virtual Machine Tool Interface) クラス・ロード・イベント (「参考文献」を参照) を使用して自作のツールを用意するという方法もありますが、それでもアプリケーション・コードをインスツルメント化し、タイムスタンプが付いたイベントを相関させなければならないことには変わりありません。
最近の JIT コンパイラーは、パフォーマンスに大きなメリットをもたらします。これらの JIT コンパイラーは技術的に極めて複雑なソフトウェアであり、ほとんどの開発者はブラックボックスとみなしています。確かに、構成オプションによる相互作用はそれほど明示的ではなく、JIT アクティビティーにも例えばガーベッジ・コレクターほどの可視性がありません。しかし、コマンドラインのオプションを使うことによって、JIT アクティビティーをより詳細に出力することは可能です。さらに、JIT 関連の JVMTI イベントは、ツールによって JIT コードの生成を追跡するために使用することができます。
JVM にメソッドの完了が通知されるようにするには (そして再コンパイルの最適化レベルを上げるには)、コマンドラインから -Xjit:compiling フラグを指定して JVM を起動します。
例えば、-verbose:class と -Xjit:compiling の両方を有効にしてある一方、すべてのクラス・ロードが完了してだいぶ経ってからでも、あるいは JIT によって生成されたコードが安定した後でも、デモ・アプリケーションが時間的要件を満たしていないことがわかったとします。この場合、アプリケーションの実行内容を他の JVM アクティビティーとの関連で詳しく調べる必要があります。
そのための 1 つの方法は、タイムスタンプを使ってコードをインスツルメント化し、主要な遅延が発生している場所を突き止めることです。リアルタイム Java プラットフォームの利点は、極めて精度の高いハイパフォーマンス・クロックを利用できることです。Reader コードのなかで疑われる部分には、以下のようなコードを追加することができます。
AbsoluteTime startTime1 = clock.getTime();
xmlSnippet.append("<reading><sensor id=\");
AbsoluteTime startTime2 = clock.getTime();
RelativeTime timeTaken = startTime2.subtract(startTime1);
System.err.println("Time taken: " + timeTaken);
|
これによって、コードで実行に時間がかかっている行を突き止めることはできますが、この方法は何度も繰り返されなければならない面倒なプロセスであり、生成されるデータは他のイベントとの相関がありません。画面上に出力されるイベントの順番を頼りにしているとしたら、例えば -verbose:gc や -verbose:classloading などを指定することによってある程度の成果は期待できるものの、それよりも遙かに優れたソリューションがあります。それは、Tuning Fork トレースです。
Tuning Fork Visualization Platform (「参考文献」を参照) は当初、Metronome ガーベッジ・コレクターの開発とデバッグを支援するために開発されました (これは拡張可能な Eclipse プラグインでもあり、例えば IBM Toolkit for Data Collection and Visual Analysis for Multi-Core Systems をはじめとする広範なアプリケーションで使用されています。「参考文献」を参照してください)。
Tuning Fork を使用する利点としては、以下のことが挙げられます。
- 強力な可視化および分析機能
- GC、クラス・ロード、JIT コンパイル、およびその他のコンポーネントからの JVM アクティビティー・データを使用できること
- アプリケーションのトレース・ポイントと JVM トレース・データを組み合わせられること
付属のソース・コードには、Tuning Fork アプリケーションのトレース・ポイントを追加するために必要な変更内容にマークが付いています (Reader.java.instrumented と Writer.java.instrumented の部分。「ダウンロード」を参照してください)。
コードは、実行時間に関するデータを標準エラー出力ストリームに書き込む前述の例よりも多少複雑になりますが、この追加作業に多大なメリットがあることは、これからわかります。Tuning Fork トレース・ポイントには、2 つの種類があります。1 つは単純なタイムスタンプ・イベントを記録するトレース・ポイント、もう 1 つはプログラマーに代わってデータをログに記録するトレース・ポイントです。どちらのイベントにしても、クラス・ロード、JIT、および GC アクティビティーを対象とした内部 JVM トレース・ポイントと同じコンポーネントを使用して記録されます。重要な点は、これによってアプリケーションおよび JVM トレース・データの両方が同じタイムスタンプ・エンジンを使用することが確実になることです。つまり、タイムスタンプを使用してすべてのイベントを相関させることが問題なくできるようになり、任意の時点で何が実行されているのかを知ることができるというわけです。さまざまなソースからのトレース・データを混在させて使おうとすると、通常は困難な作業になり、エラーと格闘することになりますが、ここで必要な Tuning Fork トレース・イベントはタイムスタンプ・イベントだけなので、これらのイベントをアプリケーション・コードの関心のある部分の最初と最後に追加します。
ソースでは、追加分のコードが以下のように区切り文字でマークされています。
/*---------------------TF INSTRUMENTATION START ------------------------- */ writerTimer.start(); /* ---------------------TF INSTRUMENTATION END ------------------------- */ |
インスツルメント化されたソースは、Reader JVM (Reader.trace) と Writer JVM (Writer.trace) のそれぞれについて 1 つのトレース・ファイルを生成します。これらのバイナリー・ファイルには、Tuning Fork ビジュアライザーでの分析に備え、すべての温度読み取りメッセージの処理開始イベントおよび終了イベントが記録されています。
Tuning Fork でインスツルメント化したバージョンに追加されているコードは、以下のあたりにあります。
- Tuning Fork トレース生成ファイル (tuningForkTraceGeneration.jar) 内のメソッドを対象とした import 文
- ロガーがタイマーおよびフィードレット (タイマーとロガーとの間のデータ・フィード) の書き込み、作成を行うための初期化コード
- インスツルメント化されたコードの初期化の実行
- フィードレットをカレント・スレッドにバインドするためのメソッド
他に必要なコードは、以下に示す時間を計測するためのコードだけです。
/*---------------------TF INSTRUMENTATION START ------------------------- */
writerTimer.start();
/* ---------------------TF INSTRUMENTATION END ------------------------- */
AbsoluteTime startTime = clock.getTime();
Code to be timed
RelativeTime timeTaken = stopTime.subtract(startTime);
/* ---------------------TF INSTRUMENTATION START ------------------------- */
writerTimer.stop();
/* ---------------------TF INSTRUMENTATION END ------------------------- */
|
この記事の例では、Tuning Fork の時間計測コードは、標準デモ・プログラムの既存の時間計測コードをラップしているため、両方のコードから取得した時間はぴったり一致します。
AbsoluteTime startTime = clock.getTime();
Code to be timed
RelativeTime timeTaken = stopTime.subtract(startTime);
|
Tuning Fork トレース・ポイントが追加されたコードを作成し、実行するには、tuningForkTraceGeneration.jar をクラス・パスに追加すればよいだけです。
内部 JVM データのログを有効にするため、コマンドラインに -XXgc:perfTraceLog=filename.trace フラグを追加します。
可視化ツールである Tuning Fork は、Windows® または Linux 上で実行できる Eclipse プラグインです。IBM WebSphere Real Time JVM 用に事前に作成された図を有効にするには、さらに別のプラグインを追加する必要があります (Tuning Fork のインフラストラクチャーは汎用的なので、Java、C、C++ などの他のアプリケーションにも使用することができます)。
プログラムの実行を理解する上で最も役立つ Tuning Fork のビューは、時系列でイベントが示されるという単純な図です。WebSphere Real Time には、事前に定義された図がいくつも用意されています (「参考文献」を参照)。これらの図は、JVM データを有効に組み合わせたビュー (例えば、GC パフォーマンス・サマリーなど) を提供します。
デモ・アプリケーションには、単純な Tuning Fork 計時機能を追加しました。このコードは単に、タイマーを定義し、既存の時間計測コードの直前直後でタイマーを開始、停止して、スレッドの実行時間が Reader の場合は 2 ミリ秒、Writer の場合は 3 ミリ秒を超えていないかどうかをチェックするだけのものです。図 2 に、このデータのほんの一部を示す Tuning Fork ビューを記載します。このビューから、コードが意図通りに実行されていることを確認することができます。
図 2. Tuning Fork トレース - デモ・アプリケーションのコード
図 2 には、Reader の実行時間が約 130 マイクロ秒であること、ソケットで送信されたデータによってトリガーされた Writer スレッドの実行時間は約 900 マイクロ秒であることが示されています (Writer スレッドの処理量のほうが多いため、この結果は想定通りです)。温度の値を読み取ってからファイルに書き込むまでのデータ転送全体にかかった時間は、1 ミリ秒をわずかに超えている程度です。つまり、5 ミリ秒の制限には十分に収まっています。またこの図からは、Reader スレッドが 2 ミリ秒の間隔で周期的に起動されていることもわかります。
Tuning Fork ビジュアライザーは、2 つのデータ・ソースからのタイムスタンプを自動的に整列させるため、時間の X 軸は両方のスレッドに適用されます。
GC サイクルの間には、このパターンにどのような変化が現れるのでしょうか。Tuning Fork がなければ、アプリケーションの観点からしかわかりませんが、このビジュアライザーによって、GC による中断がアプリケーションに及ぼす影響が遙かに明確に見えてきます。図 3 に、Writer JVM での GC アクティビティーのビューを示します。
図 3. Tuning Fork トレース — GC スライス
上記から、GC が Writer スレッドの実行時間に影響していることがわかります。Metronome がインクリメンタル方式で行う作業 (クォンタ、または Tuning Fork ではスライス) は、それぞれ約 500 マイクロ秒間実行されます。各クォンタの実行中、Writer JVM 内のすべてのアプリケーション・スレッドは中断されるため、実行時間は (通常) 900 マイクロ秒から (クォンタが Writer スレッドの実行中に発生した場合) 1.4 ミリ秒にまで増加しています。
Writer スレッドの実行時間に対する影響は、合計でクォンタによる 500 マイクロ秒をやや上回るにすぎませんが、コンテキスト・スイッチのオーバーヘッドと潜在的なプロセッサーのキャッシュ汚染の影響が出てくることは確実です。Writer スレッドが、GC クォンタ以前に実行していたコアとは別のコアにディスパッチされたとしたら、コア固有のキャッシュにより多くの負担がかかることになります。
個別の JVM で実行されていた Reader スレッドが Writer ランタイムでのGCアクティビティーに影響されなかった理由は、このコンピューターでは 4 つの CPU コアで Writer JVM の GC スレッドと Reader を同時に実行していたためです (WebSphere Real Time はデフォルトで、JVM ごとに 1 つの GC スレッドを使用します)。
Writer JVM でのその他の GC サイクルを調べたところ、2 ミリ秒を超える外れ値がいくつかあることがわかりました。図 4 をよく見てみると、これらの外れ値に該当する実行は不運にも 2 つの GC クォンタによって割り込まれていたことが明らかです。
図 4. Tuning Fork トレース — 2 つの GC スライス
このような二重の打撃はまれなものの、この事態を完全に回避するにはどうすればよいでしょうか。
2 つのクォンタによる割り込みを回避するには、アプリケーション・コードの実行時間と 1 回のクォンタを合わせた時間が、1 つの GC クォンタが終了してから次のクォンタが発生するまでの期間 (通常、約 1 ミリ秒) に収まるようにしなければなりません。したがって、Writer は約 900 マイクロ秒かかっている実行時間を 500 マイクロ秒未満に抑える必要があります。しかし、このストラテジーでさえも、GC クォンタとの競合を必ずしも回避できると保証されるわけではありません。その理由には以下の 3 つが挙げられます。
- GC による中断がスケジューリングされるタイミングには、わずかな変動があります。これは、70 パーセントの mutator (アプリケーション・スレッド) の使用率を維持するという契約を扱う方法によるものです。
- それぞれのプロセッサー・コアには一般に、例えば割り込みやタイマーなどを処理する高い優先度のカーネル・スレッドがバインドされています。これらのカーネル・スレッドは非常に短い間隔で実行されますが、アプリケーションまたは JVM スレッドよりも高い優先度を持つことがあるため、スレッドが実行されるタイミングを変動させる可能性があります。
- JVM には、ユーザーやその他の GC スレッドよりも高い優先度を持つスレッドが 1 つあります。それは、GC タイム・スライスを管理するために 450 マイクロ秒ごとに数マイクロ秒間実行される GC アラーム・スレッドです。オペレーティング・システムのスケジューラーがこのスレッドをアプリケーションまたは GC スレッドと同じコアにディスパッチした場合には、多少の遅延が発生します。
アプリケーションの実行をマイクロ秒レベルで調べていくと、スレッド間、コア間、そしてスケジューリング間の (時には珍しい) 相互作用が明らかになってきます。場合によっては、オペレーティング・システムからデータを取り込んで、これらの相互作用を十分に理解しなければならないこともあります。Tuning Fork では Linux システムの Tap ツールからデータをインポートすることも可能ですが、それについてはこの記事では取り上げません。このデータは、IBM Toolkit for Data Collection and Visual Analysis for Multi-Core Systems でも可視化することができます (「参考文献」を参照)。
デモ・アプリケーションを実行したとすると、Reader JVM の起動直後に、Writer コンソールからメッセージが相次いで表示されたはずです。まずは、以下のメッセージを見てください。
Writer deadline missed after 0 good writes. Deadline was (3 ms, 0 ns), time taken was (48 ms, 858000 ns) |
このメッセージは、Writer は最初の実行で、レコードを書き込むのに 49 ミリ秒近くかかったことをレポートしています。しばらくするとこの書き込み時間が 1 ミリ秒未満になることを考えると、これはかなりの長さです。この遅延の原因が、メソッドのバイト・コードをネイティブ・コードに変換する JIT アクティビティーに関係していないことは確かです。実行には AOT でコンパイルしたコードを使用し、JIT は実行時に無効にされるからです。他の原因としては、この問題は最初の呼び出しで発生することから、クラス・ロードが関係していると考えられます。果たして Tuning Fork では、このようなことも確認できるのでしょうか。図 5 に、Tuning Fork によってグラフ化された Writer の最初の実行を示します。
図 5. Tuning Fork トレース – Writer の最初の実行とクラス・ロード
思った通り、Writer の最初の実行が開始される直前から実行されている間、かなりのクラス・ロード・アクティビティーが行われています。このことから、クラス・ロード (およびクラス初期化の実行) が遅延の原因であることは明らかです。Writer の以降の実行は、1 ミリ秒以内に完了しています。
この連載の第 2 回で、このような遅延を防ぐための手法をいくつか説明しましたが、プリロードすべきクラスを特定する上で有効な Tuning Fork の利用方法のひとつが、Tuning Fork のビューで時間の倍率を高くしてみる方法であることがわかります。例えば図 6 には、org/apache/xerces/util/XMLChar のロードにかかる時間が 3 ミリ秒を超えていることが示されています。
図 6. Tuning Fork トレース – 時間がかかっているクラス・ロードの特定
このデモ・アプリケーションは極めて単純なものですが、XML 処理を使用する場合には、これより遙かに多くのクラスをロードしなければなりません。そのため、クラスをプリロードするか、あるいはアプリケーションが時間制約の厳しいフェーズに入る前に、ダミーで最初に実行しておくことが重要です。
これまで調べてきたのは Writer JVM 内の外れ値だけでしたが、アプリケーションの要件は、処理全体が 5 ミリ秒以内で完了することです。Reader JVM からは時間制限の超過はレポートされていません。また、その最初に繰り返される実行も、約 140 マイクロ秒に安定する以前から 1 ミリ秒未満で完了しています。Tuning Fork インスツルメント機能を追加すると、図 7 に示す統計も表示できるようになります (3 ミリ秒の外れ値は、JVM が Ctrl-C で終了されて、例外処理に関連付けられたクラスの遅延ロードが行われたことが原因で発生したものです。このようにまれにしか実行されないパスの問題と、必要なクラスを特定してプリロードする手法は第 2 回で説明しました。この場合、単純なウォームアップだけでは足りません)。
図 7. Tuning Fork トレース – Reader の統計
問題は、Reader JVM の起動時に、Reader JVM で温度計が読み取られた時点から Writer JVM でその値が書き込まれるまでのデータ転送の大多数を期限内に完了できなかったことを Writer JVM がレポートしている点です。
Data deadline missed after 0 good transfers. Deadline was (5 ms, 0 ns), transit time was (122 ms, 93000 ns) Writer deadline missed after 0 good writes. Deadline was (3 ms, 0 ns), time taken was (48 ms, 858000 ns) Data deadline missed after 0 good transfers. Deadline was (5 ms, 0 ns), transit time was (122 ms, 517000 ns) Data deadline missed after 0 good transfers. Deadline was (5 ms, 0 ns), transit time was (121 ms, 567000 ns) Data deadline missed after 0 good transfers. Deadline was (5 ms, 0 ns), transit time was (120 ms, 541000 ns) Data deadline missed after 0 good transfers. Deadline was (5 ms, 0 ns), transit time was (119 ms, 525000 ns) |
このパターンは続きましたが、オーバーランは徐々に減り、最終的には以下のようになりました。
Data deadline missed after 0 good transfers. Deadline was (5 ms, 0 ns), transit time was (10 ms, 585000 ns) Data deadline missed after 0 good transfers. Deadline was (5 ms, 0 ns), transit time was (9 ms, 588000 ns) Data deadline missed after 0 good transfers. Deadline was (5 ms, 0 ns), transit time was (8 ms, 531000 ns) Data deadline missed after 0 good transfers. Deadline was (5 ms, 0 ns), transit time was (7 ms, 469000 ns) Data deadline missed after 0 good transfers. Deadline was (5 ms, 0 ns), transit time was (6 ms, 398000 ns) Data deadline missed after 0 good transfers. Deadline was (5 ms, 0 ns), transit time was (5 ms, 518000 ns) Writer deadline missed after 3087 good writes. Deadline was (3 ms, 0 ns), time taken was (3 ms, 316000 ns) |
122 ミリ秒という最初の大幅な遅延から、遅延は徐々に短くなり、最終的には Writer または Reader の期限超過がまれにしか発生しないという状態に達しています。図 8 に、起動時のデータ転送時間のグラフを示します。
図 8. 起動時のデータ転送時間
Writer の最初の所要時間が 48 ミリ秒だったことを別にすれば、Reader からも、Writer からも、最初の 120 回のデータ転送中にオーバーランは報告されていません。それでは一体、遅延はどこで発生しているのでしょうか。この問題を解決する際にも、やはり Tuning Fork トレースが役立ちます。この場合の解決方法は、両方の JVM からのデータと両方のアプリケーション・スレッドからのデータを組み合わせることです (図 9 を参照)。コマンドラインに -verbose:gc を追加すれば、いずれの JVM でもまだ GC アクティビティーが開始されていないことを示すことができます。それでは今回もまた、クラス・ロードに責任があるのでしょうか。
図 9. Tuning Fork トレース – 両方の JVM およびアプリケーション・スレッド
図 9 で Reader JVM でのクラス・ロードが示されているのは上から 3 番目の行です。予想通り、Reader JVM でのクラス・ロードは Reader が最初に実行されるまでに完了しています。一番下の行は、Writer でのクラス・ロードです。X 軸の 20 ミリ秒と 70 ミリ秒の間にある棒の上にマウス・ポインターを重ねると、これらの棒のほとんどすべてが XML に関連していることがわかります。この場合もやはり、データ転送時間に大きく影響しているのはクラス・ロードによる遅延だということになります。最長の遅延である 122 ミリ秒は、Reader タイマーの最初の赤い棒から、Writer の最初の緑の棒 (49.88 ms というラベルが付いている棒) の終わりまでの間にあるギャップです。2 番目の転送は多少時間が短縮され、その後も転送が行われるごとに時間が短縮されるという傾向は、Writer がインバウンド・リクエストのバックログの処理をフルに実行している間続きます。そしてバックログがクリアされると、データ転送時間は 5 ミリ秒以内になります。これが、起動時のデータ転送が期限内に完了しないというパターンの説明です。しかし、クラス・ロードが唯一の要因なのでしょうか。Reader JVM と Writer JVM との間のソケットを介したデータ送信が起因している可能性はないのでしょうか。
JVM に接続するためにソケットを使用すれば、アプリケーションを 2 つのコンピューターで分割して実行することができますが、これによって遅延がもたらされる可能性があります。ソケットの使用が Reader アプリケーションと Writer アプリケーションのどちらのコードに影響を与えるのかを調べるため、両方のアプリケーション・コードに以下の変更を加えました。
- Nagle の無効化: Nagle アルゴリズム (「参考文献」を参照) は、リアルタイム・システムでは遅延をもたらすことで知られています。これは、ネットワーク・パケットをバッファーに入れてから送信するためです。Java アプリケーションで Nagle の設定をテストするには、
socket.getTcpNoDelay()を使用します。有効になっている場合、setTcpNoDelay(true)を設定すれば Nagle は無効になります。しかし Nagle を無効にしても、起動時の転送時間は短縮されませんでした。 PerformancePreferencesの使用:setPerformancePreferences(1, 2, 0)を使用することで、デフォルト・ソケットが変更されて、要件に近い振る舞いをすることが可能になります。この方法では、低遅延に最も重要性が置かれ、次に短い接続時間、そして最後に広帯域幅の順で優先されることになります。この変更をデモ・コードに加えたところ、起動時の遅延は大幅に短縮される結果となりました (図 10 を参照)。
図 10. Tuning Fork トレース – ソケットのPerformancePreferencesを設定した場合
これで、主な遅延は 40 ミリ秒まで抑えられました。これはクラス・ロードが原因の遅延なので、該当するクラスをプリロードすることで排除することができます。
今回の記事で、連載「リアルタイム Java での開発」は完了です。連載で一貫して強調してきたように、リアルタイム・アプリケーションでは予測可能性が最優先されます。この記事では、単純なリアルタイム Java アプリケーションを設計および作成し、タイマーとツールを組み合わせてアプリケーションの実行時間を分析し、アプリケーション・パフォーマンスの予測可能性を検証しました。そのなかで、GC による中断と実行中アプリケーションとの相互作用について説明し、クラス・ロードが遅延の原因となることを証明するとともに、単純なアプリケーションで XML を使用することによる影響を観察しました。そして、接続されたシステムを全体的に分析することの重要性を明らかにし、ネットワーク・ソケットが原因となっていた遅延を検出して短縮することに成功しました。
この記事でレポートしたパフォーマンス・データは、制御された環境で決定されたものです。他の動作環境で得られる結果はかなり異なってくるはずなので、使用している特定の環境に適用できるデータがどれなのかを確認してください。また、物理的に分散されたアプリケーションは、広範な変動要因による影響を受けることにも注意が必要です (「参考文献」を参照)。私たちはデモ・アプリケーションを単一のコンピューターで実行することによって、変動要因を軽減、あるいは回避しました。変動要因に対処する際には、この記事で紹介した Java ネットワークのチューニング・パラメーターをヒントにしてください。
| 内容 | ファイル名 | サイズ | ダウンロード形式 |
|---|---|---|---|
| Source code | j-rtjdev3.tar.gz | 5KB | HTTP |
学ぶために
- 連載「リアルタイム Java での開発」: この連載のすべての記事を読んでください。
- 連載「リアルタイム Java」: リアルタイム Java に関するこの 6 回連載の developerWorks 記事を読んでください。今回の記事に特に関係するのは、以下の記事です。
- 「リアルタイム Java、第 4 回: リアルタイム・ガーベッジ・コレクション」(Benjamin Biron、Ryan Sciampacone 共著、developerWorks、2007年5月): WebSphere Real Timeの一部となっている Metronome ガーベッジ・コレクターについて紹介しています。
- 「リアルタイム Java、第 3 回: スレッド化と同期」(Patrick Gallop、Mark Stoodley 共著、developerWorks、2007年4月): RTSJ でのスレッド化サポートについて学んでください。
- 「Creating a Debugging and Profiling Agent with JVMTI」(C. K. Prasad 他による共著、Sun Developer Network、2004年6月): この記事では、JVMTI を使用して Java アプリケーションのデバッグおよびプロファイリング・ツールを作成する方法を紹介しています。
- Tuning Fork ユーザー・マニュアル: Tuning Fork の資料に目を通してください。
- 「How Can the Nagle Algorithm Be Disabled?」: Nagle アルゴリズムの概要と、Lotus クライアントおよびサーバー・ソフトウェアでこのアルゴリズムを無効にする方法を説明しています。
- 「The Eight Fallacies of Distributed Computing」: 分散アプリケーションに関する危険な思い込みに注意してください。
- Technology bookstore: この記事で紹介した技術やその他の技術に関する本を参照してください。
- developerWorks Java technology ゾーン: Java プログラミングのあらゆる側面を網羅した記事が豊富に用意されています。
製品や技術を入手するために
- Tuning Fork Visualization Platform: SourceForge から Tuning Fork Visualization Platform をダウンロードしてください。
- IBM Toolkit for Data Collection and Visual Analysis for Multi-Core Systems: Java 開発者とパフォーマンス・アナリストがマルチコア・ハードウェアでの Java プログラムを分析する際には、このツールキットが役立ちます。
議論するために
- My developerWorks コミュニティーに加わってください。


