http://stackoverflow.com/questions/10570672/get-nohttpresponseexception-for-load-testing/10680629#10680629
http://stackoverflow.com/questions/10558791/apache-httpclient-interim-error-nohttpresponseexception
具体原由上面两个链接里都有, 希望帮助到有一定能力的朋友尽快找到解决方案.
下面说说笔者出现这个问题的场景.
前提
在用cas做sso的时候需要在认证中心填写表单后,再次对ticket校验吗,这是由客户端(应用端)发出的请求, 采用本文问题缘起的 HttpClients.
这里用的是 httpclient-4.4.jar.
一次优化
原来没什么想法, 只是用最简单的 CloseableHttpClient, 即 HttpClients.createDefault() 构造的. 很显然,在这种最简单版本背后是对每一个校验程序都生成一个 http client, 这应该是可以优化的.
于是采用了连接池的思路, 本着 apache 无所不能的本位思想, 鼓捣了一番,有了下面这段代码. 这里连接池的缺省配置: defaultMaxPerRoute : 2, maxTotal : 20.
private final CloseableHttpClient httpClient = HttpClients.createMinimal(new PoolingHttpClientConnectionManager(RegistryBuilder.create() .register("http", PlainConnectionSocketFactory.getSocketFactory()) .register("https", SSLConnectionSocketFactory.getSocketFactory()) .build(), new SystemDefaultDnsResolver() { @Override public InetAddress[] resolve(final String host) throws UnknownHostException { logger.info("using httpClient env is {}", env); if (host.equalsIgnoreCase("test.cas.com")) {// test.cas.com 是笔者配的内网测服hosts,不必在意. return new InetAddress[]{InetAddress.getByName("192.168.0.1")}; } else { return super.resolve(host); } } }));
这里 用的是 httpclient-4.4.jar.
好像这段代码没什么, 可是在部署到线上环境时, 偶尔会发生题目中描述的异常也即 NoHttpResponseException, 具体的原因下面慢慢解释:
首先 PoolingHttpClientConnectionManager 的注释很直观, 即
* {@code ClientConnectionPoolManager} maintains a pool of
* {@link HttpClientConnection}s and is able to service connection requests
* from multiple execution threads. Connections are pooled on a per route
* basis. A request for a route which already the manager has persistent
* connections for available in the pool will be services by leasing
* a connection from the pool rather than creating a brand new connection.
就是说这个连接池对每一个固定请求host地址都使用了持久化连接, 在第一次访问之后的相同路径请求就会使用已经持久化的连接, 这也正是持久化连接的定义.
我们可以再看一下官方的解释:
HTTP/1.1 states that HTTP connections can be re-used for multiple requests per default. HTTP/1.0 compliant endpoints can also use a mechanism to explicitly communicate their preference to keep connection alive and use it for multiple requests. HTTP agents can also keep idle connections alive for a certain period time in case a connection to the same target host is needed for subsequent requests. The ability to keep connections alive is usually refered to as connection persistence. HttpClient fully supports connection persistence.
可以得出一个废话结论: 连接池内的连接初始化后提供的都是持久化连接.
然而, 持久化连接在这里会有什么问题呢?
bug 来了
stackoverflow 上针对这一现象有解释如下:
Most likely persistent connections that are kept alive by the connection manager become stale. That is, the target server shuts down the connection on its end without HttpClient being able to react to that event, while the connection is being idle, thus rendering the connection half-closed or 'stale'. Usually this is not a problem. HttpClient employs several techniques to verify connection validity upon its lease from the pool. Even if the stale connection check is disabled and a stale connection is used to transmit a request message the request execution usually fails in the write operation with SocketException and gets automatically retried.
via oleg
即那些持久化连接用了一次或几次后就会变得 stale (老化?), 这是由于这些连接在保持握手后, 直接在通信的另一方给关闭了, 此时请求方还没来的即响应这次取消, 那这个连接有恢复闲置状态, 因此这次请求就转为 stale 或者 half-closed 状态.
大体上可以推测出是一方A使用持久化连接, 一次通信的结束时B方觉得响应输出完毕, 那么就该直接关了socket, 这也不管这个响应到没到A方, A方然后就发现这连接直接被关了...居然也不知道咋对待. 那么, throw NoHttpResponseException.
上面引用的文字里也有提到: 接收响应的写操作由于 SocketException 失效后会有重试机制. 哈哈, 解决方案有了.(特定的恶劣情况还是不可控的)
解决方案
private final CloseableHttpClient httpClient2 = HttpClientBuilder.create().setConnectionManager(new PoolingHttpClientConnectionManager(RegistryBuilder.create() .register("http", PlainConnectionSocketFactory.getSocketFactory()) .register("https", SSLConnectionSocketFactory.getSocketFactory()) .build(), new SystemDefaultDnsResolver() { @Override public InetAddress[] resolve(final String host) throws UnknownHostException { logger.info("using httpClient2 env is {}", env); if (host.equalsIgnoreCase("test.cas.com")) {// test.cas.com 是笔者配的内网测服hosts,不必在意. return new InetAddress[]{InetAddress.getByName("192.168.0.106")}; } else { return super.resolve(host); } } })).setRetryHandler(new HttpRequestRetryHandler() { @Override public boolean retryRequest(IOException exception, int executionCount, HttpContext context) { if (executionCount > 3) { logger.warn("Maximum tries reached for client http pool "); return false; } if (exception instanceof NoHttpResponseException) { logger.warn("No response from server on " + executionCount + " call"); return true; } return false; } }).build();
上文中的 HttpRequestRetryHandler 也很直观, 使用上保证线程安全即可. 具体如下:
/** * A handler for determining if an HttpRequest should be retried after a * recoverable exception during execution. ** Implementations of this interface must be thread-safe. Access to shared * data must be synchronized as methods of this interface may be executed * from multiple threads. * *
@since 4.0 */ public interface HttpRequestRetryHandler { /** * Determines if a method should be retried after an IOException * occurs during execution. * * @param exception the exception that occurred * @param executionCount the number of times this method has been * unsuccessfully executed * @param context the context for the request execution * * @return {@code true} if the method should be retried, {@code false} * otherwise */ boolean retryRequest(IOException exception, int executionCount, HttpContext context); }
thx reading & hope enjoy ~