异常现象
多次上传文件时,偶尔会出现一次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这个异常后,客户端是否已经关闭这个连接,因此每次重发都需要建立连接请求。新建连接不存在太长的空闲时间问题。
客户端增加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。
参考
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/100281.html