记一次压测问题定位:connection reset by peer,TCP三次握手后服务端发送RST

问题描述

    这两天用Go做一个比较简单的task:后端有HTTPServer和TCPServer。客户端通过http接入到HTTPServer,HTTPServer通过RPC将请求发送到TCPServer,所有的业务逻辑都由TCPServer处理。

    压测:自己的mac电脑(CPU:Intel i7, 4核,2.7GHz。内存:16G),硬件够用。客户端用Go编写,1个goruntine启动一个HTTPClient往HTTPServer发送http请求。每个HTTPClient限定为一个HTTP长链接。

    问题:压到400个HTTPClient,出现一些错误提示“read: connection reset by peer”。

问题定位以及原因

“connection reset by peer”的含义是往对端写数据的时候,对端提示已经关闭了连接。一般往一个已经被关闭的socket写会提示这个错误。但是通过log分析,服务端没有应用层面的close,客户端也没有应用层面的write。抓包发现客户端建立TCP完成3次握手后,服务端立刻就回了RST。如下图:

记一次压测问题定位:connection reset by peer,TCP三次握手后服务端发送RST_第1张图片

这个抓包很好的反应了压测中的现象:错误提示connection reset by peer,但是应用层并没有任何的读写,TCP三次握手后服务端直接通过RST关闭了连接。RST的情况见的多,这种情况着实没有遇到过。最后N次baidu google,终于找到答案。

TCP三次握手后服务端直接RST的真相

内核中处理TCP连接时维护着两个队列:SYN队列和ACCEPT队列,如上图所示,服务端在建立连接过程中内核的处理过程如下:

  • (1)客户端使用connect调用向服务端发起TCP连接,内核将此连接信息放入SYN队列,返回SYN-ACK
  • (2)服务端内核收到客户端的ACK后,将此连接从SYN队列中取出,放入ACCEPT队列
  • (3)服务端使用accept调用将连接从ACCEPT队列中取出

上述抓包说明,3次握手已经完成。但是应用层accept并没有返回,说明问题出在ACCEPT队列中。那么什么情况下,内核准确的说应该是TCP协议栈会在三次握手完成后发RST呢?原因就是ACCEPT队列满了,上述(2)中,服务端内核收到客户端的ACK后将连接放入ACCEPT队列失败,就有可能回RST拒绝连接。

进一步来看Linux协议栈的一些逻辑:SYN队列和ACCEPT队列的长度是有限制的,SYN队列长度由内核参数tcp_max_syn_backlog决定,ACCEPT队列长度可以在调用listen(backlog)通过backlog,但总最大值受到内核参数somaxconn(/proc/sys/net/core/somaxconn)限制。若SYN队列满了,新的SYN包会被直接丢弃。若ACCEPT队列满了,建立成功的连接不会从SYN队列中移除,同时也不会拒绝新的连接,这会加剧SYN队列的增长,最终会导致SYN队列的溢出。当ACCEPT队列溢出之后,只要打开tcp_abort_on_flow内核参数(默认为0,关闭),建立连接后直接回RST,拒绝连接(可以通过/proc/net/netstat中ListenOverflows和ListenDrops查看拒绝的数目)

所以真相找到了:就是ACCEPT队列溢出了导致TCP三次握手后服务端发送RST

回到我的压测环境,mac电脑的内核是unix,所以以上的一些参数别说调整了,有些找都找不到在哪。但是somaxconn这个倒是找到了:


而我压测的时候起400个goruntine,同时跟服务端建立HTTP连接,可能导致了服务端的ACCEPT队列溢出。这里之所以用可能,是因为并没有找到证据,只是理论上分析。但是验证这个问题简单:修改一下内核参数somaxconn。但是不想在mac电脑上搞了,于是将建立HTTP连接的速度放慢,20ms一个。果然,错误消失了,400个、800个、2000个client,都OK。

总结

  1. 理论还是很重要的。特别是遇到网络层面的疑难杂症,一定要结合理论,根据现象去推导。
  2. 没有头绪的时候,就抓包吧。
  3. 不要在mac上搞压测这种事,测试尽量跟线上保持一样的环境


参考

TCP连接的建立和终止

你可能感兴趣的:(Unix网络编程)