システムが無反応になってしまう3つの原因
- 「システムが○○秒間無反応になっちゃって……」
- 「システムが無反応なのに、OSのリソース使用量を見ると全然使ってないんだよね……」
JVMを使用しているシステムでは、そんな話を耳にします。なぜ、そのようなことが起こってしまうのでしょうか?
それらのおもな原因のうち、リソースに関わるものをまとめたのが表1です。
表1 無反応のおもな原因
| 主な範囲 | 無反応の主な理由 |
① | H/W, OS, M/W, JVM | リソース(CPU、メモリ、ディスク、ネットワーク)不足により処理が完了するまで停滞するなど |
② | M/W | コネクションやスレッドのプール数が足りず、割り当てられるまで待機など |
③ | JVM | GCへ与えるスレッド数が足りず、CPUを使い切れていない など |
原因は大きく分けて3つあります。
1つめは、リソース不足です。たとえば、CPUのクロックやコアが足りず、処理の完了待ちであることなどが原因として考えられます。
図1 CPUのリソース不足の例
2つめは、M/W(ミドルウェア)からアプリケーションに提供されるスレッドや、コネクションのプールにあるリソースが不足していて、その提供待ち(無応答)になっている可能性です。
M/Wでプールしているリソースが不足した状況は、会社の書籍棚に1冊しかない本を社員で順番待ちしている状況に似ています。1冊しかないため、今借りている人が返すまでは他の人は借りれません。
図2 M/Wでプールされたリソースが不足しているケース
1つめのケースで問題がGCにある場合、GCに割り当てるリソースのバランスが悪いことが原因です。たとえば、特定のプロセスに対するCPUリソースの割り当てが大きい場合、GCが起きると、CPUリソースを多く使ってしまいます。これによりCPUリソースを占有し、同じOS上で動いているほかのプロセスの動作にも影響を与えてしまい、動作が遅くなってしまうのです。
3つめは、JVMでGCの設定を誤っている(またはデフォルトのままで何もしていない)場合です。この場合、全体的なリソースは十分あるのに、GCに与えるリソースが足りていない可能性が考えられます。
図3 GCに与えるリソースが問題のケース
GCの実行頻度を握るのは「生きてるオブジェクト」の量とヒープサイズ
このようなケースに対応するためは、GCの実行頻度や停止時間を考えなければなりません。そのために、GCの内部でどのように、どのような処理が行われているかを把握していきましょう。本記事では、自プロセス上のGCによる無応答状態にフォーカスして解説します。
前回、「アプリケーション開発者がオブジェクトへの参照をすべて削除すると、JVMはGCによって不要なオブジェクトを回収し、ヒープを解放する」という仕組みを説明しました。
図4 GCによってオブジェクトを解放する(前回の図2の再掲)
ヒープに格納されているオブジェクトのうち、アプリケーションがたどり着けるオブジェクトを「生きているオブジェクト」(またはLive Object)と言います。一方、たどり着けないオブジェクトを「死んだオブジェクト」(またはDead Object)と言います。GCが回収するのは、死んだオブジェクトです。
図5 生きてるオブジェクトと死んだオブジェクト
ヒープサイズの中で、生きているオブジェクトの割合が多い場合は、一時的に使用されるオブジェクト(すぐに死んだオブジェクトになる)を少し作ると、GCが実行されてしまいます。
図6 生きてるオブジェクトの割合が多いヒープ
そのため、GCの実行頻度を適切に設定するには、生きているオブジェクトの量とヒープサイズを検討しなければなりません。
GCの停止時間は「スループット」と「レスポンスタイム」で考える
GCにはさまざまな種類があります。選択したGCの種類やサーバの環境によっては、GC中にアプリケーションが停止しまうことがある点に注意しなければなりません。
GCによってアプリケーションが停止している時間を、一般的に「停止時間」、または「Stop the World(頭文字をとってSTWと記載されることもあります)」と呼びます。
図7 停止時間とは
この停止時間内はアプリケーションのスレッドがすべて停止しているため、アプリケーションは実行されません。つまり、アプリケーションが処理できる時間は減ってしまいます。
停止時間を改善するために考えなければいけないのは、次の2つです。
- スループット(処理量)
- レスポンスタイム(応答時間)
たとえば、図8のように、処理1回あたりが0.1秒で終わる、つまりレスポンスタイムが0.1秒だったとします。単位時間を1秒とすると、1秒間に10回実行できるため、スループットは10になります。
なお、処理の実行中には新たな処理のリクエストが来ず、処理が終わったタイミングで次の処理が来るものとします。
図8 スループットとレスポンスタイム
しかし、実際はどこかのタイミングでGCが実行され、アプリケーションが停止されます。
図9は、処理中にGCが発生した場合の例です。途中にGCが2回、計0.2秒間実行され、処理の途中でもGCが実行されています。
図9 停止時間によるスループットとレスポンスタイムの影響
前述のとおり、GCによる停止時間内では、アプリケーションの処理が停止します。そのため、GCが発生すると単位時間あたりの処理できる時間が短くなり、スループットが低下します。また、アプリケーションの処理途中にGCが実行された場合、処理中のアプリケーションのレスポンスタイムが大きくなります。
例では、レスポンスタイムは8回中7回は0.1秒でしたが、最大は0.2秒です。単位時間を1秒間とすると、スループットは8です。
停止時間に対する2つのアプローチ
停止時間に対処するには、どうすれば良いのでしょうか?
アプローチの方法は、「スループットの向上」と「レスポンスタイムの短縮」のどちらを重視するかで、それぞれ以下のようになります。
スループットを向上させたい場合
バッチ処理のように、1回のレスポンスタイムの短縮よりもアプリケーションのスループットを向上したい場合は、停止時間の総和が小さくなるようにします。
図9で「スループットが要件に比べて低く、スループットを向上させなければいけない」というシチュエーションを考えてみましょう。その場合、図10 停止時間の総和が小さくなるようにすることで、スループットを向上させることができます。
図10 スループット向上のアプローチ
図10では、GCの回数は減り、1回あたりのGCの停止時間は長くなっていますが、総和は小さくなりました。その結果、スループットは8から8.5に向上しました。
レスポンスタイムを短縮したい場合
オンライン処理のように、レスポンスタイムを短縮したい場合は、1回あたりの停止時間が小さくなるようにします。
図9で、レスポンスタイムの要件が0.2秒未満、そして要件を満たしていないレスポンスがあったとします。その場合は、図11のように、1回あたりの停止時間を小さくすることでレスポンスタイムを短縮することができます。
図11 レスポンスタイム短縮のアプローチ
停止時間の総和は2秒と変わりませんが、GCの回数は増えました。そのため、1回あたりのGCの停止時間は短くなっています。
結果として、6回のレスポンスタイムは0.1秒で、2回のレスポンスタイムは0.15秒にできました。
停止時間の総和は変わっていないため、スループットには変化はありません。これでレスポンスタイムの要件であった0.2秒未満を満たすことができました。
これらの例のように、GCでは「実行頻度と停止時間の組み合わせ」を考慮する必要があります。
JVMのGCの実装はたくさんあるので、すべて把握するのは非常に困難です。そのうえ、実際のシステムでは、JVMを選べる機会はあまりありません。しかし、問題を解決するためには、使用するJVMにどのようなGCが実装され、どのように対処すればいいかを判断しなければならないのです。
次回からは、GCとスレッドの関係を解説していきます。お楽しみに。