【後編】原因不明のOutOfMemoryエラー、性能劣化の問題も一挙に解消
WebLogic Server Enterprise Editionでは、システム監視機能「JRockit Mission Control」を使うことで、複雑なトラブルの原因究明も速やかに進めることができる。だが残念ながら、同機能を使いこなしているユーザーはまだ少ないようだ。本記事では前編に続き、Application Gridソリューション部 シニアセールスコンサルタントの二川 秀智氏(日本オラクル Fusion Middleware事業統括本部 第一ソリューション本部)に聞いた、実際のトラブル事例に基づくJRockit Mission Controlの活用例を2つ紹介する(編集部)。
原因不明のOutOfMemoryエラー問題の解決
Javaアプリケーションでよく見られるトラブルの1つとして、メモリ不足が原因となって引き起こされるOutOfMemoryエラーが挙げられる。メモリ・リークなどでJVMのヒープが圧迫され、結果としてアプリケーションが使えるメモリ領域がなくなってしまうという現象だ。B社も、このOutOfMemoryエラーに悩まされていた。
B社はインターネット上でオンライン・サービスを展開しており、そのプラットフォームとしてWebLogic Serverを利用しているが、時折発生するOutOfMemoryエラーに悩まされていた。調査を試みたものの原因は判明せず、結局毎晩サーバを再起動するという対処療法的な運用を余儀なくされた。もちろんこれは放置できる問題ではないため、二川氏の下に調査依頼が来ることとなった。
二川氏はB社のシステム構成を確認した後、JRockit Mission Controlの1機能である「Memory Leak Detector」を使い、OutOfMemoryエラーの原因を探ることにした。Memory Leak DetectorはJavaアプリケーションのメモリ・リークを検出し、その原因を特定するために用意されたツールだ。これを使うことで、リークしているオブジェクトの型やインスタンスなどに関する統計情報を取得し、割り当てられた場所をビジュアルに確認することができる。
Memory Leak Detectorを使い、まずリークしているオブジェクトの型をチェックしたところ、バイト配列(byte[])がヒープ中の大きな割合を占めており、さらに増加率も極めて高く、ガベージ・コレクション(GC)で回収される割合よりも増加する割合のほうが大きいことがわかった。
もちろん、単にバイト配列が増えているというだけでは、根本的な原因の究明には至らない。そこで続けてMemory Leak Detectorのタイプ・グラフやインスタンス・グラフをチェックし、バイト配列の割り当て元を特定した。まず判明したのは、スレッドごとにオブジェクトを保持するための機構であるThreadLocalが使われているということだった。
バイト配列がヒープの40%以上を占有している
当該のバイト配列はjava.lang.ThreadLocalから参照されている
WebLogic Serverを含むJava EEアプリケーション・サーバのほとんどは、スレッド生成/廃棄のオーバーヘッドを解消するために、一度生成したスレッドを使い回すスレッド・プールの仕組みを採用している。実はこのスレッド・プールとThreadLocalの相性は良くない。スレッド・プールにプールされたスレッドはアプリケーション・サーバの停止までなくならないため、ThreadLocalを使うと、ユーザー側で明示的にクリアしない限り、当該のThreadLocalが保持する値がずっと残存してしまうという問題が発生するのだ。
二川氏は、このThreadLocalの使われ方をさらに詳しく調査した。Memory Leak Detectorはリークの可能性があるインスタンスをクラス・ローダごとに表示できる。この分析を行ったところ、当該クラス(ThreadLocalに格納された値のクラス)は多数のクラス・ローダからそれぞれロードされており、それらのクラスのインスタンスがすべてヒープに残存し続けていることが判明した。通常なら1つのアプリケーションには1つのクラス・ローダのみが紐付くのだが、これはどうしたことだろうか?
リークしているクラスが多数のクラス・ローダから何度もロードされている
実はB社の環境では、WebLogic Serverのプロダクション再デプロイメント機能を利用していた。この機能はWebLogic Server上で新旧2バージョンのアプリケーションを同時に並行して動かすことで、無停止でのアプリケーションの更新を実現する仕組みだ。この機能を使用する場合、1つのアプリケーションは最大2つのクラス・ローダを使用するが、今回は2つ以上の数のクラス・ローダが残存していた。
ここでThreadLocalの性質について説明しておこう。ThreadLocalにセットされたオブジェクトは元のThreadから強参照されている。したがってアプリケーションがアンデプロイされても、元のThreadがなくならない限り、当該オブジェクトはGCの対象にはならない。オブジェクトがGCされない限り、そのオブジェクトをロードしたクラス・ローダも破棄されないので、アプリケーションのアンデプロイが見掛け上は成功していても、クラス・ローダは破棄されずに残存してしまうという事象が発生するのだ(いわゆる、クラス・ローダ・リーク・パターン)。
WebLogic Serverのプロダクション再デプロイメント機能では、旧バージョンのアプリケーションはアクセスがなくなった(すべてが新バージョンに切り替わった)時点で"リタイア"となり、それまでに生成されたオブジェクトはこのタイミングでGC対象となる。しかし、ThreadLocalを使用していると、そこに設定されたオブジェクトは残ったままとなり、リタイア後もGC対象にはならない。つまり、見かけ上はアンデプロイが行われているように見えても、内部的にはヒープに残存したままとなる。これを繰り返すことでヒープが圧迫され、最終的にOutOfMemoryエラーに至ってしまっていたわけだ。
今回はこうして原因が判明したが、もしJRockit Flight RecorderやJRockit Mission Controlがなければ、このように原因を追及するのは極めて難しかっただろう。そもそもThreadLocalが原因だと突き止めること自体が容易ではなく、さらにクラス・ローダ・リークを特定するまで調査を進めるのは非常に困難だからだ。JRockit Flight Recorder/JRockit Mission Controlがあるからこそ、WebLogic Serverではこうした複雑なトラブルの原因究明が行えるのである。
ハードウェア・リプレースに伴う性能問題への対応
最後は、やはりインターネット上で取引会社向けにオンライン・サービスを提供しているC社のケースだ。同社もWebLogic Serverを利用していたが、パフォーマンスの向上を目的にハードウェアをアップグレードした。具体的には、4コア(1ソケット×4コア)のサーバから12コア(2ソケット×6コア)のサーバに切り換えた。
本来であればコア数が3倍に増えたのだから、性能もそれなりに向上すると考えるところだ。しかし、実際には逆に性能は悪化した。4コア・サーバでは秒間3,500件を処理できていたのに対し、12コアのサーバでは秒間2,000件しか処理できなくなっていたのである。
そこでC社はサーバの多重度やJVMのヒープ・サイズ、GCの設定などを変えたりしてみたが、結果は変わらない。OSベンダー、ハードウェア・ベンダーに問い合わせても原因はわからなかった。そこで二川氏の下に相談が寄せられたわけである。
二川氏はシステム構成についてヒアリングした後、JRockit Flight Recorderを使って動作状況をチェックした。そこでわかったのは、新サーバでは「ファット・ロック」と呼ばれる現象が旧サーバの30倍もの高頻度で発生しているという事実である。
マルチスレッド環境で並列処理を行う際、1スレッドでしか実行してはいけないセクション(クリティカル・セクション)がある場合、排他と同期の処理が必要になる。WebLogic Serverではこの処理をロックを用いて実装しており、ロックを獲得したスレッドだけがクリティカル・セクションを実行できる仕組みとなっている。
ロックを獲得できなかったスレッドは、別のスレッドがロックを解放するまで待つことになる。この際、競合が少なければSpin(ループ)してロックが解放されるのを持つ「シン・ロック」となるが、競合が多く、しばらくループを回しても獲得できない場合はスリープ状態でロックの解放を待つ「ファット・ロック」に引き上げられる。JRockit Flight Recorderで取得した稼働情報を確認したところ、12コア時の動作ではこのファット・ロックが大量に発生していた。
旧マシン(4コア/1 JVM)におけるJavaロック・プロファイリング
新マシン(12コア/1 JVM)におけるJavaロック・プロファイリング
今日のサーバ・アーキテクチャは、CPUソケットごとにメモリを接続し、さらにソケット間をインター・コネクトと呼ばれるバスを使って接続するのが一般的となっている。CPUは自身につながるメモリにはダイレクトにアクセスし、別のソケットにつながれたメモリにアクセスする場合はインター・コネクトを経由する。
マルチソケット環境における非対称メモリ・アクセス
ところで、インター・コネクト経由でのアクセスはダイレクト・アクセスに比べて遅延が大きい。今回のケースは複数のスレッドが同時にロックを獲得しようとする場合、いくつかは確実にインター・コネクト経由となるため、ロックの獲得に時間がかかり、ループで待つシン・ロックから、スレッドがスリープするファット・ロックにエスカレーションしてしまう(この現象は、マルチソケット・システムにおける課題として知られている)。
WebLogic ServerのJVMであるOracle JRockitでは、CPUソケットごとにJVMを分けて起動し、それぞれのJVMは自身が動いているソケットにダイレクトに接続したメモリだけにアクセスするように設定することができる(JVM引数の「-XX:BindToCPUs」に各ソケットに属するCPUコア番号を指定し、JVM引数「-XX:NumaMemoryPolicy=strictlocal」でソケットにローカルなメモリ・アクセスを強制する)。これによりファット・ロックの数は激減し、本来の性能を引き出すことができた。
新マシン(12コア/1 JVM)におけるJavaロック・プロファイリング(設定変更後)
* * *
以上、前後編合わせて3つのトラブル事例を紹介した。いずれのトラブルも従来のアプローチでは原因究明は難しかっただろう。だがWebLogic Serverの場合、OSからアプリケーションまで、システムの各レイヤの稼働情報を記録できるJRockit Flight Recorderを動かし、取得した稼働情報をJRockit Mission Controlによってドリルダウンしながら確認していくことで、問題個所を確実に特定することができる。WebLogic Serverのメリットは性能だけではない。ぜひ本企画の内容を参考にしながら、日ごろよりJRockit Flight Recorder /JRockit Mission Controlでシステムの稼働情報を確認する習慣を付けてほしい。もしかしたら、これまで見逃していた問題が見つかったり、以前に迷宮入りになっていた問題の原因が見つかったりするかもしれない。


