大家好,又见面了,我是你们的朋友全栈君。
问题描述
这两天用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关闭了连接。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_overflow内核参数(默认为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。
总结
- 理论还是很重要的。特别是遇到网络层面的疑难杂症,一定要结合理论,根据现象去推导。
- 没有头绪的时候,就抓包吧。
- 不要在mac上搞压测这种事,测试尽量跟线上保持一样的环境
参考
TCP连接的建立和终止
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
发布者:全栈程序员栈长,转载请注明出处:https://javaforall.cn/187093.html原文链接:https://javaforall.cn