前言
昨天压测的时候,发现少量交易出现系统异常,查询日志,发现其中有一个系统调用另外一个系统时发生NoHttpResponseException,这个异常几乎是瞬间就抛出了,没有一点停顿。蒽,来研究研究。
问题详细描述
问题发生在接入层访问业务层时,接入层发生的错误。接入层使用的是连接池。。
org.apache.http.NoHttpResponseException: xxxxxxx:xxx failed to respond
at org.apache.http.impl.conn.DefaultHttpResponseParser.parseHead(DefaultHttpResponseParser.java:143) ~[httpclient-4.5.jar:4.5]
at org.apache.http.impl.conn.DefaultHttpResponseParser.parseHead(DefaultHttpResponseParser.java:57) ~[httpclient-4.5.jar:4.5]
at org.apache.http.impl.io.AbstractMessageParser.parse(AbstractMessageParser.java:261) ~[httpcore-4.4.1.jar:4.4.1]
at org.apache.http.impl.DefaultBHttpClientConnection.receiveResponseHeader(DefaultBHttpClientConnection.java:165) ~[httpcore-4.4.1.jar:4.4.1]
at org.apache.http.impl.conn.CPoolProxy.receiveResponseHeader(CPoolProxy.java:167) ~[httpclient-4.5.jar:4.5]
at org.apache.http.protocol.HttpRequestExecutor.doReceiveResponse(HttpRequestExecutor.java:272) ~[httpcore-4.4.1.jar:4.4.1]
at org.apache.http.protocol.HttpRequestExecutor.execute(HttpRequestExecutor.java:124) ~[httpcore-4.4.1.jar:4.4.1]
at org.apache.http.impl.execchain.MainClientExec.execute(MainClientExec.java:271) ~[httpclient-4.5.jar:4.5]
at org.apache.http.impl.execchain.ProtocolExec.execute(ProtocolExec.java:184) ~[httpclient-4.5.jar:4.5]
at org.apache.http.impl.execchain.RetryExec.execute(RetryExec.java:88) ~[httpclient-4.5.jar:4.5]
at org.apache.http.impl.execchain.RedirectExec.execute(RedirectExec.java:110) ~[httpclient-4.5.jar:4.5]
at org.apache.http.impl.client.InternalHttpClient.doExecute(InternalHttpClient.java:184) ~[httpclient-4.5.jar:4.5]
at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:82) ~[httpclient-4.5.jar:4.5]
at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:107) ~[httpclient-4.5.jar:4.5]
http连接过期
我在stackoverflow上找到了一个哥们的答案,挺好的,先分享如下
https://stackoverflow.com/questions/10558791/apache-httpclient-interim-error-nohttpresponseexception
很可能是connection manager 中保留存活的某些持久化连接 已经过期了。也就是说,目标服务器在它自己的这一端关闭了连接,但是httpclinet没有对这个事件做出响应,当连接闲置的时候,使连接变成半关闭或者‘过期’。通常情况下这并不是问题。 httpclient会利用好几个技术来检查连接的有效性在连接被释放回连接池时。即使关闭了过期检查,当使用过期的连接来传输请求信息,请求的执行会在做写操作时报SocketException,自动重试。然而,有些情况下,写操作会被终止,而没有抛出异常,因此,接下来的读取操作会返回-1(end of stream)。这种情况下,httpclinet别无他法,只能认为http请求成功了,原因是服务端发生不可知的错误,导致处理失败。
最简单的方式来补救这种情况是驱逐 连接池中 过期的 和 闲置超过一定的时间(1 min)的连接 。细节请参考
http://hc.apache.org/httpcomponents-client-ga/tutorial/html/connmgmt.html#d5e659
延申阅读-驱逐连接策略
经典阻塞I/O模型的缺点之一是:只有阻塞发生在I/O操作,网络socket可以对I/O事件做出反应。当连接被释放到连接池中时,它是存活的,然而它不能监控socke的状态,并不能响应任何I/O事件。当服务端关闭了连接,客户端不能够发现连接状态的改变(做出关闭连接的动作)。
HttpClinet尝试着缓解这样的问题 ,通过测试连接是否过期,连接是否有效(由于服务端关闭连接),在使用连接执行http请求之前。测试过期连接的操作并不是100%可靠。唯一可行的方法是使用一个专用的监控线程来驱逐那些很长时间闲置的过期连接。监控线程可以周期的在连接池中调用 ClientConnectionManager#closeExpiredConnections() 来关闭所有过期的连接,并且驱逐关闭的连接。也可以可选的调用 ClientConnectionManager#closeIdleConnections()来关闭闲置超过一定时间的连接。
参考代码
public static class IdleConnectionMonitorThread extends Thread {
private final HttpClientConnectionManager connMgr;
private volatile boolean shutdown;
public IdleConnectionMonitorThread(HttpClientConnectionManager connMgr) {
super();
this.connMgr = connMgr;
}
@Override
public void run() {
try {
while (!shutdown) {
synchronized (this) {
wait(5000);
// Close expired connections
connMgr.closeExpiredConnections();
// Optionally, close connections
// that have been idle longer than 30 sec
connMgr.closeIdleConnections(30, TimeUnit.SECONDS);
}
}
} catch (InterruptedException ex) {
// terminate
}
}
public void shutdown() {
shutdown = true;
synchronized (this) {
notifyAll();
}
}
}
解决方案
- 添加驱逐连接策略
- client端捕捉这个异常,然后考虑重传机制
后记
连接池是把双刃剑。在使用新东西前,必须做好功课,切勿从网上copy一个,看起来能用就ok了。
stay hungry, stay foolish