close_wait troubleshooting

问题背景

最近, 有好几个业务反馈试用 apache httpclient 时出现了问题, 使用的版本是 4.3.1, 表现出来的现象就是机器的连接监控出现大量的 close_wait. 只有在 gc 的时候, 这些大量的连接才会被回收释放.

问题分析

tcp 四次挥手

要分析 close_wait 形成的原因, 首先需要知道 tcp 连接在关闭时的流程, 如图:

tcp 连接的关闭可以由连接双方任何一方发起, 从图中可知, close_wait 的状态发生在被动关闭方收到 fin, 并发出 ack 之后. 而在被动关闭方发出 fin 以后, 状态则会转为 last_ack. close_wait 的状态可能会永远持续下去, 直到连接被 close 状态变为 last_ack.

由上我们已经知道, 造成这个现象的原因就是连接被对方关闭, 而自己没有发出 fin. 从 api 层面来讲, 就是没有调用 socket 的 close()

代码分析

查看业务代码, 发现业务的请求逻辑如下

public class RequestUtils {

    public static String get(String url) {
        CloseableHttpClient client = HttpClients.createDefault();
        HttpGet get = new HttpGet(url);
        CloseableHttpResponse response = null;
        try {
            response = client.execute(get);
            return EntityUtils.toString(response.getEntity());
        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            if (response != null) {
                try {
                    response.close();
                } catch (IOException e1) {
                    // ignored

                }
            }
        }
    }
}

看起来似乎没有什么问题, 最后已经调用了 response.close(), 看起来连接应该已经被关闭了, 但真的是这样吗? 既然连接是处于 close_wait 状态, 那么说明代码并有调用 socket.close(). 跟踪 response.close() 的代码, 到底层后会发现, close 时有一个判断逻辑, 即 Response 的 header 中有 Connection: keep-alive, 则连接不会被关闭, 以便下次请求同一个网站的时候复用连接. 所以, 问题就出在这.

问题原因

连接没有主动关闭, 是造成 close_wait 的直接原因. 那么更本原因是什么? 以前我们总是会说, httpclient 要复用, 连接要复用, 到底是为什么, 不复用会造成什么潜在的问题, 其实就是这个原因. 以上业务代码的问题就在于每次调用这个方法都会创建一个新的 HttpClient, 他的问题在于:
1. 浪费资源, 每次需要重新初始化 client
2. 由于 keep-alive 的存在 (http/1.1 协议默认开启 keep-alive), 导致连接不会被关闭, 只会被回收
3. 局部方法内生成 client, 在 finally 内没有关闭 client, 导致连接的泄露
实际上, 上面出现 close_wait 的应该是在 server 那边 hang 住连接超过一定时间以后超时, server 才关闭连接的, 因为既然 server 返回 keep-alive, 说明 server 实际上也期待客户端能复用连接, 却没想到客户端把连接给泄露了.

解决问题

知道了问题原因, 如何改进业务代码? 改进之前, 应该清楚使用 httpclient 的一些原则:
1. 复用 HttpClient 避免重复初始化
2. 使用连接池管理连接, 避免每次新建连接
3. 设置合理的连接池大小, 以及连接存活时间
改进后的代码如下:

public class RequestUtils2 {

    private static final CloseableHttpClient CLIENT;

    static {
        PoolingHttpClientConnectionManager connectionManager = 
                new PoolingHttpClientConnectionManager(60, TimeUnit.SECONDS);
        connectionManager.setMaxTotal(1080);
        connectionManager.setDefaultMaxPerRoute(128);

        CLIENT = HttpClientBuilder.create()
                .setConnectionManager(connectionManager)
                .build();
    }

    public static String get(String url) {
        HttpGet get = new HttpGet(url);
        CloseableHttpResponse response = null;
        try {
            response = CLIENT.execute(get);
            return EntityUtils.toString(response.getEntity());
        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            if (response != null) {
                try {
                    response.close();
                } catch (IOException e1) {
                    // ignored

                }
            }
        }
    }
}

测试代码

有想测试这个问题的同学, 可以用以下代码启动一个 httpserver

public class SimpleHttpServer {

    public static void main(String[] args) throws Exception {
        HttpServer server = HttpServer.create(new InetSocketAddress(8000), 0);
        server.createContext("/test", new MyHandler());
        server.setExecutor(null); // creates a default executor

        server.start();
    }

    static class MyHandler implements HttpHandler {

        @Override
        public void handle(HttpExchange t) throws IOException {
            String response = "This is the response";
            Headers headers = t.getResponseHeaders();
            headers.add("Connection", "keep-alive");
            t.sendResponseHeaders(200, response.length());
            OutputStream os = t.getResponseBody();
            os.write(response.getBytes());
            os.close();
        }
    }
}

然后使用错误代码请求

public class RequestUtilsTest {

    @Test
    public void testRequest() throws Exception {
        String s = RequestUtils.get("http://localhost:8000/test");
        System.out.println(s);
        Thread.sleep(Long.MAX_VALUE);
    }
}

等到请求结束后, 手动结束 httpserver, 再用 sudo netstat -antup (Mac 使用 lsof -i -P) 查看一下端口状态, 你会发现如下信息

java      45896     liuzhenwei   48u  IPv6 0x8c89b842b9402bf3      0t0    TCP localhost:52519->localhost:8000 (CLOSE_WAIT)

题外话

有人会疑问, server 端关闭连接后, client 需要手动关闭连接吗? 关于这一点, 可以参考写简单的 socket 业务逻辑时的代码, 如果被动关闭方需要发出 fin, 是需要调用 socket.close() 的. 例如, 一般我们会这么写:

    Socket socket = new Socket("localhost", 9000);
    InputStream in = socket.getInputStream();
    while (true) {
        int read = in.read();
        if (read == -1) { // 这个 -1 就是 server close 连接后发出的

            in.close();
            break;
        }
        System.out.println(read);
    }

也就是说, 主动关闭方如果 close 了, 那么被动关闭方这边状态会变为 close_wait, 同时收到 -1, 然后我们需要手动调用 socket.close().