Connection reset

在使用HttpClient调用后台resetful服务时,“Connection reset”是一个比较常见的问题,有同窗跟我私信说被这个问题困扰好久了,今天就来分析下,但愿能帮到你们。例如咱们线上的网关日志就会抛该错误:html

 

从日志中能够看到是Socket套接字在read数据时抛出了该错误。java

 

致使“Connection reset”的缘由是服务器端由于某种缘由关闭了Connection,而客户端依然在读写数据,此时服务器会返回复位标志“RST”,而后此时客户端就会提示“java.net.SocketException: Connection reset”。nginx

可能有同窗对复位标志“RST”还不太了解,这里简单解释一下:编程

TCP创建链接时须要三次握手,在释放链接须要四次挥手;例如三次握手的过程以下:tomcat

  1. 第一次握手:客户端发送syn包(syn=j)到服务器,并进入SYN_SENT状态,等待服务器确认;服务器

  2. 第二次握手:服务器收到syn包,并会确认客户的SYN(ack=j+1),同时本身也发送一个SYN包(syn=k),即SYN+ACK包,此时服务器进入SYN_RECV状态;网络

  3. 第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED(TCP链接成功)状态,完成三次握手。oracle

能够看到握手时会在客户端和服务器之间传递一些TCP头信息,好比ACK标志、SYN标志以及挥手时的FIN标志等。负载均衡

除了以上这些常见的标志头信息,还有另一些标志头信息,好比推标志PSH、复位标志RST等。其中复位标志RST的做用就是“复位相应的TCP链接”。ide

 

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

2

httpGet.setProtocolVersion(HttpVersion.HTTP_1_0);

httpGet.addHeader(HTTP.CONN_DIRECTIVE, HTTP.CONN_CLOSE);

 

最后再补充几句,虽然对于每次请求TCP长链接只能节约大约1ms的时间,可是具体是使用长链接仍是短链接仍是要衡量下,好比你的服务天天的pv是1亿,那么使用长链接节约的总时间为:

1

1亿*1ms=10^8*1ms=10^5*1s=10^5*1h/360027.78h

神奇的是,亿万级pv的服务使用长链接一天内节约的总时间为27.78小时(居然大于一天)。

因此使用长链接仍是短链接你们须要根据本身的服务访问量、扩展性等因素衡量下。可是必定要注意:服务器和客户端的链接必定要保持一致,要么都是长链接,要么都是短链接。

相关文章
相关标签/搜索