CLOVER🍀

That was when it all began.

HttpClientとConnectionManagerとCLOSE_WAITと

JavaでHTTP通信をやろうとすると、必ずと言っていいほど登場する、Apache HttpClient4(以前は、Jakarta Commons HttpClient3)ですが、これでちょっとハマっていた事象を見かけたので、軽くメモを。

あ、その時に使われていたのは、Jakarta Commons HttpClient3の方です。んで、ソケットのクローズ漏れでハマってました。

これ、チュートリアルだけを信じて実装すると、ハマるんですよねぇ…。
http://hc.apache.org/httpclient-3.x/tutorial.html

チュートリアルの中に

      // Release the connection.
      method.releaseConnection();

みたいなことが書いてあって、いかにもコネクションをクローズしてくれそうな雰囲気がありますが、これは意味が違います。HttpClientが持つ、HttpConnectionManagerのインスタンスにコネクションを返却しますよ、という意味であって、別にコネクションを閉じてくれるわけではないんですよね。

割と当たり前の情報?

これを検証するために、以下のようなコードを用意してみました。
build.sbt

name := "httpclient3-example"

version := "0.0.1"

organization := "littlewings"

libraryDependencies += "commons-httpclient" % "commons-httpclient" % "3.1"

HttpClient3Example.java

import java.io.IOException;

import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpException;
import org.apache.commons.httpclient.HttpMethod;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.httpclient.methods.GetMethod;

public class HttpClient3Example {
    private static final String URL = "http://localhost:8080/index.html";
    private static final int EXEC_COUNT = 10;

    public static void main(String[] args) {
        for (int i = 0; i < EXEC_COUNT; i++) {
            executeHttpMethod();

            try {
                long sleepTime = 5 * 1000L;
                System.out.printf("Sleeping %dmsec...%n", sleepTime);
                Thread.sleep(sleepTime);
            } catch (InterruptedException e) { }
        }
    }

    public static void executeHttpMethod() {
        HttpClient client = new HttpClient();
        HttpMethod method = new GetMethod(URL);

        try {
            System.out.printf("Access URL[%s]%n", URL);
            int statusCode = client.executeMethod(method);

            String body = new String(method.getResponseBody(), "UTF-8");
            System.out.printf("Status Code = %d%n", statusCode);
            System.out.printf("Response Body = %s%n", body);
        } catch (HttpException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            method.releaseConnection();
        }
    }
}

んで、前回作成した簡易HTTPサーバを起動しまして

$ groovy LightHttpd.groovy 
LightHttpd Startup. Sat Oct 13 17:18:30 JST 2012

実行してみます。

$ sbt
[info] Set current project to httpclient3-example (in build file:/xxxxx/httpclient3-example/)
> run
[info] Compiling 1 Java source to /xxxxx/httpclient3-example/target/scala-2.9.2/classes...
[info] Running HttpClient3Example 
Access URL[http://localhost:8080/index.html]
Status Code = 200
Response Body = Accessed URL = /index.html
Accessed Date = Sat Oct 13 17:19:10 JST 2012

Sleeping 5000msec...
Access URL[http://localhost:8080/index.html]
Status Code = 200
Response Body = Accessed URL = /index.html
Accessed Date = Sat Oct 13 17:19:15 JST 2012

 〜省略〜

Access URL[http://localhost:8080/index.html]
Status Code = 200
Response Body = Accessed URL = /index.html
Accessed Date = Sat Oct 13 17:19:55 JST 2012

Sleeping 5000msec...
[success] Total time: 53 s, completed 2012/10/13 17:20:00

この間に、別のコンソールでnetstatを使ってソケットの状態を監視してみます。

$ netstat -an | grep 127.0.0.1:8080 
tcp6       0      0 127.0.0.1:50398         127.0.0.1:8080          ESTABLISHED
tcp6       0      0 127.0.0.1:8080          127.0.0.1:46391         FIN_WAIT2  
tcp6       0      0 127.0.0.1:42644         127.0.0.1:8080          ESTABLISHED
tcp6       0      0 127.0.0.1:8080          127.0.0.1:47286         FIN_WAIT2  
tcp6       0      0 127.0.0.1:8080          127.0.0.1:56907         FIN_WAIT2  
tcp6       0      0 127.0.0.1:40523         127.0.0.1:8080          ESTABLISHED
tcp6       0      0 127.0.0.1:8080          127.0.0.1:60165         ESTABLISHED
tcp6       1      0 127.0.0.1:56907         127.0.0.1:8080          CLOSE_WAIT 
tcp6       0      0 127.0.0.1:58088         127.0.0.1:8080          ESTABLISHED
tcp6       0      0 127.0.0.1:60165         127.0.0.1:8080          ESTABLISHED
tcp6       1      0 127.0.0.1:47286         127.0.0.1:8080          CLOSE_WAIT 
tcp6       0      0 127.0.0.1:8080          127.0.0.1:53817         FIN_WAIT2  
tcp6       1      0 127.0.0.1:46391         127.0.0.1:8080          CLOSE_WAIT 
tcp6       0      0 127.0.0.1:8080          127.0.0.1:50398         ESTABLISHED
tcp6       0      0 127.0.0.1:8080          127.0.0.1:40523         ESTABLISHED
tcp6       0      0 127.0.0.1:8080          127.0.0.1:58088         ESTABLISHED
tcp6       1      0 127.0.0.1:53817         127.0.0.1:8080          CLOSE_WAIT 
tcp6       0      0 127.0.0.1:8080          127.0.0.1:42644         ESTABLISHED

サーバ側とクライアント側の接続が見えていますが、バラバラとESTABLISHED、CLOSE_WAIT、FIN_WAIT2となってますね。

もう少し待つと、CLOSE_WAITとFIN_WAIT2だけになります。

$ netstat -an | grep 127.0.0.1:8080 
tcp6       1      0 127.0.0.1:50398         127.0.0.1:8080          CLOSE_WAIT 
tcp6       0      0 127.0.0.1:8080          127.0.0.1:46391         FIN_WAIT2  
tcp6       1      0 127.0.0.1:42644         127.0.0.1:8080          CLOSE_WAIT 
tcp6       0      0 127.0.0.1:8080          127.0.0.1:47286         FIN_WAIT2  
tcp6       0      0 127.0.0.1:8080          127.0.0.1:56907         FIN_WAIT2  
tcp6       1      0 127.0.0.1:40523         127.0.0.1:8080          CLOSE_WAIT 
tcp6       1      0 127.0.0.1:38467         127.0.0.1:8080          CLOSE_WAIT 
tcp6       0      0 127.0.0.1:8080          127.0.0.1:60165         FIN_WAIT2  
tcp6       1      0 127.0.0.1:56907         127.0.0.1:8080          CLOSE_WAIT 
tcp6       1      0 127.0.0.1:58088         127.0.0.1:8080          CLOSE_WAIT 
tcp6       1      0 127.0.0.1:60165         127.0.0.1:8080          CLOSE_WAIT 
tcp6       1      0 127.0.0.1:47286         127.0.0.1:8080          CLOSE_WAIT 
tcp6       0      0 127.0.0.1:8080          127.0.0.1:53817         FIN_WAIT2  
tcp6       1      0 127.0.0.1:46391         127.0.0.1:8080          CLOSE_WAIT 
tcp6       0      0 127.0.0.1:8080          127.0.0.1:50398         FIN_WAIT2  
tcp6       0      0 127.0.0.1:8080          127.0.0.1:38467         FIN_WAIT2  
tcp6       0      0 127.0.0.1:8080          127.0.0.1:40523         FIN_WAIT2  
tcp6       0      0 127.0.0.1:8080          127.0.0.1:58088         FIN_WAIT2  
tcp6       1      0 127.0.0.1:53817         127.0.0.1:8080          CLOSE_WAIT 
tcp6       0      0 127.0.0.1:8080          127.0.0.1:42644         FIN_WAIT2

最後には、CLOSE_WAITだけになります。

$ netstat -an | grep 127.0.0.1:8080 
tcp6       1      0 127.0.0.1:50398         127.0.0.1:8080          CLOSE_WAIT 
tcp6       1      0 127.0.0.1:42644         127.0.0.1:8080          CLOSE_WAIT 
tcp6       1      0 127.0.0.1:40523         127.0.0.1:8080          CLOSE_WAIT 
tcp6       1      0 127.0.0.1:38467         127.0.0.1:8080          CLOSE_WAIT 
tcp6       1      0 127.0.0.1:56907         127.0.0.1:8080          CLOSE_WAIT 
tcp6       1      0 127.0.0.1:58088         127.0.0.1:8080          CLOSE_WAIT 
tcp6       1      0 127.0.0.1:60165         127.0.0.1:8080          CLOSE_WAIT 
tcp6       1      0 127.0.0.1:47286         127.0.0.1:8080          CLOSE_WAIT 
tcp6       1      0 127.0.0.1:46391         127.0.0.1:8080          CLOSE_WAIT 
tcp6       1      0 127.0.0.1:53817         127.0.0.1:8080          CLOSE_WAIT

さらにず〜っと放ったらかしにしておくと、netstatからは消えますがlsofで見ると「can't identify protocol」と表記されるような状態になってしまいます。

こうなると、HTTP通信を実行する度にファイルディクリプタを使用量が増え続け、Tomcat上で動かすようなサーバ系のプログラムとして組んでいた場合は、Too many open filesエラーを引いてサーバの停止に追い込まれます。

まあ、結論だけ言うとソケットをクローズしていないことが原因です。

ソケットがFIN_WAIT2として見えているのはHTTPサーバ側の方で、CLOSE_WAITとなっているのはクライアント側の方です。で、CLOSE_WAITは相手はクローズしたけど、自分のクローズ命令を待っている状態なので、要は「早く自分の持っているソケットをクローズしましょうね」と言っているわけです。

なので、これに対処するには以下のどちらかの修正を行います。

1)HttpConnectionManagerの持つ、アイドル状態のコネクションをクローズする
HttpMethod#releaseConnectionでHttpConnectionManagerのプールに戻されたコネクションをクローズするため、finally句の部分を以下のように修正します。

        } finally {
            method.releaseConnection();
            client.getHttpConnectionManager().closeIdleConnections(0);
        }

2)HTTPヘッダに「Connection: close」を追加する

        HttpClient client = new HttpClient();
        HttpMethod method = new GetMethod(URL);
        method.addRequestHeader("Connection", "close");

このいずれかの対処を行うと、HttpClientでソケットのクローズを行ってくれます。

実行すると、今度はTIME_WAITに遷移するようになります。

$ netstat -an | grep 127.0.0.1:8080
tcp6       0      0 127.0.0.1:57912         127.0.0.1:8080          TIME_WAIT  
tcp6       0      0 127.0.0.1:44164         127.0.0.1:8080          TIME_WAIT  
tcp6       0      0 127.0.0.1:39976         127.0.0.1:8080          TIME_WAIT  
tcp6       0      0 127.0.0.1:43772         127.0.0.1:8080          TIME_WAIT  
tcp6       0      0 127.0.0.1:55436         127.0.0.1:8080          TIME_WAIT  
tcp6       0      0 127.0.0.1:45129         127.0.0.1:8080          TIME_WAIT  
tcp6       0      0 127.0.0.1:51501         127.0.0.1:8080          TIME_WAIT  
tcp6       0      0 127.0.0.1:57594         127.0.0.1:8080          TIME_WAIT  
tcp6       0      0 127.0.0.1:48916         127.0.0.1:8080          TIME_WAIT  
tcp6       0      0 127.0.0.1:38695         127.0.0.1:8080          TIME_WAIT

TIME_WAITなので、少し放っておくと開放されます。

最後に、Apache HttpClient4版で。

Quick Startを見て実装すると
http://hc.apache.org/httpcomponents-client-ga/quickstart.html

こんな感じになって
build.sbt

name := "httpclient4-example"

version := "0.0.1"

libraryDependencies += "org.apache.httpcomponents" % "httpclient" % "4.2.1"

HttpClient4Example.java

import java.io.IOException;

import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.util.EntityUtils;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.DefaultHttpClient;

public class HttpClient4Example {
    private static final String URL = "http://localhost:8080/index.html";
    private static final int EXEC_COUNT = 10;

    public static void main(String[] args) {
        for (int i = 0; i < EXEC_COUNT; i++) {
            executeHttpMethod();

            try {
                long sleepTime = 5 * 1000L;
                System.out.printf("Sleeping %dmsec...%n", sleepTime);
                Thread.sleep(sleepTime);
            } catch (InterruptedException e) { }
        }
    }

    public static void executeHttpMethod() {
        HttpClient client = new DefaultHttpClient();
        HttpGet getMethod = new HttpGet(URL);

        try {
            System.out.printf("Access URL[%s]%n", URL);
            HttpResponse response = client.execute(getMethod);

            HttpEntity entity = response.getEntity();
            String body = EntityUtils.toString(entity, "UTF-8");

            System.out.printf("Status Line = %s%n", response.getStatusLine());
            System.out.printf("Response Body = %s%n", body);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            getMethod.releaseConnection();
        }
    }
}

このコードだと、ソケットがCLOSE_WAITで止まってしまい、やっぱりハマります。

ここは、Examplesの「Manual connection release」
http://hc.apache.org/httpcomponents-client-ga/examples.html
もしくはTutrialの「Connection manager shutdown」
http://hc.apache.org/httpcomponents-client-ga/tutorial/html/connmgmt.html#d5e635
のどちらかを参考に、finally句を以下のように修正します。

        } finally {
            // getMethod.releaseConnection(); => こっちは要らない
            client.getConnectionManager().shutdown();
        }

Apache HttpClient4の場合は、HttpClientインターフェースのAPIにも書いているんですが…
http://hc.apache.org/httpcomponents-client-ga/httpclient/apidocs/org/apache/http/client/HttpClient.html
なんでQuick StartはreleaseConnectionの方しか書いてないんでしょうね?

割と当たり前の話かもしれませんが、起動して処理が終わるとプロセスも終了するバッチプログラムならいいんですが、サーバ系だとけっこう問題になると思うので一応書いてみます。