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