OutOfMemoryErrorの原因と対応(2)
前回(OutOfMemoryError回避のためのJavaコーディング – 前編)に引き続き、OutOfMemoryErrorの話題。前回は、OutOfMemoryErrorを3パターンに分けた。
- (A)サイズオーバー型
- 巨大な領域確保によって一気にヒープの最大サイズをオーバー
- (B)メモリリーク型
- 開放されないオブジェクトが溜まり続けることで使用中メモリが徐々に増加し、メモリが枯渇
- (C)マルチスレッド型
- 長時間処理により”死んだ”スレッドがメモリを食い尽くす
今回は(C)マルチスレッド型に関連するOutOfMemoryErrorを紹介する。
(C)”死んだ”スレッドがメモリを食い尽くす – マルチスレッド型
HTTPリクエストの処理に長時間を要する場合、そのリクエスト処理中で確保したメモリ領域が開放されずに残されたままとなることがある。
長時間かかる処理といえば、大量データの処理や複雑な処理がまず頭に浮かぶけど、それだけじゃない。サーバー側では通常、DBを始めとしていろんな外部通信を行っている。その外部通信のレスポンスタイムが悪くなったりすると処理時間が長くなり、OutOfMemoryErrorが発生するときがある。
軽い処理だから・・・と安心していた場所で発生するのがこのタイプの特徴だ。
サンプル:HTTP通信タイムアウト未設定でOutOfMemoryError
HTTPで外部通信を行い、その結果を出力するServletを考えてみよう。
public class UrlConnectionServlet extends HttpServlet { protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { byte[] bytes = new byte[10240]; URL url = new URL("http://172.16.0.2/foo.jsp"); HttpURLConnection urlConnection = (HttpURLConnection)url.openConnection(); response.setContentType(urlConnection.getContentType()); InputStream in = urlConnection.getInputStream(); OutputStream out = response.getOutputStream(); int readBytes; while ((readBytes = in.read(bytes, 0, bytes.length)) > 0) { out.write(bytes, 0, readBytes); } } }
一見すると問題のなさそうなServletだけど、通信先への接続状況によってはOutOfMemoryErrorが発生するかもしれない。このサンプルの場合、8行目で指定した外部通信先foo.jspがすぐに応答を返すのであれば、何も問題はナシ。しかし、foo.jspのレスポンスタイムが悪くなると、15行目が進まなくなる。このServletへのリクエストも増えてくると、OutOfMemoryErrorが発生し始める。
今回のケースでも、オブジェクトが開放されないという現象が発生する。開放されないオブジェクトとは、リクエスト処理が停止した際に使用中のオブジェクトだ。具体的に言うと、10240バイトのバイト配列(6行目)やHttpURLConnectionオブジェクト(9行目)、HttpServletRequestオブジェクト(3行目の引数)などが対象となる。
開放されないヒープ領域のサイズがそれほど大きくなかったとしても、リクエスト処理が完了するまではそのヒープ領域を占領し続ける。だから、レスポンスタイムが長くなれば長くなるほど、リクエスト数も増えていき、結果的にヒープ領域を埋め尽くしてしまう。
使用可能なスレッドが徐々に減っていってしまうことも問題になる。スレッドはリクエストを受けた時点で開始され、レスポンスを出力した時点で終了する。つまりレスポンスを返すことができなければスレッドは終了しない。一方システム上の制約もあって、Javaアプリケーションサーバーなら1サーバーで同時に300スレッドくらいが限界。となると、上限に行き着くのは意外なくらいあっという間だ。もし上限に届かなかったとしても使用可能なスレッド数が少なくなってしまうから、サーバーは徐々にリクエストを処理できなくなる。なんとなく遅いとか、なんとなく不安定・・・という状態になるはずだ。
外部通信には適切なタイムアウト設定が必須!
HTTPやJDBCのような外部通信は、通信先サーバーやネットワークの事情により、遅延したり接続できなかったりすることがある。そのため外部通信を行う際は、適切なタイムアウト値の設定が必要になる。
HttpURLConnetionクラスは、恐ろしいことにタイムアウトのデフォルト値が無期限!先ほどのサンプルでは、仮に通信先がレスポンスを永久に返さなかった場合、Servletも永久にレスポンスを返さない。こうなるとヒープ領域が開放するには、サーバーを停止するか、通信先サーバーを停止させるしかない・・・。
だからHTTP通信を行うとき、setConnectTimeoutメソッドやsetReadTimeoutメソッドでタイムアウトを明示的に設定しなければならない。修正後の8~10行目は次のようになる。
URL url = new URL("http://172.16.0.2/foo.jsp"); HttpURLConnection urlConnection = (HttpURLConnection)url.openConnection(); urlConnection.setConnectTimeout(10 * 1000); // 追加 urlConnection.setReadTimeout(10 * 1000); // 追加 response.setContentType(urlConnection.getContentType());
処理内容にもよるけど、タイムアウトは10秒とかせいぜい20秒で十分。レスポンスが遅いと感じたら10秒以内にもう一度再読み込みする人がほとんどだから、長い間待っていても仕方がない。
ちなみにこのsetConnectTimeoutメソッドとsetReadTimeoutメソッドは、Java5でやっと導入された。なかったらものすごい困るのに、何で今までなかったんだろう・・・?Java5より前のバージョンでは、HttpUrlConnectionクラスの代わりにApache Commons HttpClientを使用するのが吉。ただ、途中からHttpClientに乗り換える場合、コードの変更量が多くて大変だから、最初からどちらを使うのかを決めておいたほうがいいと思う。
まとめ
外部通信のタイムアウト値をデフォルト設定のまま使用していたり、無期限になったりしていないだろうか?タイムアウト値は単に処理時間だけに影響すると考えがち。だけど実際には、全体のパフォーマンス低下やシステムダウンをも引き起こす可能性がある。サーバーの安定運用には適切なタイムアウト設定が大切だ。
あと、外部通信以外でも同じようなことがいえる。たとえば繰り返し処理を行っている場合は、繰り返しの上限値(いわゆるストッパー)を設定し、一定時間内に必ず処理が完了するようにしておくのがいい。 通常は10回くらいの繰り返し処理だから大丈夫だろう、と思っていた箇所が、数十万・数百万になったとしたら・・・?
次回はOutOfMemoryErrorの完結編。OutOfMemoryErrorが発生したときの対処方法を紹介する。
(参考サイト)