使用 HttpClient 的连接泄漏状态 CLOSE_WAIT

Connections leaking with state CLOSE_WAIT with HttpClient

我们正在使用 JDK11 java.net.http HTTP 客户端从 API 获取数据。在我们收到响应后,连接在我们的服务器中保持打开状态,TCP 状态为 CLOSE_WAIT,这意味着客户端必须关闭连接。

来自RFC 793术语:

CLOSE-WAIT - represents waiting for a connection termination request from the local user.

这是我们的客户端代码,它作为无状态 REST API 在 Java 12 上的 WildFly 16 运行 上运行。我们不明白为什么会这样。

import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpClient.Version;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.http.HttpResponse.BodyHandlers;

public class SocketSandbox {

    public static void main(String[] args) throws Exception {
        HttpClient client = HttpClient.newBuilder().version(Version.HTTP_1_1).build();
        try (var listener = new ServerSocket(59090)) {
            System.out.println("Server is running...");
            while (true) {
                try (var socket = listener.accept()) {
                    HttpRequest request = HttpRequest
                            .newBuilder(URI.create("<remote_URL>"))
                            .header("content-type", "application/json").build();
                    HttpResponse<String> response = client.send(request, BodyHandlers.ofString());
                    var out = new PrintWriter(socket.getOutputStream(), true);
                    out.println(String.format("Response HTTP status: %s", response.statusCode()));
                }
            }
        }
    }

}

我们得到“状态代码”,表示已处理 http 响应。

使用相同的代码调用其他端点时连接正常。这似乎是我们正在调用的远程 API 的一个特殊问题,但我们仍然不明白为什么 Java HTTP 客户端保持连接打开。

我们尝试了 Windows 和 Linux 机器,甚至尝试了 WildfFly 之外的独立机器,但结果相同。在每个请求之后,即使是从我们的无状态客户端执行并收到响应,每个请求都保留为 CLOSE_WAIT 并且永远不会关闭。

如果我们关闭 Java 进程,连接将会消失。

HTTP客户端发送的

Headers:

connection: 'Upgrade, HTTP2-Settings','content-length': '0',
host: 'localhost:3000', 'http2-settings': 'AAEAAEAAAAIAAAABAAMAAABkAAQBAAAAAAUAAEAA',
upgrade: 'h2c',
user-agent': 'Java-http-client/12'

服务器 returns 响应 header:Connection: close

更新 (1)

我们尝试 fine-tune 实现中的池参数 class jdk.internal.net.http.ConnectionPool

没有解决问题

System.setProperty("jdk.httpclient.keepalive.timeout", "5"); // seconds
System.setProperty("jdk.httpclient.connectionPoolSize", "1");

更新 (2)

使用 Apache HTTP 连接会在 CLOSE_WAIT 状态下停留大约 90 秒,但在那之后它可以连接。

调用方法HttpGet.releaseConnection()强制立即关闭连接。

HttpClient client = HttpClients.createDefault();
HttpGet get = new HttpGet("https://<placeholderdomain>/api/incidents/list");
get.addHeader("content-type", "application/json");
HttpResponse response = client.execute(get);

// This right here did the trick
get.releaseConnection();

return response.getStatusLine().getStatusCode();

并且使用 OkHttp 客户端,它开箱即用,没有连接卡住。

OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
        .url("https://<placeholderdomain>/grb/sb/incidentes/listar")
        .header("content-type", "application/json").build();
Response response = client.newCall(request).execute();
return response.body().string();

我们仍在努力寻找如何让它在 java-http-client 中工作,这样我们就不必重写代码。

我不建议为每个新请求创建一个新客户端。这违背了 HTTP/2 允许在单个连接上多路复用请求的目的。

第二个就是两个属性:

System.setProperty("jdk.httpclient.keepalive.timeout", "5"); // seconds
System.setProperty("jdk.httpclient.connectionPoolSize", "1");

仅适用于 HTTP/1.1 个连接,不适用于 HTTP/2。还要注意这些属性只在 class 加载时读取一次。所以在之后设置它们 java.net.http classes 已加载将无效。

最后,在释放 HttpClient 之后可能需要一些时间才能关闭所有保持活动的连接 - 这样做的内部机制基本上依赖于 GC - 这对短暂的 HttpClients 不是很友好.

已提交并确认为实施中的错误。

https://bugs.openjdk.java.net/browse/JDK-8221395

更新

检查 JIRA 问题,它已在 JDK 13 中修复,并已反向移植到 11.0.6。 (不确定 12 个)