关闭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
找出是哪一步骤耗费了大量时间:
可以看到执行的关键步骤:
1.发起请求,耗时262ms
,没问题
2.读取文件头,耗时16ms
,很快
3.调用close
方法,耗时5589ms
!!
关闭响应为什么耗时这么久?
因为close
方法并不会断开连接,因为频繁的创建和销毁连接成本很高,所以HttpClient
会尽可能的复用链接。
因此即使调用了CloseableHttpResponse
的close()
方法,HttpClient
通常会尝试读取并处理完整的响应体,以确保连接在返回到连接池之前是干净的。这是因为未读取的响应体数据可能会导致连接保持活动状态,从而影响后续的请求。
所以说其实调用close()
方法后继续等待整个文件流直到结束!
是不是还挺坑的… 🙄🙄🙄
解决办法
- 通过自定义ConnectionReuseStrategy避免 HTTP 客户端复用连接
HttpClientBuilder
.create()
// 这将导致每次请求后都关闭连接,而不是尝试复用它。
.setConnectionReuseStrategy((response, context) -> false)
.build()
- 给请求头增加
Connection
:close
需要外调的HTTP接口也返回这个响应头如果响应头不包含,连接依然会被复用。
不可能要求所有渠道给我们返回这个响应头,所说这种方案无法采用。