欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

记一次NoHttpResponseException问题排查

程序员文章站 2022-04-07 19:46:31
...

上传文件程序会有一定的概率提示错误,错误率大概在1%以下,错误信息是:org.apache.http.NoHttpResponseException , s3-us-west-1.amazonaws.com:80 failed to respond,看着是上传到S3的过程中发送了网络错误?

记一次NoHttpResponseException问题排查

通过查阅资料,发现了一篇比较好的文章:一次NoHttpResponseException问题分析解决。这个文章的观点是会发生这个错误的原因是服务端关闭了连接,而客户端还在使用该连接,导致服务端响应RST报文,客户端收到RST报NoHttpResponseException异常。

为了说明这个场景,就要提一下Keepalive机制。Keepalive是HTTP的连接复用机制,在HTTP1.0时代,每个请求经过三次握手后,只会传输一次HTTP请求和响应报文后,就进入四次挥手关闭连接了。而TCP建立连接和关闭连接的代价是比较大的,导致HTTP1.0的通道利用率较低,时延较高。针对这个问题,退出了Keepalive机制,一个TCP连接建立后,可以在上面发送多个HTTP报文,只有这个TCP连接的空闲时间达到超时时间,才会被关闭。HTTP1.1默认开启Keepalive。这里的关闭行为可能发生在客户端和服务端,比如客户端的Keepalive超时时间更短,则客户端就会先关闭连接,如果服务端配置的Keepalive超时时间更短,则服务端就会先关闭连接。

乍看起来无论那一边关闭连接都没什么问题,但是还是有细节需要注意。比如服务端关闭连接,发送FIN包,在这个FIN包发送但是还未到达客户端期间,客户端如果继续复用这个TCP连接,发送HTTP请求报文的话,服务端会因为在四次挥手期间不接收报文而发送RST报文给客户端,客户端收到RST报文就会提示异常。

根据上面的理论知识,可以推测org.apache.http.NoHttpResponseException , s3-us-west-1.amazonaws.com:80 failed to respond这个错误发生的原因是因为我的程序的HttpClient的Keepalive时间大于S3服务器的,导致S3服务端关闭连接时,可能发生异常。我们做个试验看看。

首先写一个简单程序观察一下AWS S3服务端的Keepalive时间

String url = "一个可以访问的S3下载地址";
CloseableHttpClient httpClient = HttpClients.createDefault();
HttpGet request = new HttpGet(url);

httpClient.execute(request, response -> {
    String content = EntityUtils.toString(response.getEntity());
    System.out.println(content);
    return content;
});

Thread.sleep(99999);

Wireshark抓包观察HTTP响应报文后,经过多久进入四次挥手:

记一次NoHttpResponseException问题排查

可以看出服务端发送FIN包距离上一个请求的时间大概是23秒,也就是AWS S3服务端的Keepalive时间大致为23秒。

接着我们模拟客户端在服务端关闭连接的同时发送请求的场景,看看能否复现NoHttpResponseException错误:

String url = "http://s3-us-west-1.amazonaws.com/sdpcs-prod-awsca/88ea9001-bad0-4b46-86e5-e6bc518c9fdc?Expires=1718171230&response-content-type=image/jpeg&response-cache-control=max-age=157680000&AWSAccessKeyId=AKIAI7P7PYLVYWVVYTLQ&Signature=iCeE6/IHtxmOarOc3Q1hUowWqDc=";
CloseableHttpClient httpClient = HttpClients.createDefault();
HttpGet request = new HttpGet(url);

for (int i = 0; i < 100000; i  ) {
    httpClient.execute(request, response -> {
        String content = EntityUtils.toString(response.getEntity());
        System.out.println(content);
        return content;
    });

    Thread.sleep(23000);
}

多执行几次,就能复现出NoHttpResponseException错误:

六月 14, 2019 2:09:14 下午 org.apache.http.impl.execchain.RetryExec execute
信息: I/O exception (org.apache.http.NoHttpResponseException) caught when processing request to {}->http://s3-us-west-1.amazonaws.com:80: The target server failed to respond
六月 14, 2019 2:09:14 下午 org.apache.http.impl.execchain.RetryExec execute
信息: Retrying request to {}->http://s3-us-west-1.amazonaws.com:80

分析抓包:

记一次NoHttpResponseException问题排查

可以看到2400号请求距离上一个请求23秒,然后在服务端还未收到2400号请求时,客户端就收到了服务端发来的FIN请求,进入了四次挥手流程。然后当服务端收到2400号请求后,响应RST请求,导致客户端提示错误。

HttpClient提供了关闭空闲连接的功能:

CloseableHttpClient httpClient = HttpClients.custom()
                .evictIdleConnections(5, TimeUnit.SECONDS)
                .build();

我们设置一个低于S3 Keepalive的时间再次执行,就不会出现NoHttpResponseException错误了。

除了在客户端设置小于服务端的Keepalive时间,还有一种做法是在出现NoHttpResponseException时进行重试,也是可以的,还可以减少TIME_WAIT数量。

本文独立博客地址:记一次NoHttpResponseException问题排查 | 木杉的博客