[Java] A Great Example of the Power of Flight Recorder

原文はこちら。
https://blogs.oracle.com/buck/entry/a_great_example_of_the

以下はOracle  JVM Sustaining EngineeringチームのマネージャであるMattis Castegrenの寄稿です。

先頃JFR (Java Flight Recorder) のレコーディング機能を使いバグを解決した格好の例がありましたのでご紹介します。

The Problem

この問題は別のチームからもたらされたもので、彼らのアプリケーションが完全に5~6秒ほど、およそ5分おきに完全に固まってしまう、という現象でした。

GC?

アプリケーションを実行してJFRでレコーディングしました。そう、まず最初にチェックするのはGCです。

Parallel GCを使っており、30分のレコーディングで2回、4秒間のstop-the-worldが発生していました。しかし、このアプリケーションはその回数以上に固まっていました。この問題はGCによる停止が見られるクラスタの別のマシンなのか、このマシンが別のマシンを待っているのか、と考えましたが、G1GCですべてのマシンを実行した後、GCによる停止は減ったものの、まだ長時間の停止が見られました。

WLS Events

で、ほかに何を言うことができるでしょうか。このチームはWLDF(WebLogic診断フレームワーク / WebLogic Diagnostic Framework)を使っていました。このフレームワークはサーブレットのリクエストやデータベースのクエリのようなWLSアクションのイベントを生成し、非常に強力です。イベントグラフを見ると、サーブレット・リクエストのみを有効にし、ちょっと目を細めてみましょう。長いリクエストのバンドのような休止を確認することができます。



より明確にするため、[ログ]タブに移動し、サーブレット呼び出しイベントを時間で並べ替え、4秒を上回るものを選択し、右クリックしてドロップダウン・メニューから[操作セット]>[選択項目の設定]を選択しました。[グラフ]タブに戻り、[操作セットのみを表示]を選択すると、かなり明確なイメージが得られます。



どうも定期的に固まってしまうようで、しかもこれはGCが原因ではないように見えます(少なくともすべての一時停止がGCによって引き起こされたものではない)。

Is the JVM frozen?

次の疑問は、JVM全体が固まってしまうのか、それともワーカースレッドだけが固まるのでしょうか。ロックの問題か何かでしょうか。この疑問を解くため、すべてのイベントを有効にして「操作セットのみを表示」のチェックを外し、休止の一つにズームインしました。グラフビューに慣れていない場合、いささか雑然としているように見えるかもしれません(注意:上のタイムスライダーにズームインします)


基本的に、各スレッドには異なるイベントプロデューサを表す複数の線があります。スレッドの一つにズームインします。



上部の黄褐色のイベントは、長時間のサーブレット呼び出しです。その下の赤いイベントは長時間のデータベースへのリクエスト、長時間の青のイベントはI/Oイベントです。マウスカーソルをそれらの上におくと、各イベントの情報を得ることができます。
ここでは、このスレッドが長時間のデータベースリクエストによって発生している長いI/Oを待っているように見えますが、その他のスレッドのほとんどはこのパターンには合致しませんでした。長時間のI/Oが一つのスレッドを一時停止させ、そのスレッドがあるリソースを保持していたことが問題だったのでしょうか。同期イベントを発見しなかったのですが、もしかするとその他の何かがあったのでしょうか。もしそうだとすれば、この時間範囲内で、そのリソースが必要なくなったスレッドから別のイベントが発見できたはずです。さらにスクロールダウンしてみたところ、あるスレッドから答えを見つけました。
ちょっとズームインしてみましょう。以下のように見えるいくつかのスレッドが確認できました。



黄色のイベントは待機イベントです。これらにはすべて1秒のタイムアウトがありますが、6秒間の待機イベントが2個突然現れました。タイムアウトは変更なく1秒であったにもかかわらず、です。これはJVM全体が固まっていることを強く示すものです。

Why is it frozen?

では、なぜJVMが固まったのでしょう。ここで、再度一時停止したスレッドにズームインし、[ログ]タブに移動しました。一時停止する直前に生成されたイベントを知りたかったので、開始時刻ですべてのイベントを並べ替え、すべてのイベントのリストをスクロールし、一時停止直前のイベントを見てみました。


8時46分08秒に、4.8秒要した「VM Operation」イベントがあります。その後、8時46分13秒(約4.8秒後)まで他のイベントは作成されません。イベントを見ると、「oracle.webcenter.DefaultTimer」スレッドから呼び出されたThreadDumpイベント、であることがわかります。スレッドダンプは”stop the world”を引き起こすので、これは理にかなっています。

Putting the pieces together

操作セットをよい感じにしてみましょう。ズームアウトし、スレッドダンプが発生した4秒間のすべてのVMの操作イベントを選択しました(2回のGCに起因するParallelGCFailedAllocation操作に伴う2個のイベントもありました)。私は操作セットにこれらのイベントを追加しました。
また、[イベント]タブに移動してoracle.webcenter.DefaultTimerスレッドを選択し、そのスレッドでのすべてのイベントを操作セットに追加しました。



[グラフ]タブに戻り、すべてのイベントタイプを有効化し、[操作セットのみを表示]を選択すると、以下のいい感じのイメージができあがりました。



DefaultTimerスレッドは定期的に4分45秒待機します。待機を終了すると、VMThreadのスレッドダンプを収集する2個のVMOperationを呼び出します。これらのオペレーションはこのアプリケーションで見られた休止タイミングと完全に相互に関連があります(GCによって発生したVMOperationsを除きます)。

The problem?

それでは、何が問題だったのでしょうか。スレッドダンプを引き起こしたDefaultTimerスレッドでスタックトレースを発見したアプリケーションチームへ調査を戻したところ、ソースコードは以下のようになっていました。
ThreadInfo[] threadInfos = threadMXBean.getThreadInfo(threadIds, true, true);
このメソッドのJavadocを見ると、以下のような記載がありました。
Interface ThreadMXBean(Java SE 7)
http://docs.oracle.com/javase/7/docs/api/java/lang/management/ThreadMXBean.html#getThreadInfo%28long[],%20boolean,%20boolean%29
"This method obtains a snapshot of the thread information for each thread including:
  • the entire stack trace,
  • the object monitors currently locked by the thread if lockedMonitors is true, and
  • the ownable synchronizers currently locked by the thread if lockedSynchronizers is true.
…
This method is designed for troubleshooting use, but not for synchronisation control. It might be an expensive operation."
インタフェース ThreadMXBean (Java SE 7)
http://docs.oracle.com/javase/jp/7/api/java/lang/management/ThreadMXBean.html#getThreadInfo%28long[],%20boolean,%20boolean%29
"このメソッドは、スレッドごとにスレッド情報のスナップショットを取得します。たとえば次のとおりです。
  • スタックトレース全体、
  • lockedMonitors が true の場合はスレッドによって現在ロックされているオブジェクトモニター、および
  • lockedSynchronizers が true の場合はスレッドによって現在ロックされている所有可能なシンクロナイザ。
…
このメソッドはトラブルシューティングのために設計されていますが、同期制御のためのものではありません。このメソッドの操作は負荷が大きくなる可能性があります。"
これでわかりました。このメソッドを"true, true" で呼び出すことで、モニターを含めてスレッドダンプが生成され、その結果(Javadocが警告しているように)システムが数秒間固まったのです。監視情報を収集していなければ、この問題はわからなかったでしょう(わずかに長いParallel GCの休止を除けば、うまく動いているように見えたのですから)。

Summary

まとめると、このデバッグはJFRイベントによって可能になったもので、使えなかったとしたら、スレッドダンプによって発生した長時間の停止をとらえたログを見つけ出すまで何度も繰り返し実行しなければならなかったでしょう。そしてどのスレッドがスレッドダンプ生成のトリガーになっているのかを探し出すために、さらに繰り返しの作業が必要になったことでしょう。今や、必要なのは、1回の実行で取得できるデータ、つまり最初に失敗した際に取得したデータだけでよいのです。JFRのパワーを見せつける格好の例です。

0 件のコメント:

コメントを投稿