网络:connect reset by peer是怎么回事
现象:服务端发现了connect reset by peer
我们在做一些应用排查的时候,时常会在日志里看到跟 TCP 有关的报错。比如 connection reset by peer“连接被对端 reset(重置)”,这个字面上的意思是看明白了。但是,心里不免发毛:
- 这个 reset 会影响我们的业务吗,这次事务到底有没有成功呢?
- 这个 reset 发生在具体什么阶段,属于 TCP 的正常断连吗?
- 我们要怎么做才能避免这种 reset 呢?
要回到这类追问,光靠日志就不行了。
事实上,网络分层的好处在于每一层都只要做好自己的事情就可以了。而坏处就比如当前的这种情况:应用层只需要操作系统告诉它:“你的连接层被reset了”。但是为什么会reset呢?应用层无法知道,只有操作系统知道,但是操作系统只是把事情处理掉,往内部reset计数器+1,但是也不记录这次reset的上下文。
怎么办呢?我们就需要深入到网络层,将应用层的现象和网络层联系起来
怎么做?
- 我们需要选择一端做抓包,这次是客户端;
- 检查应用日志,发现没几分钟就出现了 connection reset by peer 的报错;
- 对照报错日志和抓包文件,寻找线索。
我们看下报错日志是怎么样子的
可以看到:
- recv() failed:这里recv是linux网络编程接口,用来接收数据的。然后我们man recv,可以看到这个系统调用的详细信息以及异常状态码
- 104:在man手册中可以看到,104 对应的是 ECONNRESET,这也是一个TCP连接被RST报文异常关闭的情况
下面开始分析抓包文件
握手阶段发生的RST
以IP为条件的过滤器
ip.addr eq my_ip:过滤出源IP或者目的IP为my_ip的报文
ip.src eq my_ip:过滤出源IP为my_ip的报文
ip.dst eq my_ip:过滤出目的IP为my_ip的报文
与TCP 标识位有关的过滤器
TCP的标识位(SYN、ACK、FIN、PSH、RST等)跟我们需要定位的问题息息相关
比如, connection reset by peer这样的问题就有可能和RST标识位有关。
怎们写呢?我们可以选中任意一个报文,注意到TCP的Flags部分:
因此,RST报文的过滤条件就是:
tcp.flags.reset eq 1
因此,可以写出过滤条件如下:
ip.addr eq 10.255.252.31 and tcp.flags.reset eq 1
这样我们就能看到很多符号条件的RST报文
可以看到,符合过滤条件的报文个数,一共9122个,占所有报文的4%。
找到某个报文所属的其他报文
怎么更加减少呢?任选一个报文,右单击,选中 Follow ->
TCP Stream找,到它所在的整个TCP流的其他所有报文。
比如下面是与172号报文所属的整个 TCP 流的报文:
可以看到,这个RST出于握手阶段:这个RST报文是握手阶段的第三个报文,但是它不是期望的ACK,而是RST+ACK,所以握手失败了。
疑问:这是我们要找的connect reset by peer?
要回答这个问题,我们就要先了解应用程序是怎么跟内核的 TCP 协议栈交互的。一般来说,客户端发起连接,依次调用的是这几个系统调用:
- socket()
- connect()
而服务端监听时,依次调用:
- socket()
- bind()
- listen()
- accept()
服务端的用户空间程序要使用TCP连接,首先要获得上面accept()的返回,而accept()能够返回的前提是正常完成三次握手。
但是,可以看到,第三次握手失败了。握手失败也就不是有效的连接。而connect reset by peer的意思是连接被关闭,连接已经建立起来了。我们要找的是在连接建立后发生的RST
继续过滤
排除掉[握手阶段的RST]
要怎么样才能把握手阶段的RST排除呢?那么我们需要找出握手阶段的RST的特征。
这些RST的序列号是1,确认号也是1,因此可以这样写:
tcp.seq eq 1 or tcp.ack eq 1
所以,过滤器就变成了如下:
ip.addr eq 10.255.252.31 and tcp.flags.reset eq 1 and !(tcp.seq eq 1 or tcp.ack eq 1)
还是有很多。怎么减少范围呢?
以[时间]为过滤器找到[RST]
- 我们一定能从日志中找到是什么时候出现了
connect reset by peer
的。这样我们就可以进一步缩小范围了
比如,发现日志:
于是,我们就可以写出如下过滤器:
frame.time >="dec 01, 2015 15:49:48" and frame.time <="dec 01, 2015 15:49:49"
于是得到的RST报文如下:
接下来要做的就是:对比这三个RST所在的TCP流里的应用层数据(也就是HTTP请求和返回),跟日志中的请求和返回对比,找到到底是哪个RST引起的报错
继续分析
我们先来看看,11393号报文所属的流是什么情况?
然后我们来看一下 11448 号报文所属的 TCP 流。
原来,11448 跟 11450 是在同一个流里面的。现在清楚了,3 个 RST,分别属于 2 个HTTP 事务。它们分别是对应了一个 URL 里带“weixin”字符串的请求,和一个 URL 里带“app”字符串的请求。那么,在这个时间点(15:49:48)对应的日志是关于哪一个 URL 的呢?
你只要往右拖动一下鼠标,就能看到 POST URL 里的“weixin”字符串了。而包号 11448和 11450 这两个 RST 所在的 TCP 流的请求,也是带“weixin”字符串的,所以它们就是匹配上面这条日志的 RST!
为什么我们可以确定这个 TCP 流就是对应这条日志的,主要三点原因:
- 时间吻合;
- RST 行为吻合;
- URL 路径吻合。
现在,我们可以将应用程序和网络报文之间联系起来了
于是,我们可以画出如下状态图:
也就是说,握手和HTTP POST请求和响应均正常,但是客户端在对HTTP 200这个响应做了ACK之后,随机发送了RST ACK,破坏了正常的TCP四次挥手。
这正是这个RST,导致服务端的recv()的调用收到了ECONNRESET报错,从而出现了 connection reset by peer。
这个对应用会有什么影响呢?从上面可以看出:
- 对服务端来说,多了一个报错日志,但是其POST请求还是成功了(因为回复了HTTP 200)(服务端没问题)
- 对客户端来说,那要根据客户端的日志具体分析了(问题出在客户端)
那么,对于开头的三问:
- 这个reset是否影响业务,还需要继续查客户端应用,但是服务端事务是成功被处理了
- 这个reset发生在事务处理完成后,但是不属于TCP正常断连,还需要继续查客户端代码问题
- 要避免这种reset,需要在客户端代码进行修复
ps:客户端用RST来断开连接并不妥当,需要从代码上找原因。比如客户端在Receive Buffer 里还有数据未被读取的情况下,就调用了 close()。对应用的影响究竟如何,就要看具体的应用逻辑了
我们要怎么做才能避免这种 reset 呢?
- close()系统调用会走到tcp_close(),在这个函数里,会做判断,如果buffer里还有数据未读,它就直接调用tcp_send_active_reset(),发出RST,并把这个连接直接设置到CLOSED状态,也就是不进入TIME_WAIT
- 对于这种情况,为了避免close()的时候发出RST,需要检查业务代码,确保在调用close()之前,把接收缓冲区中的数据读取掉
小结
connect reset by peer,意识是对端peer回复了TCP RST(reset),终止了一次连接。
发生这个问题的原因有很多,比如网络不稳定、或者防火墙来几个 RST,也都有可能导致类似的 connection reset by peer 的问题。我们需要具体抓包具体分析
我们需要抓包分析回答出下面的三个问题
- 这个 reset 会影响我们的业务吗,这次事务到底有没有成功呢?
- 这个 reset 发生在具体什么阶段,属于 TCP 的正常断连吗?
- 我们要怎么做才能避免这种 reset 呢?
这次案例发生的原因在客户端代码这里,需要客户端去做代码修复
更多推荐








所有评论(0)