本連載は、日立製作所が提供するアプリケーションサーバ「Cosminexus」の開発担当者へのインタビューを通じて、Webシステムにおける、さまざまな問題/トラブルの解決に効くノウハウや注意点を紹介していく。現在起きている問題の解決や、今後の開発のご参考に(編集部)
Webシステムを安定稼働させるには、考慮しなければならないことは数々存在する。システムの適切なサイジングを行うことも、その1つだ。
今回は、その中でもJavaVMのメモリのサイジング(見積もり)とGC(ガベージ・コレクション)に着目して説明する。
今回は、以下の問題についての話だ。
性能が安定しない。普段は軽い処理なのに、ピーク時などにレスポンス遅延が発生する。
メモリサイジングをミスした場合、業務ピーク時のGCの多発や長期化からこの現象に陥る場合がある。CPU利用率が100%に張り付いて性能が出ない場合は、GCが原因かを疑う必要がある。その場合、まずはGCログを参照し、GCの処理時間や発生頻度から1分ごとのGC処理の占有率を確認してみるとよい。
占有率が大きい場合、アプリケーションの処理やメモリのサイジングを見直す必要がある。以降では、GCを考慮したメモリのサイジングについて説明する。
初めに、Cosminexusの場合を例に、JavaVMのメモリ空間の構成を示す。
一般に、JavaVMのメモリ空間は、JavaVMが管理するJavaVM固有領域と「Cヒープ」などのOSネイティブの領域で構成される(Cosminexusの場合は、このほかに後述のFull GCレス機能に対応する「Explicitヒープ」が存在するという)。
JavaVM固有領域は、Javaオブジェクトを配置する「Javaヒープ領域」と、ロードされたclassなどの情報を格納する「Permanent領域」に分けられ、JavaVMが世代別GCを採用する場合、Javaヒープはさらに、「Eden」「Survivor」「Old」領域に分けられる。
生成されたオブジェクトはまずEden領域に配置される。GCのタイミングで使用済みのオブジェクトは回収されるが、使用中のオブジェクトはSurvivor領域に移動する。そして、生存期間の長いオブジェクトは、何度かのGCの後でOld領域へと移動する仕組みとなっている。
JavaVMは、メモリ領域の利用状況に応じて使用済みメモリ領域を回収する。これがGCである。世代別GCを採用する場合、GCには「Copy GC」「Full GC」がある。
Copy GCは「New領域」(Eden+Survivor)を、Full GCではJavaVM固有領域全体を対象に、使用済みメモリ領域を回収する。そのため、GCの多発や長期化を防ぐには、それぞれのGCの発生要因を把握し、適切なサイジングを施すことが必要となる。
例として、CosminexusでのGCの主な発生要因を見てみよう。
Permanent領域は、Webシステムの稼働中は変動が少ない。そのため、主に、Javaヒープを対象に見積もっていき、Full GCの発生要因1と2が発生しないようにサイジングする必要がある。
また、Old領域の拡張が発生しないように、Javaヒープ領域の初期値と最大値は同じ値にすることを推奨する。
一般に、Copy GCに比べて、Full GCは実行に時間がかかる。そのため、Full GCはなるべく発生させず、使用済みメモリ領域はCopy GCでうまく回収させることが安定稼働のポイントとなる。
そのためには、Full GCの発生間隔を考慮してOld領域のサイズを見積もることが重要だ。ポイントを以下に示す。
Old領域のメモリサイズは、「アプリケーションが使用するメモリサイズ」+「Java EEサーバで使用するメモリサイズ」+「New領域のメモリサイズ」で見積もる。
この中で重要なのは、「アプリケーションで使用するメモリサイズ」だ。このメモリサイズはOld領域に移動する「生存期間が長いオブジェクト」のサイズから見積もることとなる。
Webシステムの場合、トランザクション処理で利用されるオブジェクトは確保しても、すぐに捨てられるのに対し、セッション情報などはクライアントのログインからログアウトまで、ずっと利用されるため、生存期間が長い。そのため、セッション情報の蓄積量からアプリケーションで使用するメモリサイズを見積もるとよい。
具体的には、セッション情報のサイズと毎秒の想定ログイン数から毎秒増加するセッション情報のメモリサイズを算出し、すべてがOld領域に移動した場合でも、Full GCの発生許容間隔を満たすように見積もることになる。
ただし、最大ログイン数に関しては、さらに考慮が必要だ。Full GCのタイミングでは、最大ログイン数分のセッションは使用中で、Full GC後も残る場合がある。そうなると、次回のFull GCは、許容時間よりも早く発生してしまう。そのため、最大同時ログイン数分のメモリサイズを余分に確保する必要がある。
Old領域全体では「Java EEサーバで使用するメモリサイズ」+「New領域のメモリサイズ」が追加で必要となる。「New領域のメモリサイズ」は、先ほど述べたFull GC発生要因の1に対する備えである。
JavaVMは、GCでNew領域からOld領域に多くのオブジェクトが移動した場合でも、メモリ不足とならないように、Old領域の空き領域がNew領域サイズよりも少ない場合には、Full GCを実行する。そのため、New領域のメモリサイズ分は余分に見積もる必要がある。
New領域では、Survivor領域の見積もりが大切だ。Survivor領域での見積もりのポイントは領域のサイズとOld領域にオブジェクトが移動する年齢だ。
Survivor領域のサイズが小さく、Copy GCのタイミングでSurvivor領域があふれると、Old領域にオブジェクトが移動する。生存期間の短いオブジェクトがOld領域に移動すると、Full GCの要因となってしまう。そのため、Survivor領域は十分なサイズが求められる。
一方で、いつまでもSurvivor領域にオブジェクトが残っていると、Copy GCの処理時間は長くなる。そのため、オブジェクトの年齢(生き残ったCopy GCの回数)が、ある“しきい値”を超えると、そのオブジェクトはOld領域に移動するようになっている。
そのため、以下をチェックする必要がある。Survivor領域があふれて、Eden領域からOld領域に直行することがないように注意が必要だ。
見積もりの考え方としては、以下のようにするとよい。
ちなみにCosminexusでは、Survivor領域におけるOld領域への移動年齢のしきい値はCopy GCごとに動的に変動するという。MaxTenuringThresholdオプションの値を超える年齢のオブジェクトは必ずOld領域に移動するが、しきい値は、Survivor領域のメモリ利用率が「TargetSurvivorRatio」オプションの値に近くなる年齢を算出し、次回Copy GCでの移動のしきい値として利用される。
また、Survivor領域でのオブジェクトの年齢分布を調べるには、Cosminexusでは独自のオプション「-XX:+HitachiVerboseGCPrintTenuringDistribution」を利用するとよいという。Copy GCでのSurvivor領域の使用状況がログに出力できる。世代を考慮した累計のメモリ使用量が出力されるため、年齢分布が把握できる。
Eden領域のサイズは、Copy GCの頻度に直結する。当然ながら、サイズが大きいとCopy GCの発生間隔は長くなる。Eden領域のサイズは、システムのCopy GC許容間隔の条件を満たすように設定する。
1トランザクションで使用するメモリ使用量とシステムのTPSから、毎秒のメモリ使用量を求めることができる。そこで、Eden領域がいっぱいになるまでの時間がCopy GCの許容間隔よりも大きくなるように見積もる。
なお、Copy GCの処理時間そのものはオブジェクトのサイズよりも個数に影響される。
このように、GCを考慮してメモリサイズを見積もることとなる。現実的なラインとしては、Copy GCが10〜20回の間にFull GCが1回発生するぐらいが目安だ。当然のことだが、見積もり後にGCの間隔が想定と違う場合には、各領域のメモリサイズを再度調整することとなる。
Webシステムでは、Full GC発生の一番の要因は、HTTPセッションに関連するオブジェクトだ。セッションと関連するオブジェクトはログインからログアウトまで生存し、生存期間が長い。そのため、Old領域に蓄積していきFull GC発生の要因となるのだ。
このようなGC発生の要因を考慮し、高い精度のメモリサイジングを行うことで、システムの安定稼働へとつなげてみてはいかがだろうか。
ちなみにCosminexusでは、Full GCレス機能を備え、HTTPセッションのような一定期間存在するオブジェクトを、Explicitヒープで管理することが可能だ。これにより、Old領域へのセッションオブジェクトが移動することを防止し、Full GCの発生を抑止できるという(参考:日立がアプリサーバ新版、Full GC回避し「世界を止めない」)。最新版のV8.5では、より機能が強化され、アプリケーションや特定フレームワークでOld領域増加の要因がある場合でも、適用できるようになったそうだ。
Copyright © ITmedia, Inc. All Rights Reserved.