RestEasy client を使用した HTTP 要求を行うと、TCP コネクションが長時間にわたり残存する問題が発生することがあります。 RestEasy の公式ドキュメントを読めば「HTTP コネクションは適切に release し、適切に close しなければならない」ということは分かるのですが、RestEasy にてどのように HTTP コネクションが管理されているのか良く分からなかったので、そのあたりを調べてみました。
RestEasy と HttpComponents
RestEasy が HTTP 要求を行う際、デフォルトでは、HttpComponents を内部で使用することになっています。
このあたりは、RestEasy における HTTP 要求を示すクラスである ClientRequest
の実装を見れば分かりやすいでしょう。
何も意識しないと、以下のコンストラクタを使うことになりますが、ここではデフォルトの ClientExecutor
を getDefaultExecutor()
で生成しています。
public ClientRequest(String uriTemplate) { this(uriTemplate, getDefaultExecutor()); }
では、その実装はというと、以下のように、ApacheHttpClient4Executor
を使用するようになっていることがわかります。
private static String defaultExecutorClasss = "org.jboss.resteasy.client.core.executors.ApacheHttpClient4Executor"; public static ClientExecutor getDefaultExecutor() { try { Class<?> clazz = Thread.currentThread().getContextClassLoader().loadClass(defaultExecutorClasss); return (ClientExecutor) clazz.newInstance(); } catch (Exception e) { throw new RuntimeException(e); } }
あとはこの ApacheHttpClient4Executor
の実装を見れば、httpClient
に代表される httpComponents のクラスが山積みです。
httpComponent における HTTP コネクション管理
HTTP での通信を行う際は当然ながら HTTP コネクションを管理する必要があります。
httpComponent においては、この責務は ClientConnectionManager
Interface の実装クラスが負うということになっています。具体的には、ClientConnectionManager
は
- HTTP コネクションの作成
- Keep Alive な HTTP コネクションの管理
- 個々のスレッドに対してどの HTTP コネクションを使わせるのかの制御
を行うっていう Interface となり、その実装クラスとして、後述する BasicClientConnectionManager
や PoolingClientConnectionManager
なんてものが存在しています。
RestEasy のデフォルト
さて、RestEasy に戻りますが、何も意識しないで RestEasy を使用すると、ClientExecutor
としては先述のように ApacheHttpClient4Executor
が使用されます。
そして、このコンストラクタでは、以下のように httpClient
として DefaultHttpClient
が生成されます。
public ApacheHttpClient4Executor() { this.httpClient = new DefaultHttpClient(); this.createdHttpClient = true; checkClientExceptionMapper(); }
DefaultHttpClient
は、HTTP コネクションを管理する ClientConnectionManager
として、 BasicClientConnectionManager
を使う実装になっています。
BasicClientConnectionManager
は HTTP コネクションを 1 本だけ管理するという ConnectionManager であって、シングルスレッドのみで使われることを意図したものになっています。
これまでのまとめ
これまでの RestEasy の挙動を簡単にまとめると、
ClientRequest
を直接使用して HTTP 要求を行う際、(ApacheHttpClient4Executor が都度生成される結果として) 都度 httpClient が作成され、- その中で、
BasicClientConnectionManager
が作られる
という振舞いになります。
実装1: BasicClientConnectionManager
で close/release しない
たとえば以下のようなコードで 20 スレッドで HTTP 要求を行うと、HTTP コネクションが 20 本作成されるとともに、個々の HTTP コネクションが keep-alive のタイムアウト時間が経過するまで開放されない(5 秒間の間、20 本の ESTABLISHED な TCP コネクションが作成され、それが 5 秒後に CLOSE_WAIT に遷移し、当分の間残存する。
public static void main(String... args) throws Exception { final int threadNum = 20; final String uri = "http://192.168.99.100:8080"; // Docker コンテナ上の Apache Listen ポート ExecutorService service = Executors.newFixedThreadPool(threadNum); for (int i = 0; i < threadNum; ++i) { service.submit(new GetRunnable(uri)); } service.shutdown(); // netstat 観測用の時間 Thread.sleep(10000L); } public static class GetRunnable implements Runnable { private String uri; public GetRunnable(String uri) { this.uri = uri; } public void run() { try { new ClientRequest(uri).get(); } catch (Exception e) { e.printStackTrace(); } } }
実装2: PoolingClientConnectionManager
で close/release しない
次に PoolingClientConnectionManager
を使用してみる。この ConnectionManager は、その名前の通り、HTTP コネクションをプールとして管理する。
RestEasy が (正しくは httpComponent が)デフォルトで使用する BasicClientConnectionManager
との違いは、BasicClientConnectionManager
は個々の実行スレッドがそれぞれインスタンスを保持することを意図している一方で、PoolingClientConnectionManager
はマルチスレッドで共有されることを(通常は)意図している点となる。
PoolingClientConnectionManager
を使用するためには、次のように書けば良い。
public static void main(String... args) throws Exception { final int threadNum = 20; final String uri = "http://192.168.99.100:8080"; ClientConnectionManager connManager = new PoolingClientConnectionManager(); HttpClient client = new DefaultHttpClient(connManager); ExecutorService service = Executors.newFixedThreadPool(threadNum); for (int i = 0; i < threadNum; ++i) { service.submit(new GetRunnable(uri, client)); } service.shutdown(); } public static class GetRunnable implements Runnable { private String uri; private HttpClient client; public GetRunnable(String uri, HttpClient client) { this.uri = uri; this.client = client; } public void run() { ClientExecutor executor = new ApacheHttpClient4Executor(client); try { new ClientRequest(uri, executor).get(); } catch (Exception e) { e.printStackTrace(); } } }
実際にこれを実行すると、非常に面白いことが起こる。 20 本のスレッドを同時実行しているはずなのに、Apache のアクセスログに記録されるのは 2 リクエストのみである。おい残りの 18 本はどこに行った。
192.168.99.1 - - [23/Apr/2016:13:03:52 +0000] "GET / HTTP/1.1" 200 12 192.168.99.1 - - [23/Apr/2016:13:03:52 +0000] "GET / HTTP/1.1" 200 12
また、この後で netstat を見ると、以下のように 2 本のコネクションが CLOSE_WAIT 状態のままとなっている。
tcp4 0 0 192.168.99.1.64268 192.168.99.100.8081 CLOSE_WAIT tcp4 0 0 192.168.99.1.64267 192.168.99.100.8081 CLOSE_WAIT
種明かしをすると、以下の 2 つに起因して、残りの 18 スレッドは HTTP コネクション待ちの状態になっている。
- コネクションを適切に release していない
PoolingClientConnectionManager
のプール数の上限は、デフォルトだとトータルで 20 本、かつ、1 つの宛先ホスト毎に 2 本という制限がある
1 つ目の問題を解決するためには、ClientResponse#releaseConnection
を呼び出せば良い。
releaseConnection
は、HTTP コネクションをプールに返却するメソッドである。これにより HTTP コネクションが他のスレッドから使用できるようになるため、先ほどのような問題は解消される。
diff --git a/src/main/java/com/kiririmode/blog/http/client/ResteasyClient.java b/src/main/java/com/kiririmode/blog/http/client/ResteasyClient.java index 34198f6..e54889a 100644 --- a/src/main/java/com/kiririmode/blog/http/client/ResteasyClient.java +++ b/src/main/java/com/kiririmode/blog/http/client/ResteasyClient.java @@ -9,6 +9,7 @@ import org.apache.http.impl.client.DefaultHttpClient; import org.apache.http.impl.conn.PoolingClientConnectionManager; import org.jboss.resteasy.client.ClientExecutor; import org.jboss.resteasy.client.ClientRequest; +import org.jboss.resteasy.client.ClientResponse; import org.jboss.resteasy.client.core.executors.ApacheHttpClient4Executor; public class ResteasyClient { @@ -41,10 +42,15 @@ public class ResteasyClient { public void run() { ClientExecutor executor = new ApacheHttpClient4Executor(client); + ClientResponse<?> response = null; try { - new ClientRequest(uri, executor).get(); + response = new ClientRequest(uri, executor).get(); } catch (Exception e) { e.printStackTrace(); + } finally { + if (response != null) { + response.releaseConnection(); + } } }
2 つ目の問題に対しては、単純にプールの設定を変更してやれば良い。
diff --git a/src/main/java/com/kiririmode/blog/http/client/ResteasyClient.java b/src/main/java/com/kiririmode/blog/http/client/ResteasyClient.java index e54889a..14ae85b 100644 --- a/src/main/java/com/kiririmode/blog/http/client/ResteasyClient.java +++ b/src/main/java/com/kiririmode/blog/http/client/ResteasyClient.java @@ -18,7 +18,9 @@ public class ResteasyClient { final int threadNum = 20; final String uri = "http://192.168.99.100:8081"; - ClientConnectionManager connManager = new PoolingClientConnectionManager(); + PoolingClientConnectionManager connManager = new PoolingClientConnectionManager(); + connManager.setDefaultMaxPerRoute(10); + connManager.setMaxTotal(100); HttpClient client = new DefaultHttpClient(connManager); ExecutorService service = Executors.newFixedThreadPool(threadNum);
実装3: PoolingClientConnectionManager
で close/release する
残りの問題は、CLOSE_WAIT が残り続ける問題なのだけれど、これは単純に ConnectionManager に対して HTTP コネクションの close を指示すれば良い。
これを行うのは ClientConnectionManager#shutdown
メソッドである。
以下のように呼び出すことにより、TCP コネクションが CLOSE_WAIT 状態で残存することはなくなった。
diff --git a/src/main/java/com/kiririmode/blog/http/client/ResteasyClient.java b/src/main/java/com/kiririmode/blog/http/client/ResteasyClient.java index 14811f9..b229cde 100644 --- a/src/main/java/com/kiririmode/blog/http/client/ResteasyClient.java +++ b/src/main/java/com/kiririmode/blog/http/client/ResteasyClient.java @@ -2,6 +2,7 @@ package com.kiririmode.blog.http.client; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; import org.apache.http.client.HttpClient; import org.apache.http.impl.client.DefaultHttpClient; @@ -28,7 +29,9 @@ public class ResteasyClient { } service.shutdown(); - Thread.sleep(10000L); + service.awaitTermination(10L, TimeUnit.SECONDS); + + client.getConnectionManager().shutdown(); } public static class GetRunnable implements Runnable {