异常现象
多次上传文件时,偶尔会出现一次failed to respond异常,但是重试一次又正常了。
错误日志
原因分析
服务端keep-alive超时断开连接
spring resttemplate使用apache httpclient4.4 连接池。
主要是因为httpclient之前与服务端建立的连接断开,但是没有通知客户端或者客户端还没有收到通知,导致下次请求该服务时httpclient继续使用该连接导致报错。
服务端tomcat 默认的keep-alive timeout :60s,httpclient的连接池中设置的时间大于60s,连接空闲时间超过60s后再次从连接池拿出进行请求时,就会出现failed to respond异常。
服务器端负载过大,丢弃链接
当服务器端由于负载过大等情况发生时,可能导致在收到请求后无法处理(比如没有足够的线程资源),会直接丢弃链接而不进行处理。此时客户端就会报错:NoHttpResponseException,建议出现这种情况时,可以选择重试。
抓包分析
可以通过tcp报文分析出,客户端和服务器连接的最大空闲时间,看看报文的交互过程。
- 注意到图中第2882个包,服务器返回前一个请求的响应完成(10:16:25),到第2888个包(10:16:46)客户端发送的下一个请求包。直接有21s的空闲间隔,结合多个完整的连接请求断开的时间,可以判断出服务器在美国连接空闲20s后自动就会发起断开连接。
- 客户端发出的第2888个包在收到服务器发送的2891个FIN包之前,客户端发送了2888和2889两个请求报文(客户端此时为收到服务器FIN报文)。但发送后,服务端发送的FIN包立刻就到了客户端,可以推测出,服务端在发送FIN报文前还没有收到客户端的请求报文,但是刚刚发送FIN报文却没有收到[FIN、ACK]报文,因此服务器无法判断是否是正常结束,所有就发出来RST包,关闭连接。
- 客户端使用的httpclient的60s的长连接发送请求,使用的http1.1协议默认的keepalive的,同一个线程的多个请求可以复用同一个长连接。正是由于服务器发出的FIN包的时间与客户端在连接空闲了20s时扔使用这个连接发送数据时之间微秒的时间差(服务器发送了FIN报文,但是客户端还没有收到,但是客户端已经发送了请求数据包),所以导致出现NoHttpResponseException异常。
解决方案
客户端捕获异常重试(推荐)
推荐使用重发机制。
http请求使用重发机制,捕获NohttpResponseException的异常,重新发送请求,重发3次后还是失败才停止。由于服务器不知道客户端捕获到NohttpResponseException这个异常后,客户端是否已经关闭这个连接,因此每次重发都需要建立连接请求。新建连接不存在太长的空闲时间问题。 参考代码: https://blog.csdn.net/u010800970/article/details/79996698
客户端增加KeepAliveStrategy策略
配置keepAlive策略,目的是让客户端在服务端还没有发送断开连接报文时,客户端提前发送断开连接请求。
即客户端的keepAlive时间要配置的比服务端的keepAlive小(服务端默认:keepAlive 60s)。
@Configuration public class RestTemplateConfig { @Bean public RestTemplate restTemplate(RestTemplateBuilder builder) { return builder .setConnectTimeout(Duration.ofMillis(1000)) // 连接建立超时时间 .setReadTimeout(Duration.ofMillis(2000)) // 响应数据超时时间 .requestFactory(this::requestFactory) // 请求工厂 .build(); }
@Bean public HttpComponentsClientHttpRequestFactory requestFactory() { PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(30, TimeUnit.SECONDS); connectionManager.setMaxTotal(200); connectionManager.setDefaultMaxPerRoute(20); CloseableHttpClient httpClient = HttpClients.custom() .setConnectionManager(connectionManager) .evictIdleConnections(30, TimeUnit.SECONDS) .disableAutomaticRetries() // 有 Keep-Alive 认里面的值,没有的话永久有效 //.setKeepAliveStrategy(DefaultConnectionKeepAliveStrategy.INSTANCE) // 换成自定义的 .setKeepAliveStrategy(new CustomConnectionKeepAliveStrategy()) .build(); HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(httpClient); return requestFactory; } /** * KeepAlive策略 */ public class CustomConnectionKeepAliveStrategy implements ConnectionKeepAliveStrategy { // 连接超过20s没有数据就主动断开与服务器的连接 private final long DEFAULT_SECONDS = 20; @Override public long getKeepAliveDuration(HttpResponse response, HttpContext context) { long timeOut = Arrays.asList(response.getHeaders(HTTP.CONN_KEEP_ALIVE)) .stream() .filter(h -> StringUtils.equalsIgnoreCase(h.getName(), "timeout") && StringUtils.isNumeric(h.getValue())) .findFirst() .map(h -> NumberUtils.toLong(h.getValue(), DEFAULT_SECONDS)) .orElse(DEFAULT_SECONDS) * 1000; System.out.println(timeOut); return timeOut; } }
}
客户端http连接不允许复用
不推荐使用,这样完全发挥不错线程池的优势。
HttpPost httpPost = new HttpPost(url);
// 设置不使用长连接
httpPost.setHeader("Connection", "close");
服务端修改配置
不推荐,服务同时使用默认keepAlive 60s,connection timeout 60s。