Apache的httpclient长连接泄漏

16年刚进公司的时候,我曾经做了个需求,把请求微信支付改为长连接。我们使用*Apache*的*httpclient*,改起来还是很容易。上线后,效果也挺显著,平均请求耗时降低了几十毫秒。可惜好景不长,程序跑了几天后,耗时变得越来越长,比短连接时还慢

16年刚进公司的时候,我曾经做了个需求,把请求微信支付改为长连接。我们使用Apachehttpclient,改起来还是很容易。上线后,效果也挺显著,平均请求耗时降低了几十毫秒。可惜好景不长,程序跑了几天后,耗时变得越来越长,比短连接时还慢。
我让运维跑netstat后,发现与微信的连接有好几万条,都是CLOSE-WAIT,一看就是连接泄漏的问题。但这就奇怪了,平常短连接跑的都是好好的,为什么换成长连接就泄漏了?难道短连接关闭的方法,不适用于长连接?于是就去git clone一下httpclient的代码,并git checkout到我们使用的版本(git真好用,为什么我们公司还要用svn呢,每次看着同事切换个分支就感觉麻烦)。经过一个星期各种妙想天开的在代码里找泄漏的原因和尝试,最终还是没有找到,只能用回短连接。
最近,在研究微信商户证书过期更新httpclient对象时,突然想到,双向证书这种情况的长连接是怎么复用的呢?感觉好像可以找到两年前心结的原因,我就把系统配置为长连接,然后发起多次微信退款。每发起一次,连接就增加一条,过一段时间后就会变为CLOSE-WAIT

连接的state

MainClientExec的execute函数,在获取连接池的连接时,会根据路由和连接的state来选取。这里的state为上下文的userToken

Object userToken = context.getUserToken();

final ConnectionRequest connRequest = connManager.requestConnection(route, userToken);

MainClientExec的execute函数,在返回response前,会获取userToken,并设置为连接的state。

if (userToken == null) {
    userToken = userTokenHandler.getUserToken(context);
    context.setAttribute(HttpClientContext.USER_TOKEN, userToken);
}
if (userToken != null) {
    connHolder.setState(userToken);
}

// check for entity, release connection if possible
final HttpEntity entity = response.getEntity();
if (entity == null || !entity.isStreaming()) {
    // connection not needed and (assumed to be) in re-usable state
    connHolder.releaseConnection();
    return new HttpResponseProxy(response, null);
} else {
    return new HttpResponseProxy(response, connHolder);

对于双向证书的情况,userToken会是什么呢?

public Object getUserToken(final HttpContext context) {

    final HttpClientContext clientContext = HttpClientContext.adapt(context);

    Principal userPrincipal = null;

    final AuthState targetAuthState = clientContext.getTargetAuthState();
    if (targetAuthState != null) {
        userPrincipal = getAuthPrincipal(targetAuthState);
        if (userPrincipal == null) {
            final AuthState proxyAuthState = clientContext.getProxyAuthState();
            userPrincipal = getAuthPrincipal(proxyAuthState);
        }   
    }   

    if (userPrincipal == null) {
        final HttpConnection conn = clientContext.getConnection();
        if (conn.isOpen() && conn instanceof ManagedHttpClientConnection) {
            final SSLSession sslsession = ((ManagedHttpClientConnection) conn).getSSLSession();
            if (sslsession != null) {
                userPrincipal = sslsession.getLocalPrincipal();
            }   
        }   
    }   

    return userPrincipal;
}

首先,系统会判断连接是否使用了http的鉴权,假如使用了就使用鉴权用户主体。否则,就检查是否有SSLSession,假如有,就取SSLSession的本地证书的subject作为userToken。那么,双向证书的情况就是使用商户证书的subject。

总结

上下文里没有设置userToken,去连接池取连接时,都是取state为空的连接,但由于连接使用过后都会设置连接的state为商户证书的subject。所以每次获取的都是新连接。经过这次的经验,连接泄漏,不一定是由于连接没有归还,也可能是复用没有生效。

解决方案

由于使用双向证书时,都是一个商户生成一个httpclient对象,我们在设置证书的时候可以把商户证书的subject设置为userToken