使用HttpClient的close()引发的问题

使用HttpClient的close()引发的问题

马草原 962 2024-03-13

关闭Response耗时问题

需求很简单:根据渠道标识符,获取最新的清算文件结构。渠道方提供了清算文件的下载地址,文件格式为 csv。由于我们只需要获取清算文件的结构,因此只需读取 CSV 的首行并将其转换为 JSON 对象即可。虽然渠道方提供的清算文件示例各不相同,但我们只关心表头,而样例数据的具体内容并不重要。

我的想法很简单,就是使用 HttpClient 请求对应的接口,读取表头后立即关闭连接。由于只需要读取很少的字节数,即使一些渠道方不在东南亚地区,速度也不会太慢(因为并没有等待整个文件流)。然而,实际事与愿违。

功能方面没有问题,确实可以正确获取表头,但是请求速度很慢。经过测试,发现仅仅读取表头和下载完整文件所消耗的时间似乎是一致的…

这很奇怪,按理说只读取文件头就关闭了流应该很快就会结束,但是实际测试发现一个请求花费了长达6秒来处理。这就很诡异了。

业务代码不能公开,下面是伪代码:

原代码是用的try-with-resources来自动关闭连接的 这里只是为了能更直观的看到是调用了close()就是这个cloes引发的问题

  CloseableHttpClient httpClient = HttpClients.createDefault();
  HttpGet httpGet = new HttpGet(channelUrl);
  CloseableHttpResponse response = null;
  try {
    response = httpClient.execute(httpGet);

    // 读取头部字段并将其转换为Json对象
    JsonObject obj = response...

      return obj;
  } finally {
    if (response != null) {
      response.close();
    }
  }

分析原因

使用Arthas 来分析一下这个方法的trace 找出是哪一步骤耗费了大量时间:
Arthas

可以看到执行的关键步骤:
1.发起请求,耗时262ms,没问题
2.读取文件头,耗时16ms,很快
3.调用close方法,耗时5589ms!!

关闭响应为什么耗时这么久?

因为close方法并不会断开连接,因为频繁的创建和销毁连接成本很高,所以HttpClient会尽可能的复用链接。

因此即使调用了CloseableHttpResponseclose() 方法,HttpClient通常会尝试读取并处理完整的响应体,以确保连接在返回到连接池之前是干净的。这是因为未读取的响应体数据可能会导致连接保持活动状态,从而影响后续的请求。

所以说其实调用close()方法后继续等待整个文件流直到结束!

是不是还挺坑的… 🙄🙄🙄

解决办法

  1. 通过自定义ConnectionReuseStrategy避免 HTTP 客户端复用连接
HttpClientBuilder
        .create()
  			// 这将导致每次请求后都关闭连接,而不是尝试复用它。
        .setConnectionReuseStrategy((response, context) -> false)
        .build()
  1. 给请求头增加 Connection : close

需要外调的HTTP接口也返回这个响应头如果响应头不包含,连接依然会被复用。

不可能要求所有渠道给我们返回这个响应头,所说这种方案无法采用。