编程技术文章分享与教程

网站首页 > 技术文章 正文

http连接泄露,httpClient到底要关闭哪些资源

hmc789 2024-11-17 11:15:01 技术文章 2 ℃

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 status200,则通过EntityUtils.toByteArrayresponseEntity进行消费,如果是404或者500则直接throw exception,既不消费也不关闭。?注:该作者写代码不严谨,http stats又不只有这三种情况,按照MDN的定义,服务端也可能返回201 Created202 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。应该关闭HttpClientCloseableHttpResponse即可。?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);
    }
}

Tags:

标签列表
最新留言