渋谷Java第二回で発表した内容のもう少し掘り下げた内容です。 主にUNIX系OSでのJavaアプリケーションサーバ運用を想定しています。
なおJava7以前のVMを取り扱っていますが、Java8においてもPermanent以外はほぼ同様かと思います。
運用フェーズのリソースリーク
「運用が始まってからJavaWebアプリのリソース障害が発覚する」ということがちょいちょいあります。
特に開発と運用が綺麗に分離されている組織などではエアポケットになりやすい部分かと思います。
もちろんリソースリークの起きないコードを書くこと、リソースリークが発生していないことをテストする、ということが一番重要です。 しかし、新規サービスの立ち上げにおいては往々にして誰も事前に想定できなかったアクセスパターンが生まれたりします。それによって初めてアプリケーションの非機能的な弱点が見つかることが現実には起こります。
また、例えば「OutOfMemoryErrorが起きた」という事象を聞いただけでは開発者はおそらく調査はできないでしょう。 ヒープダンプがあれば調査はできるかもしれませんが、ヒープダンプはJVMが取り扱っているほぼすべての情報を含んでおり、個人情報や決済情報、機密情報が含まれている可能性を考慮すると、秘密保持契約を結んでいる委託先であっても提供が難しいケースもあるかもしれません。かといって加工することは現実的ではないかと思います。
私の経験上、ある程度のテストを経てサービスインしたアプリであれば仮に運用中にリソースリークが発生することが発覚してもシステムが即死するほど深刻な状況に陥ることはまずありません。 つまり、リソースリークを予見した監視を行うことによって、問題発見後もサービスを継続し、調査や改修のための時間を稼ぐことが可能になります。
運用フェーズにおいて発覚するリソースリークの問題については以下の点が重要となります。
- サービス障害となる前に検知できるか
- 開発者に対して必要な情報を提示できるか
- 問題が解決するまでサービスを維持できるか
これらのために最低限どのような監視を行えばいいかを考えてみました。
運用しているアプリケーションそのものに精通していなくても外側から見ていけるものを中心に挙げていますので、現在商用稼働中のものでも始められるかと思います。
メトリクスの可視化
原則、システム監視とはインシデントの検知までは全自動であるべきです。 しかしサービスの成長によるアクセス増やシステム改修によって挙動が変化していきますし、全自動化するためにはそれなりの知見が必要となります。 そのためメトリクスの可視化を行う必要がでてきます。
数値を記録していき、適宜集計・グラフ化するというのも一つの方法ですが、CactiやMunin、Zabbixなどで常時グラフ化することをお勧めします。
グラフ化することでアプリ開発者でなくてもある程度直感的に異常を感じ取れるのではないでしょうか。
JVMの監視観点
各ヒープ領域のサイズと使用量
正しくはサイズ・使用量・使用率のうち2つを取得すればOKです。どのようなグラフをレンダリングしたいかで決まるかと思います。 サイズについては、アプリケーションサーバの場合はヒープが自動拡張されないよう設定しますが、ヒープサイズを変えたタイミングが一目でわかります。
Young(Eden/Survivor)/Old/Permanetはそれぞれ別々の役割を持った領域です。 それらの使用量を合計してもさほど意味はないように思います。 それぞれの領域を別々のグラフにする、またはすべての領域を積み重ねることで、メモリマッピングの全体イメージがつかみやすくなるのではないかと思います。
縦軸がメモリ量(塗りつぶしがサイズ、線が実使用量/紫がPermanent,橙・青がSurvivor,黄色がOld,緑がYoung)、横軸が経過時間となります。
例えば典型的なメモリリークは以下のようなグラフになります。
Full GCの頻度
前述のメモリリークのグラフでは時間経過と共に次のFullGCまでの間隔が短くなり、最終的には短時間で何度もFullGCが発生しています。 一般的に、特段アクセスが増えているわけでもないのにFull GCの頻度が高くなってきた場合はメモリリークが疑われます。
この頻度をメトリクス化するためには、JMXやjstatコマンドなどで定期的にFull GCの回数を取得します。 ただ、私の経験上はこのメトリクスはあまり役に立ちませんでした。 正確には「閾値設定が抜群に難しい」という結論です。
「規定時間内に何回Full GCが発生したか」を閾値に監視した場合は以下の問題があります。
- 監視間隔を長めにした場合、ざっくりしすぎていて値の正常性がわかりにくい。
- 監視間隔を短くした場合、警告発生時には手遅れになっている場合が多い。
GCログの解析
つまるところ、メモリリークとは「Full GCが発生してもOld領域が回復しない状態」です。 これは「Full GC後のOld使用量(使用率)」を監視することで兆候をとらえることができます。 手っ取り早いのはGCログを解析することです。
VMオプションに-XX:+PrintGCDetailsを付けてGCログを出力することでFull GC前後のOldの使用量を知ることができます。 VMオプションやGCアルゴリズムによってログの出力書式が異なりますが、シリアル(パラレルGC)の場合は下記のParOldGenの部分となります。
[Full GC [PSYoungGen: 216M->0M(1024M)] [ParOldGen: 1640M->208M(2048M)] 1856M->208M(3072M) [PSPermGen: 252M->252M(512M)], 0.0120775 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]
この場合Full GC後のOld使用率は約10%となります。この値をメトリクスとします。 できれば運用前にこのトレンドを押さえておきたいですが、それが難しい場合は50%程度にして置くことでメモリリークの兆候をつかめるのではないかと思います。
ファイルオープン数
アプリケーションサーバはリクエストに対して事前にプロセスやスレッドを用意しておき、そのプロセスやスレッドが何度もリクエストを処理することでオーバーヘッドや負荷を軽減させていますが、その代償として開発者はリソース管理を厳しく行わなければならないという責任を負います。 ファイルや外部との通信が適切にクローズされない場合もまたリソースリーク起因の障害を引き起こすことになります。
代表的な監視メトリクスとしてはファイルオープン数があります。 UNIX系システムでは通信(ソケット)もファイル扱いとなるため、通信のクローズ漏れでもファイルオープン数が上昇していきます。 こういったケースでは同時にメモリも失われていくケースも多いです。 ファイルオープン数の上限値はカーネル設定で定められており、この上限に達すると新規接続が受け付けられない、外部通信ができないなどの障害を引き起こします。 適切な値はサービスイン前に負荷試験などによって見極めてください。
ちなみにLinuxの場合、一般ユーザのデフォルト値は1024のため、割と簡単に上限に達してしまいます。 特にJavaアプリケーションサーバ&アプリは起動するだけで大量のjarファイルを読み込みますので、注意が必要です。
これらはlsofやpfilesコマンド等でアプリケーションサーバのファイルオープン数を取得することですべて把握することができます。 この際、ファイルの種類ごとにある程度分類した形で値を取得するのがポイントです。
ちなみにDBについてはコネクションプーリングで管理されることが多いためほとんど心配はないですが、外部のWebAPIとの通信やメールの送信、テンプレートエンジンなどでリソースリークを起こしやすいように見えます。また、Full GCでリソースが解放されることもあるため、この場合のワークアラウンドとしてはスレッドダンプ出力などであえてFull GCを発生させるという方法もあります。
ヒープ統計情報
VMオプションに-XX:+PrintClassHistogramを付けた状態で、 UNIX系の場合は
kill -3 <アプリケーションのPID>
を実行することで、GCログにヒープ統計情報が出力されます。(アプリケーションサーバの同一の実効ユーザ、または管理者権限が必要です)この際、直前にFull GCが発生します。
出力内容にはどのオブジェクトが何個、何バイト分存在しているか、というリストが出力されます。 これによってどんなオブジェクトがメモリ上に残っているかを確認できます。 ただしこれは現在の状態を出力したものですので、定期的に取得することで初めてオブジェクトの増減を確認することができます。 この処理自体はそんなに重くない処理ですので、定期的に(とりあえず1日1~数回でいいです)取得するよう設定すべきです。
別の可能性:純粋なメモリ不足
性能試験の不足に起因するケースとも言えますが、バースト的にアクセスがあった際ににOOMになる場合はメモリリークより先にメモリ不足を疑ったほうがいいです。この場合、切り分けという意味でもメモリ割り当てを増やすのが第一選択ではないでしょうか。
リソースリークの結果、危なそうだったら…
必要な情報が取れていれば、アプリケーションサーバ再起動を検討すべきです。 切り離してヒープダンプを取得することも有効かもしれません。
正常化するまでシステムを持たせるのがシステム運用の腕の見せどころともいえますが、設計や構成検討の段階でサーバ冗長化とセッションの外部ストア化orセッションレプリケーションは最低限視野に入れておく必要があります。