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)

释放连接池中的连接 是用 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);
}

我好奇这个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);
/// ...

推测与实际一致,链接被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);
}

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

// ConnectionHolder
@Override
public boolean cancel() {
    final boolean alreadyReleased = this.released.get();
    log.debug("Cancelling request execution");
    abortConnection();
    return !alreadyReleased;
}

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);
}

对应日志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;
}

看到这里的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
    }
}

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

你可能感兴趣的:(Java,组件,java,linux,httpclient)