现象描述
在使用 HttpClient 调用后台 resetful 服务时,“Connection reset” 是一个比较常见的问题,有同学跟我私信说被这个问题困扰很久了,今天就来分析下,希望能帮到大家。例如我们线上的网关日志就会抛该错误:
从日志中可以看到是 Socket 套接字在 read 数据时抛出了该错误。
原因分析
导致 “Connection reset” 的原因是服务器端因为某种原因关闭了 Connection,而客户端依然在读写数据,此时服务器会返回复位标志 “RST”,然后此时客户端就会提示 “java.net.SocketException: Connection reset”。
TCP回顾
可能有同学对复位标志 “RST” 还不太了解,这里简单解释一下:
TCP 建立连接时需要三次握手,在释放连接需要四次挥手;例如三次握手的过程如下:
- 第一次握手:客户端发送 syn 包(syn=j)到服务器,并进入 SYN_SENT 状态,等待服务器确认;
- 第二次握手:服务器收到 syn 包,并会确认客户的 SYN(ack=j+1),同时自己也发送一个 SYN 包(syn=k),即 SYN+ACK 包,此时服务器进入 SYN_RECV 状态;
- 第三次握手:客户端收到服务器的 SYN+ACK 包,向服务器发送确认包 ACK (ack=k+1),此包发送完毕,客户端和服务器进入 ESTABLISHED(TCP 连接成功)状态,完成三次握手。
可以看到握手时会在客户端和服务器之间传递一些 TCP 头信息,比如 ACK 标志、SYN 标志以及挥手时的 FIN 标志等。
除了以上这些常见的标志头信息,还有另外一些标志头信息,比如推标志 PSH、复位标志 RST 等。其中复位标志 RST 的作用就是 “复位相应的 TCP 连接”。
TCP 连接和释放时还有许多细节,比如半连接状态、半关闭状态等。详情请参考这方面的巨著《TCP/IP 详解》和《UNIX 网络编程》。
原因阐述
前面说到出现 “Connection reset” 的原因是服务器关闭了 Connection [调用了 Socket.close () 方法]。大家可能有疑问了:服务器关闭了 Connection 为什么会返回 “RST” 而不是返回 “FIN” 标志。原因在于 Socket.close () 方法的语义和 TCP 的 “FIN” 标志语义不一样:发送 TCP 的 “FIN” 标志表示我不再发送数据了,而 Socket.close () 表示我不在发送也不接受数据了。问题就出在 “我不接受数据” 上,如果此时客户端还往服务器发送数据,服务器内核接收到数据,但是发现此时 Socket 已经 close 了,则会返回 “RST” 标志给客户端。当然,此时客户端就会提示:“Connection reset”。详细说明可以参考 oracle 的有关文档:http://docs.oracle.com/javase/1.5.0/docs/guide/net/articles/connection_release.html。
另一个可能导致的 “Connection reset” 的原因是服务器设置了 Socket.setLinger (true, 0)。但我检查过线上的 tomcat 配置,是没有使用该设置的,而且线上的服务器都使用了 nginx 进行反向代理,所以并不是该原因导致的。关于该原因上面的 oracle 文档也谈到了并给出了解释。
此外啰嗦一下,另外还有一种比较常见的错误 “Connection reset by peer”,该错误和 “Connection reset” 是有区别的:
-
服务器返回了 “RST” 时,如果此时客户端正在从 Socket 套接字的输出流中读数据则会提示 Connection reset”;
-
服务器返回了 “RST” 时,如果此时客户端正在往 Socket 套接字的输入流中写数据则会提示 “Connection reset by peer”。
“Connection reset by peer” 如下图所示:
问题解决
前面谈到了导致 “Connection reset” 的原因,而具体的解决方案有如下几种:
-
出错了重试;
-
客户端和服务器统一使用 TCP 长连接;
-
客户端和服务器统一使用 TCP 短连接。
出错重试
首先是出错了重试:这种方案可以简单防止 “Connection reset” 错误,然后如果服务不是 “幂等” 的则不能使用该方法;比如提交订单操作就不是幂等的,如果使用重试则可能造成重复提单。
统一建立长连接
然后是客户端和服务器统一使用 TCP 长连接:客户端使用 TCP 长连接很容易配置(直接设置 HttpClient 就好),而服务器配置长连接就比较麻烦了,就拿 tomcat 来说,需要设置 tomcat 的 maxKeepAliveRequests、connectionTimeout 等参数。另外如果使用了 nginx 进行反向代理或负载均衡,此时也需要配置 nginx 以支持长连接(nginx 默认是对客户端使用长连接,对服务器使用短连接)。
使用长连接可以避免每次建立 TCP 连接的三次握手而节约一定的时间,但是我这边由于是内网,客户端和服务器的 3 次握手很快,大约只需 1ms。ping 一下大约 0.93ms(一次往返);三次握手也是一次往返(第三次握手不用返回)。根据 80/20 原理,1ms 可以忽略不计;又考虑到长连接的扩展性不如短连接好、修改 nginx 和 tomcat 的配置代价很大(所有后台服务都需要修改);所以这里并没有使用长连接。ping 服务器的时间如下图:
统一建立短连接
最后的解决方案是客户端和服务器统一使用 TCP 短连接:我这边正是这么干的,而使用短连接既不用改 nginx 配置,也不用改 tomcat 配置,只需在使用 HttpClient 时使用 http1.0 协议并增加 http 请求的 header 信息(Connection: Close),源码如下:
1 httpGet.setProtocolVersion(HttpVersion.HTTP_1_0); 2 httpGet.addHeader(HTTP.CONN_DIRECTIVE, HTTP.CONN_CLOSE);
最后再补充几句,虽然对于每次请求 TCP 长连接只能节约大约 1ms 的时间,但是具体是使用长连接还是短连接还是要衡量下,比如你的服务每天的 pv 是 1 亿,那么使用长连接节约的总时间为:
1亿*1ms=10^8*1ms=10^5*1s=10^5*1h/3600≈27.78h
神奇的是,亿万级 pv 的服务使用长连接一天内节约的总时间为 27.78 小时(竟然大于一天)。
所以使用长连接还是短连接大家需要根据自己的服务访问量、扩展性等因素衡量下。但是一定要注意:服务器和客户端的连接一定要保持一致,要么都是长连接,要么都是短连接。
知识拓展
稍微补充下,有时改成短链接不一定能完全解决该问题,因为在http请求发送和返回响应肯定是需要时间的,在服务器高并发环境下很容易触发安全策略或者其他策略导致链接强制断开(比如服务器限制了单个ip连接数),在不用考虑幂等问题时,可以采用重试机制。
这里使用okhttp4使用拦截器重试,这样能大概率解决所有问题
1 private static OkHttpClient okHttpClient = new OkHttpClient.Builder() 2 .connectTimeout(0, TimeUnit.SECONDS) 3 .readTimeout(0, TimeUnit.SECONDS) 4 .retryOnConnectionFailure(true) 5 .addInterceptor(myOkHttpRetryInterceptor) 6 .build();
其中myOkHttpRetryInterceptor可以仿照这个博客编写
1 package com.gomefinance.esign.httpretry; 2 3 import lombok.extern.slf4j.Slf4j; 4 import okhttp3.Interceptor; 5 import okhttp3.Request; 6 import okhttp3.Response; 7 8 import java.io.IOException; 9 import java.io.InterruptedIOException; 10 import java.util.List; 11 12 /** 13 * User: Administrator 14 * Date: 2017/9/19 15 * Description: 16 */ 17 18 @Slf4j 19 public class MyOkHttpRetryInterceptor implements Interceptor { 20 public int executionCount;//最大重试次数 21 private long retryInterval;//重试的间隔 22 MyOkHttpRetryInterceptor(Builder builder) { 23 this.executionCount = builder.executionCount; 24 this.retryInterval = builder.retryInterval; 25 } 26 27 28 29 @Override 30 public Response intercept(Chain chain) throws IOException { 31 Request request = chain.request(); 32 Response response = doRequest(chain, request); 33 int retryNum = 0; 34 while ((response == null || !response.isSuccessful()) && retryNum <= executionCount) { 35 log.info("intercept Request is not successful - {}",retryNum); 36 final long nextInterval = getRetryInterval(); 37 try { 38 log.info("Wait for {}",nextInterval); 39 Thread.sleep(nextInterval); 40 } catch (final InterruptedException e) { 41 Thread.currentThread().interrupt(); 42 throw new InterruptedIOException(); 43 } 44 retryNum++; 45 // retry the request 46 response = doRequest(chain, request); 47 } 48 return response; 49 } 50 51 private Response doRequest(Chain chain, Request request) { 52 Response response = null; 53 try { 54 response = chain.proceed(request); 55 } catch (Exception e) { 56 } 57 return response; 58 } 59 60 /** 61 * retry间隔时间 62 */ 63 public long getRetryInterval() { 64 return this.retryInterval; 65 } 66 67 public static final class Builder { 68 private int executionCount; 69 private long retryInterval; 70 public Builder() { 71 executionCount = 3; 72 retryInterval = 1000; 73 } 74 75 public MyOkHttpRetryInterceptor.Builder executionCount(int executionCount){ 76 this.executionCount =executionCount; 77 return this; 78 } 79 80 public MyOkHttpRetryInterceptor.Builder retryInterval(long retryInterval){ 81 this.retryInterval =retryInterval; 82 return this; 83 } 84 public MyOkHttpRetryInterceptor build() { 85 return new MyOkHttpRetryInterceptor(this); 86 } 87 } 88 89 }
标签:reset,解决方案,TCP,Connection,服务器,连接,客户端 From: https://www.cnblogs.com/fnlingnzb-learner/p/16976807.html