异常现象

多次上传文件时,偶尔会出现一次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://blog.csdn.net/siantbaicn/article/details/80854528

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐