网站首页 > 技术文章 正文
http连接泄露,httpClient到底要关闭哪些资源
问题描述
现场应用运行一段时间就会卡住没有响应,导出线程信息后,有700多个线程卡在dubbo rpc调用上,难道是发现了dubbo的一个就隐藏bug?
at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:108)
at net.dubboclub.restful.client.HttpInvoker.post(HttpInvoker.java:47)
at net.dubboclub.restful.client.RestfulInvoker.invoke(RestfulInvoker.java:77)
at com.alibaba.dubbo.rpc.protocol.dubbo.filter.FutureFilter.invoke(FutureFilter.java:53)
at com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper$1.invoke(ProtocolFilterWrapper.java:91)
at com.alibaba.dubbo.monitor.support.MonitorFilter.invoke(MonitorFilter.java:75)
at com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper$1.invoke(ProtocolFilterWrapper.java:91)
at com.alibaba.dubbo.rpc.filter.ConsumerContextFilter.invoke(ConsumerContextFilter.java:48)
at com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper$1.invoke(ProtocolFilterWrapper.java:91)
at com.alibaba.dubbo.rpc.listener.ListenerInvokerWrapper.invoke(ListenerInvokerWrapper.java:74)
at com.alibaba.dubbo.rpc.protocol.InvokerWrapper.invoke(InvokerWrapper.java:53)
at com.alibaba.dubbo.rpc.cluster.support.FailoverClusterInvoker.doInvoke(FailoverClusterInvoker.java:77)
at com.alibaba.dubbo.rpc.cluster.support.AbstractClusterInvoker.invoke(AbstractClusterInvoker.java:227)
at com.alibaba.dubbo.rpc.cluster.support.wrapper.MockClusterInvoker.invoke(MockClusterInvoker.java:72)
at com.alibaba.dubbo.rpc.proxy.InvokerInvocationHandler.invoke(InvokerInvocationHandler.java:52)
at com.alibaba.dubbo.common.bytecode.proxy0.getAjxx(proxy0.java)
排查思路
700多个线程卡在dubbo rpc调用,大概率是出现了连接泄露,调用栈是业务系统代码 -> dubbo rpc -> dubboclub HttpInvoker.post -> apache CloseableHttpClient.execute,所以应该是出现了http连接泄露。怎么排查这个问题,从调用链路上看,经过了四步,那是不是这四部分都得排查呢?我既然这么问,那就肯定不是。
?业务系统代码调用dubbo rpc时,业务系统代码是不用显示调用任何rpc相关的代码的,所在的资源调用和释放都是在dubbo中封装好的,所以和业务系统代码应该没啥关系?dubbo rpc,做为一个广泛使用的rpc框架,出现资源泄露这种低级问题的可能性不大。?注:以前用dubbo时,如果zookeeper宕了,应用启动时,会一直卡住,因为dubbo默认连接zookeeper的超时时间是Integer.MAX_VALUE毫秒,换算一下为24天,这个超时时间的设置确实让人费解,还不如默认10S啥的,超时报错。或者是设计者另有深意??dubboclub HttpInvoker.post,这个包名起的,看着像是dubbo提供的又不像,去dubbo官网找了一下,没找到这个包和对应的代码,上github上一顿搜索,发现其是一个个人做的dubbo扩展,用于给dubbo提供restful支持,已经常年没有更新了。?apache CloseableHttpClient.execute,同dubbo,apache自己是不太可能出现连接泄露的,如果出现了,那就是你的业务代码没写好,该释放没释放。
这么一分析,dubboclub HttpInvoker.post嫌疑最大,直接上github上去扒代码。
dubboclub源码赏析
核心代码如下
public static byte[] post(String url,byte[] requestContent,Map<String,String> headerMap) throws IOException {
HttpPost httpPost = new HttpPost(url);
CloseableHttpResponse response = httpclient.execute(httpPost);
int responseCode = response.getStatusLine().getStatusCode();
if(responseCode==200){
HttpEntity responseEntity = response.getEntity();
if(responseEntity!=null){
return EntityUtils.toByteArray(responseEntity);
}
}else if(responseCode==404){
throw new RpcException(RpcException.UNKNOWN_EXCEPTION,"not found service for url ["+url+"]");
}else if(responseCode==500){
throw new RpcException(RpcException.NETWORK_EXCEPTION,"occur an exception at server end.");
}
return null;
}
?就这么十来行代码,一看问题就很清晰了,CloseableHttpResponse就没有关闭过,导致出现了连接泄露。但是又有一个问题,这段代码运行了挺长时间了,而且调用频率也很高,要出问题早就出问题了,为什么现在才暴露出来呢。代码逻辑是如果http status是200,则通过EntityUtils.toByteArray对responseEntity进行消费,如果是404或者500则直接throw exception,既不消费也不关闭。?注:该作者写代码不严谨,http stats又不只有这三种情况,按照MDN的定义,服务端也可能返回201 Created,202 Accepted或者204 No Content,这样的话既没有日志,也没有报错,直接返回null,连接也泄露了,实属大坑。
难道是EntityUtils.toByteArray内部会进行连接释放,于是写了段测试代码进行调试验证,发现在通过EntityUtils进行消费时,确实会进到ResponseEntityProxy.streamClosed调用releaseConnection进行连接释放。
public boolean streamClosed(final InputStream wrapped) throws IOException {
try {
final boolean open = connHolder != null && !connHolder.isReleased();
// this assumes that closing the stream will
// consume the remainder of the response body:
try {
wrapped.close();
releaseConnection();
} catch (final SocketException ex) {
if (open) {
throw ex;
}
}
} catch (final IOException ex) {
abortConnection();
throw ex;
} catch (final RuntimeException ex) {
abortConnection();
throw ex;
} finally {
cleanup();
}
return false;
}
apache httpclient这段代码写的也很一般,有一种拼凑感,让我不得不吐个槽。
?HttpEntity里的stream关闭时会偷偷释放连接,这个就不太合理。HttpEntity有一个方法是isRepeatable,是说该HttpEntity是否可循环使用,我消费一次你就把流给关了,我循环个锤子哦。?HttpEntity不是Closable的,这个也算合理,因为一次http调用,当中的中间对象太多,HttpClient、HttpPost、CloseableHttpResponse、HttpEntity。应该关闭HttpClient和CloseableHttpResponse即可。?HttpClients.createDefault这个方法名也不喜欢,注释写的也不好,就因为这个,有些人不知道这个Client是否应该关闭,也不知道createDefault内部其实是对http连接池化的(默认单域名2个连接,总共两个连接),然后自己又弄了个池来放HttpClient,造孽呀。要我起名,就起HttpClients.createClosabletPoolingClient,谁知道你default干了个der。?上述代码中,streamClosed也很奇葩,先调了一下releaseConnection,后面又在finally里调用cleanup();,cleanup里干的事是直接把ConnectionHolder给close了,close时把reusable还置为false,相当于第一步把钱“还”到钱包里,然后下一步把钱包给扔了。得亏releaseConnection时判断了一下released是不是true,不然就出bug了。这个cleanup自己也是调了个寂寞。
问题结论
原因很清晰了
?项目组私自引了一个github上的个人项目,dubbo-plus,用于给dubbo进行http调用扩展?该项目代码写的太烂,如果调用出现404或者500就不释放连接?HttpClients.createDefault默认是生成单域2个连接,全部10个连接的http连接池?如果调用出现过404或者500,就出现了连接泄露
HttpClient到底要关闭哪些资源
?CloseableHttpResponse是肯定要关的,关闭这个会把http连接归还到池里?CloseableHttpClient httpclient = HttpClients.createDefault()可关可不关,这个内部本身就是一个http连接池,我个人倾向于这个做为全局变量共享使用就行了。apache这个实现也很不优美,你default实现做成池了还不和别人说,给的代码示例也是每次调用完就关闭,那你创建个池的目的是啥呢,你是认为我们在一个方法内部就调好多次http请求??官方代码示例如下:
try (CloseableHttpClient httpclient = HttpClients.createDefault()) {
HttpGet httpGet = new HttpGet("http://httpbin.org/get");
// The underlying HTTP connection is still held by the response object
// to allow the response content to be streamed directly from the network socket.
// In order to ensure correct deallocation of system resources
// the user MUST call CloseableHttpResponse#close() from a finally clause.
// Please note that if response content is not fully consumed the underlying
// connection cannot be safely re-used and will be shut down and discarded
// by the connection manager.
try (CloseableHttpResponse response1 = httpclient.execute(httpGet)) {
System.out.println(response1.getCode() + " " + response1.getReasonPhrase());
HttpEntity entity1 = response1.getEntity();
// do something useful with the response body
// and ensure it is fully consumed
EntityUtils.consume(entity1);
}
HttpPost httpPost = new HttpPost("http://httpbin.org/post");
List<NameValuePair> nvps = new ArrayList<>();
nvps.add(new BasicNameValuePair("username", "vip"));
nvps.add(new BasicNameValuePair("password", "secret"));
httpPost.setEntity(new UrlEncodedFormEntity(nvps));
try (CloseableHttpResponse response2 = httpclient.execute(httpPost)) {
System.out.println(response2.getCode() + " " + response2.getReasonPhrase());
HttpEntity entity2 = response2.getEntity();
// do something useful with the response body
// and ensure it is fully consumed
EntityUtils.consume(entity2);
}
}
猜你喜欢
- 2024-11-17 看完这几道 JavaScript 面试题,让你与考官对答如流(上)
- 2024-11-17 JS 鼠标框选(页面选择)时返回对应的 HTML 或文本内容
- 2024-11-17 一文读懂 WebAssembly(WASM)中的字符串
- 2024-11-17 DefineProperty 和Proxy双向绑定演示,你还不知道么?
- 2024-11-17 必考知识点-JavaScript类型转换(讲原理)
- 2024-11-17 文件上传,排版是伤(上传文件出现乱码是怎么回事)
- 2024-11-17 分享一些有趣的,你从不使用的html属性
- 2024-11-17 惊艳到了,每个开发人员都必须要知道的6个HTML属性!
- 2024-11-17 (鸡汤文)这一次我终于搞懂了 JavaScript 定时器的 this 指向
- 2024-11-17 js基础面试题131-160道题目(50道js面试题)
- 标签列表
-
- content-disposition (47)
- nth-child (56)
- math.pow (44)
- 原型和原型链 (63)
- canvas mdn (36)
- css @media (49)
- promise mdn (39)
- readasdataurl (52)
- if-modified-since (49)
- css ::after (50)
- border-image-slice (40)
- flex mdn (37)
- .join (41)
- function.apply (60)
- input type number (64)
- weakmap (62)
- js arguments (45)
- js delete方法 (61)
- blob type (44)
- math.max.apply (51)
- js (44)
- firefox 3 (47)
- cssbox-sizing (52)
- js删除 (49)
- js for continue (56)
- 最新留言
-