• ApacheHTTPClient的连接释放-EverNote同步


    使用经验
    1. 配置三个timeout config
      SO_TIMEOUT 防止socket read hang住
      CONNCT_TIMEOUT 防止connect超时很长,默认采用系统3或7次SYNC重试 windows:21s linux:128s
      REQUEST_CONNECTION_TIMEOUT 防止从连接池获取不到连接时hang住
    2. 连接池需要释放连接 防止只借用不归还,特别是异常时造成连接耗尽 具体参见下面的分析
    3. 连接池需要设置defaultMaxPerRoute, 默认是2,每个route(可以认为是一个域名,但是看它的equals方法,本地IP Address不同也不是一个route)只能建立两个HTTP连接(已经验证)
    4. 使用连接池时,连接的关闭融合进了Keep-Alive的处理:即ConnectionReuseStrategy 可以在这里选择关闭连接;当Response未被Consume时(根本上是EofSensorInputstream.close()),直接关闭response是能完成连接关闭的
    源码分析
    1. 请求发起链路:可以看到处理了重定向还有重试,不过默认不重试的 看代码重试默认是开启的, 如果你不指定disable;RetryExec中,对幂等请求,如GET、PUT是会自动重试的,对POST不重试,判断依据是看是否不是 instantOf HttpEntityEnclosingRequest Github-HC-4.5.14

    去除自动重试HttpClientBuilder.create().disableAutomaticRetries().build();

    at org.apache.http.impl.conn.DefaultHttpClientConnectionOperator.connect(DefaultHttpClientConnectionOperator.java:158)
    	at org.apache.http.impl.conn.PoolingHttpClientConnectionManager.connect(PoolingHttpClientConnectionManager.java:353)
    	at org.apache.http.impl.execchain.MainClientExec.establishRoute(MainClientExec.java:380)
    	at org.apache.http.impl.execchain.MainClientExec.execute(MainClientExec.java:236)
    	at org.apache.http.impl.execchain.ProtocolExec.execute(ProtocolExec.java:184)
    	at org.apache.http.impl.execchain.RetryExec.execute(RetryExec.java:88)
    	at org.apache.http.impl.execchain.RedirectExec.execute(RedirectExec.java:110)
    	at org.apache.http.impl.client.InternalHttpClient.doExecute(InternalHttpClient.java:184)
    	at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:82)
    	at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:107)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    释放连接池中的连接 是用 CloseableHttpResponse.close()还是HttpRequestBase.releaseConnection() 同时测试时EntityUtils也有释放链接的作用。具体怎么释放不是很明白

    MainClientExec

    同样看MainClientExec 中在捕获到IOException时也是会终止连接,而 NoHttpResponse是IOException的一种.

    CloseableHttpResponse
    // HttpResponseProxy
    @Override
    public void close() throws IOException {
        if (this.connHolder != null) {
            this.connHolder.close();
        }
    }
    // ConnectionHolder
    @Override
    public void close() throws IOException {
        releaseConnection(false);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    我好奇这个reusable一直为false,难道是不再用了吗,那么连接池的意义何在? 真的是close了,默认没有复用

    private void releaseConnection(final boolean reusable) {
        if (this.released.compareAndSet(false, true)) {
            synchronized (this.managedConn) {
                if (reusable) {
                    this.manager.releaseConnection(this.managedConn,
                            this.state, this.validDuration, this.tunit);
                } else {
                    try {
                        this.managedConn.close();
                        log.debug("Connection discarded");
                    } catch (final IOException ex) {
                        if (this.log.isDebugEnabled()) {
                            this.log.debug(ex.getMessage(), ex);
                        }
                    } finally {
                        this.manager.releaseConnection(
                                this.managedConn, null, 0, TimeUnit.MILLISECONDS);
    /// ...
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    推测与实际一致,链接被close
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AnDdSXrq-1684054299659)(en-resource://database/11356:1)]

    ReleaseConnection
    // HttpRequestBase
    /**
     * A convenience method to simplify migration from HttpClient 3.1 API. This method is
     * equivalent to {@link #reset()}.
     *
     * @since 4.2
     */
    public void releaseConnection() {
        reset();
    }
    
    // AbstractExecutionAwareRequest   根据调试这里的Cancellable是ConnectionHolder 
    public void reset() {
        final Cancellable cancellable = this.cancellableRef.getAndSet(null);
        if (cancellable != null) {
            cancellable.cancel();
        }
        this.aborted.set(false);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    ConnectionHoldercancel就很特殊了,是终止链接

    // ConnectionHolder
    @Override
    public boolean cancel() {
        final boolean alreadyReleased = this.released.get();
        log.debug("Cancelling request execution");
        abortConnection();
        return !alreadyReleased;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    ConnectionHolder所有操作

    // ConnectionHolder
    @Override
    public void releaseConnection() {
        releaseConnection(this.reusable);
    }
    // 如果连接已经释放,abort没有任何起作用
    @Override
    public void abortConnection() {
        if (this.released.compareAndSet(false, true)) {
            synchronized (this.managedConn) {
                try {
                    this.managedConn.shutdown();
                    log.debug("Connection discarded");
                } catch (final IOException ex) {
                    if (this.log.isDebugEnabled()) {
                        this.log.debug(ex.getMessage(), ex);
                    }
                } finally {
                    this.manager.releaseConnection(
                            this.managedConn, null, 0, TimeUnit.MILLISECONDS);
                }
            }
        }
    }
    @Override
    public boolean cancel() {
        final boolean alreadyReleased = this.released.get();
        log.debug("Cancelling request execution");
        abortConnection();
        return !alreadyReleased;
    }
    @Override
    public void close() throws IOException {
        releaseConnection(false);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    对应日志MainClientExec开头都是在ConnectionHolder类中输出,它这个log类很奇怪
    2022/08/31 17:19:52:382 CST [DEBUG] MainClientExec - Cancelling request execution
    2022/08/31 17:19:52:382 CST [DEBUG] DefaultManagedHttpClientConnection - http-outgoing-0: Shutdown connection
    2022/08/31 17:19:52:383 CST [DEBUG] MainClientExec - Connection discarded 丢弃连接

    EntityUtils.toString

    toString的时候的确释放了连接,并且是采用了最好的方式:
    ConnectionHolder#releaseConnectionreuseTrue
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bEQwj9WD-1684054299661)(en-resource://database/11358:1)]
    org.apache.http.impl.execchain.ResponseEntityProxy#streamClosed 关闭了流;注意不要被简单的InputStream迷惑了,Wrap了好几层呢;它有个eofwatcherResponseEntityProxy, proxy中eofDetected处理releaseConnection了, 而releaseConnection是考虑了Keepalive机制的
    实际上不只toString,EntityUtils中各种消费InputStream的方法最后都有close的动作,即使EntityUtils.consumeQuietly()

    @Override
    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    看到这里的cleanup吓我一跳,因为它又去调用了connHolder#close! ,但实际上因为链接已经被释放,并没有进行connection#close操作: 
    if (this.released.compareAndSet(false, true))

    // ResponseEntityProxy
    private void cleanup() throws IOException {
        if (this.connHolder != null) {
            this.connHolder.close();
        }
    }
    private void abortConnection() throws IOException {
        if (this.connHolder != null) {
            this.connHolder.abortConnection();
        }
    }
    public void releaseConnection() throws IOException {
        if (this.connHolder != null) {
            this.connHolder.releaseConnection(); // connectionHolder的reusable为true
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    ConnectionHolderreuse是根据keepAlive修改的,具体可以参考org.apache.http.impl.DefaultConnectionReuseStrategy 在MainClientExec中关注markReusablemarkNonReusable 的使用,它决定了连接是否重用。
    :https://blog.51cto.com/u_15310381/3201932

    如果request首部中包含Connection:Close,不复用
    如果response中Content-Length长度设置不正确(小于0),不复用
    如果response首部包含Connection:Close,不复用
    如果reponse首部包含Connection:Keep-Alive,复用
    都没命中的情况下,如果HTTP版本高于1.0则复用

    注意Spring返回400时,Connection是Close

    从HttpClientBuilder、Strategy、xxxExec看连接处理过程

    我使用的4.5.13中,看到几个Strategy
    ServiceUnavailableRetryStrategy (默认未启用)
    ConnectionReuseStrategy

    在builder构建中,你可以指定各种Strategy,
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xMesbEpI-1684054299662)(en-resource://database/12849:1)]
    实际上通过责任链模式给这个链ClientExecChain 增加相应的处理器 在覆盖编写自己的Strategy要注意builder中策略的判断顺序、override是与上个exec的执行顺序(文章最初的堆栈)

    那为什么MainClientExec中有retry的for循环逻辑,最上面堆栈显示也有个RetryExec?不是有点乱?MainClientExec中主要是为了认证需要进行重试,不是异常重试。

    结论

    response.close() 或者 httpPost.close()是不对的,关闭warapped流即可.

    1. httpPost.close是不对的,因为你在response.close前必须得getContent而后从流中读取然后关闭,在关闭流的时候,因为不是一个简单的InputStream,而是EofSensorInputStream,在close的时候已经释放了连接,所以在response.close的时候不会再connHolder.releaseConnection(false)

    2. 测试程序功能可能不全面干扰了测试结果:比如我在测试重试时,使用了GET方法,看到了重试,但实际业务并不发的是POST;比如我在GET时在HttpContext中拿到了connection,经测试无法关闭连接,我就认为此时是无法关闭的,这其实是不准确的,实际上因为测试返回的body为空,同时MainClientExec返回的connectHolder是null;所以测试时要和实际使用一致同时结合源码看。

      如何在Response上下文中关闭连接?
    3. 重写自己的ConnectionReuseStrategy,恰当的时候return false

    4. 在未consume response时,调用responseclose操作也是可以关闭连接的

    5. 在IO异常时,MainClientExec是会自动关闭连接的;在抛出一个NohttpResponseException时,连接也是会关闭的;根本原因是读到了EOF,这也是一个IO异常。
      但是
      conn.releaseConnection并不会关闭连接,可能因为Keep-Alive,重用连接,将释放连接返回连池,具体可以看ConnectionReleaseTrigger的注释

      钩子函数在哪里?

    我们经常会有需求修改开源代码的某个流程,嵌入我们的处理逻辑,一般人们并不能完全熟悉开源代码,不知道应该写在哪里。但是我们知道,写开源代码的大佬们技术肯定是过关并且代码经过千锤百炼,一定有这么个钩子。我最近的需要就是根据httpStatusCode去关闭ApacheHTTPClient的连接(因为istio-proxy对不可达IP返回503且没有Connection: close头),最终是搜索到了(上一节中)
    那么一般怎么找到这种hook点?
    6. 描述需求进行搜索
    7. 翻看源码的技巧 翻看源码一个个看、弄懂逻辑是很花时间,看类名就行,比如包GitHub-HC-4.5.14下的各种Strategy:DefaultServiceUnavailableRetryStrategy.java DefaultHttpRequestRetryHandler.java

    附录
    1. 官方提供的利用线程池的多线程示例代码:https://github.com/apache/httpcomponents-client/blob/4.5.x/httpclient/src/examples/org/apache/http/examples/client/ClientMultiThreadedExecution.java 更多示例:https://hc.apache.org/httpcomponents-client-4.5.x/examples.html
    2. HTTPClient打开debug日志以便调试
      没成功:https://www.baeldung.com/apache-httpclient-enable-logging
      管用:https://hc.apache.org/httpclient-legacy/logging.html
      这篇CSDN就是从leacy官网来的:https://blog.csdn.net/ganmutou/article/details/72884525?locationNum=8&fps=1
  • 相关阅读:
    【JS 原型对象和构造函数有何关系】
    基于STM32设计的校园一卡通(设计配套的手机APP)
    基于javaweb的房屋租赁后台管理系统
    【HDU No. 1224】 免费DIY之旅
    使用 Google Cloud Run 在 GCP 上部署 React 应用
    SpringTask基础使用
    【Python基础知识】面试基础知识
    Asp-Net-Core开发笔记:EFCore统一实体和属性命名风格
    过滤器Filter/监听器Listener
    【算法练习Day24】递增子序列&&全排列&&全排列 II
  • 原文地址:https://blog.csdn.net/KHZ_222/article/details/126798584