现象:服务端发现了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 呢?

这次案例发生的原因在客户端代码这里,需要客户端去做代码修复

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐