TCP RST报文解析:Connection reset by peer从原理到排查实践

发布时间:2026/10/22 15:05:01
TCP RST报文解析:Connection reset by peer从原理到排查实践
“Connection reset by peer”这大概是后台开发最常碰到的报错之一。客户端发请求服务端直接甩出来一个TCP RST日志里留一句“由对端重置连接”剩下就是一屋子人查防火墙、查服务状态、查网络折腾半天发现可能只是某个参数没配对。RST全称TCP Reset中文叫“复位报文”。它是TCP协议里最雷厉风行的角色不需要协商、不需要确认一个包过去连接当场死亡。很多人习惯把RST和网络故障划等号一看到抓包里出现RST就“网络坏了”但RST在正常业务里其实非常常见有时候甚至是负载均衡健康检查故意发出来的。这篇文章不聊虚的就针对“RST从哪来、怎么定位、怎么处理”这套问题梳理我在实际排查中遇到的几种典型场景。无论你是后端开发、运维还是做网络监控看完应该都能建立一套自己的排查思路下次看到RST不再慌。1. RST是什么它和FIN到底哪里不一样1.1 FIN是好好告别RST是直接摔门正常关闭走四次挥手主动方发FIN对端回ACK然后反向也来一轮FIN/ACK双方优雅地把连接关掉。这个过程的潜台词是“我数据发完了你那边也确认完了咱俩好聚好散”。RST完全不是这个套路。它相当于一个人连“再见”都不说直接把门摔上。收到RST的一端内核会立刻丢弃这个连接的一切状态接收缓冲区里没来得及读的数据丢掉发送缓冲区里还没确认的数据也丢掉连接直接从连接表里消失。整个过程不走TIME_WAIT也不需要对方确认。我把FIN和RST的区别整理成一张表排查的时候对照着看会很直观维度FIN四次挥手RST复位含义有序关闭双方数据已传完异常终止连接立即作废标志位FINRST是否需要确认需要ACK逐次确认不需要收到即生效缓冲区处理等对端收完再关未读数据直接丢弃TIME_WAIT主动关闭方进入TIME_WAIT不走TIME_WAIT立即清理常见触发应用正常close()数据已发完端口未监听、协议异常、主动放弃很多场景下业务日志里出现“Connection reset by peer”本质就是你收到了对端发来的RST。但RST究竟是对端故意发的还是网络中间设备发的还是内核“顺手”发的就是我们后面要拆解的重点。1.2 有效RST和无效RST先别急着下结论这里有个特别容易被忽略的细节RST不一定“有效”。早年协议栈对RST的校验很宽松攻击者只要伪造一个源IP和端口再猜一个序列号就可能把一条正常连接打断这叫“盲RST攻击”。后来业界改了规范核心思路就是收到RST时内核要检查它的序列号是否落在当前连接的接收窗口里。序列号对得上才认对不上直接丢弃连接继续跑。所以你现在抓包可能会看到一种现象抓包里确实有不少RST包但服务一点没受影响业务完全正常。这种大概率就是无效RST——某些扫描工具或者安全设备在猜序列号猜不中就瞎发。排查时别一看到RST就跟业务故障挂钩先确认它有没有“被内核接受”。判断方法很简单但不一定直观如果这条连接随后还正常收发数据或者根本没断那这个RST就是无效的。如果紧接着出现连接断开、发送失败那才是有效RST,才需要我们继续追。2. 工作里最常见的几种RST场景2.1 端口没监听内核替服务说“不”这是最基础、也最常见的一种。客户端connect()到一个端口但目标机器上根本没有进程监听这个端口内核协议栈看一眼监听表直接回一个RST意思是“这门没开别敲了”。抓包表现非常明确客户端发SYN服务器回RST通常带ACK标志即RST,ACK而且这个RST几乎瞬间就回来了客户端connect()立刻失败。应用层看到的错误是ECONNREFUSEDerrno 111不是超时。这里有个和其他场景的重要区别RST是“明确拒绝”“速度快”而防火墙丢弃则是“石沉大海”SYN一直重传、客户端一直等最后超时报ETIMEDOUTerrno 110。判断到底是“没监听”还是“被防火墙丢了”看抓包里有没有RST就行。实际排查中我踩过一个坑服务明明在跑但监听的是127.0.0.1客户端从外网连内核发现目标IP是本机但没有对应监听一样回RST。所以看到RST别默认“服务挂了”先看一眼ss -tlnp确认监听地址对不对、监听没监听。很多时候是配置里bind的地址写错了。2.2 accept队列满门开着但柜台没人办理如果说“端口未监听”是这扇门根本不存在那accept队列满就是“门开着但前台柜台没人处理队伍排到门外新客户直接被劝退”。TCP三次握手完成后连接不会直接进入应用而是先放在内核的全连接队列accept queue里等应用调用accept()取走。如果应用accept得太慢队列满了新来的连接就放不进去。Linux默认行为是把三次握手的最终ACK悄悄丢掉客户端那边就会觉得握手没完成继续重传SYN/ACK。但如果你启用了某个内核参数net.ipv4.tcp_abort_on_overflow1那队列满时内核会直接回RST客户端看到的症状就从“连接建立超时”变成了“连接被重置”。抓包特征能看到正常的SYN、SYN-ACK但握手收尾阶段出现RST。日志上客户端报“Connection reset by peer”服务端自己可能没什么明显异常——因为accept循环还在跑只是忙不过来。排查方法很直接ss -lntp看监听队列注意Send-Qbacklog上限和Recv-Q当前堆积的连接数。如果Recv-Q长期接近Send-Q说明应用accept速度跟不上请求在排队。另一个辅助指标是netstat -s里的SYN重传计数在涨。这个场景的处理思路不是无脑调大backlog。backlog调大只是把“排队区”扩宽如果应用本身处理不过来排队区再宽也迟早会满。真正要做的是优化accept循环、增加worker进程、或者排查业务里有没有慢查询导致连接占用过久。2.3 close()时机不对数据还没读完就关门这种情况非常阴因为它完全符合代码里“正常关闭”的逻辑但内核不这么认为。举个例子服务端处理完请求调用close()准备关闭socket。但如果这时候接收缓冲区里还有客户端发来但没被读取的数据Linux内核不会发FIN而是直接发RST。原因也好理解TCP协议要求如果连接关闭时还有未处理的数据就不能发FIN——因为FIN代表“我已读完了你发的所有数据并处理完毕”明显不是事实。这时候发FIN会误导对端RST才是诚实的这些数据我不要了连接强制终止。业务表现上客户端会收到“Connection reset by peer”而不是正常的EOF。如果你的日志里经常出现对端报RST而你的代码看起来是正常close的那就要检查一下close之前有没有把接收缓冲区的数据读完比如有没有可能客户端发来的请求body只读了一半就开始处理并close抓包特征这个场景最迷惑的地方在于连接可能已经稳定运行了很久数据交换也正常但就在close这一刻预想中的FIN没出现取而代之的是RST。它的触发点非常局部所以很多团队排查了好几天才发现是某个业务分支写的close()逻辑有问题。另外一个相关的坑是某些框架在收到客户端异常中断时会主动close但socket里还残留着没读完的数据内核顺手就回一个RST两个方向都被打断错误日志就更多了。遇到这种情况与其纠结内核行为不如先改代码确保close前把该读的都读完。2.4 SO_LINGER0主动给自己挖RST坑还有一种RST是应用自己“作”出来的。SO_LINGER这个socket选项用来控制close()之后的行为。如果设置SO_LINGER且linger时间为0那么close()被调用时内核会立即发送RST彻底跳过FIN握手和TIME_WAIT状态。很多人为了应对大量短连接导致的TIME_WAIT堆积就想用这个办法“秒关连接”。表面上看效果确实立竿见影端口马上释放没有TIME_WAIT了新连接不会被旧连接卡住。但代价极其容易被低估RST会直接打断对端还没发完的数据和还没确认的数据业务层表现就是莫名其妙地“Connection reset by peer”。如果只是简单场景也许能忍但一旦连接上还有进行中的请求这个RST的破坏力就完全暴露了。我的建议是别为了消TIME_WAIT去无脑SO_LINGER0。TIME_WAIT虽然看起来占着端口但它在TCP设计里有重要的兜底作用防止旧连接的迟到数据包串到新连接。想减少TIME_WAIT的负面影响优先考虑连接池、HTTP keep-alive、调大本地端口范围这些方式不会带来RST这种破坏性副作用。2.5 对端重启或进程崩溃连接表没了旧连接全变“非法”服务运行得好好的突然服务器重启、容器漂移或者进程core dump后新进程起来——内核里的TCP连接表被清空了。问题是客户端那边完全不知情它还维持着一条“自以为健康”的连接。等到客户端在这条旧连接上继续发包哪怕只是一个窗口探测包服务器这边查连接表查无此连接于是按照TCP规范直接回一个RST。客户端看到这个RST一脸懵刚才还聊得好好的怎么说翻脸就翻脸这种场景有两个明显的排查特征。一是时间点特征RST出现的时间总是和某次重启、重新部署、网络配置变更高度重合。拉出服务器启动时间、容器重启时间和抓包时间一对比基本就能对上。二是方向特征在抓包里看RST往往是从“重启过的那一端”发出来的而不是从“没变过的那一端”。我处理过几次类似问题最后都指向同一个结论不要相信长连接能永远活着。对长时间运行的TCP连接最好靠业务层的心跳或者TCP的keepalive机制尽早感知对端死亡而不是让连接一直悬着等到真正发包时才发现已经断了。RST在这里其实是在帮你快速发现故障只是信息的“送达方式”有点粗暴。2.6 重传超时内核给连接判了死刑但RST可能迟到这个场景最容易被调包侠看漏。假设客户端发了一包数据服务器一直没回ACK。发送方会按指数退避算法不断重传比如1秒、2秒、4秒、8秒……一直重传到超过上限Linux里是net.ipv4.tcp_retries2默认大概坚持15分钟内核最终放弃把连接标记为死亡并通知应用层返回ETIMEDOUT。注意这一步内核通常不会主动发RST它只是默默把连接清掉。但故事到这里往往还没结束。如果客户端这边已经放弃了服务器那边却因为网络恢复后又开始发送旧数据客户端内核一查连接表没有这条连接。于是它回一个RST。也就是说RST可能出现在“重传超时”很久之后而且方向是从“先放弃的一方”发出来的。所以你在抓包里会看到一串重传然后安静很长一段时间突然冒出一个RST。别只看最后一个包往前面翻一翻真正的凶手是一连串重传。日志层面两个错误码则会同时出现一边是ETIMEDOUTerrno 110另一边是ECONNRESETerrno 104。排查这种场景的要点是RST只是最终症状重传计时器才是根因。重点要查这段时间内为什么ACK一直回不来——是网络丢包、对方CPU打满、还是某个中间设备把包吃了把重传时间段拉出来对照系统负载往往比死盯RST本身更有效。3. 实际动手怎么用抓包让RST“开口说话”3.1 tcpdump的正确姿势排查RST的第一步永远是抓包。抓包别乱抓命令写对了能省一半力气。最基础的抓RST命令sudo tcpdump -i any tcp[tcpflags] tcp-rst ! 0 -nn -S-i any抓所有网卡省得看流量到底从哪个口进来。-nn不做DNS和端口名解析避免额外噪音。-S显示绝对序列号。这个特别重要否则看相对序列号时容易误判RST的序号是否有效。tcp[tcpflags] tcp-rst ! 0这是tcpdump的经典过滤写法只抓RST包。如果只想抓某条连接的上下文比如看RST前后到底发生了什么可以把过滤条件带上端口sudo tcpdump -i any -nn -S tcp port 8080 and (tcp[tcpflags] tcp-rst ! 0 or tcp[tcpflags] tcp-syn ! 0)这样能同时抓到这个端口上的SYN和RST方便判断是“握手被拒”还是“传输被打断”。抓包时我习惯顺手-w rst.pcap把原始包存下来因为很多问题不是当场能看明白的存成pcap文件丢给Wireshark慢慢看比在终端里翻滚动输出舒服得多。3.2 三步判断RST是谁发的、为什么发拿到抓包结果我会按三个层面去看。第一步看方向。RST是从客户端发到服务器还是反过来的结合业务日志基本能确定“谁先翻脸”。如果两边都有RST就更可能是中间设备干的。第二步看RST出现的位置。是握手阶段、传输阶段还是本该挥手的地方三个阶段对应完全不同的原因握手阶段出现RST优先怀疑端口没监听、防火墙拒绝传输阶段出现RST优先怀疑对端进程崩了、连接表被清空、或者重传超时挥手阶段出现RST往往和close()时机不对、SO_LINGER设置有关。第三步看序列号。这一步能帮你区分“有效RST”和“无效RST”。如果RST的序列号落在当前连接接收窗口内它是被内核接受的如果是个离谱的值那多半是扫描器或者安全设备瞎猜的。看到后一种基本可以安心这个RST不是业务故障的根因。3.3 不抓包也能初步定位的系统命令有些时候你没法在业务机器上抓包或者问题已经过去了只能靠系统积累的数据反推。这些命令值得收藏# 看新连接队列积压情况 ss -lntp | grep 8080 # 看每条连接的存活状态和重传情况 ss -tin state established ( dport :8080 ) # 看系统累积的RST统计 netstat -s | grep -i resetnetstat -s的输出里通常能看到几类计数“connections reset due to unexpected data”“resends”“resets received”等。如果RST计数在增长但具体到某条连接又看不出规律说明可能是全局性策略在网上做手脚。配套的日志排查也别拉下。Nginx节点上如果连接被对端重置error.log里多半会有“upstream prematurely closed connection”之类记录应用层如果是Java/Python生态常见异常就是“Connection reset”和“Broken pipe”。这些日志信息虽然不能直接告诉你RST怎么来的但能帮你圈定时间和涉及节点。3.4 Wireshark里进一步确认细节把pcap拖进Wireshark我最常用这几个操作显示过滤打tcp.flags.reset 1立刻把所有RST包挑出来。点开一个RST包看TCP头里Sequence number和Acknowledgment number和我们上面的三步判断法对一下。对可疑连接右键Follow - TCP Stream看完整对话确认RST之前双方到底聊到哪一步。用Statistics - TCP Stream Graph - Time-Sequence Graph画时序图看RST是不是发生在某个时间拐点。Wireshark还有一个方便的地方它会在Expert Info里自动标注“Connection reset (RST)”点开就能跳到对应位置尤其适合抓包文件比较大的时候。4. 几个容易被误判成“故障”的RST藏得很深4.1 健康检查RST是它的“正常工作”这是所有RST排查里最冤的一个。负载均衡器做TCP健康检查时常见的做法是发一个SYN收到SYN-ACK后认为服务存活然后立刻断开这个探测连接。有些LB实现为了不留下TIME_WAIT直接发RST来断。于是在你的抓包里就会看到规律的RST不断出现而且源IP是LB的地址。判断方法也很明确RST出现的时间和频率高度固定比如每5秒一次、每10秒一次而且往往伴随成对的SYN和SYN-ACK。这种就不用处理反正在抓包里看到规律RST先想想是不是探活流量别一上来就报故障。4.2 中间设备“越权”发RST跨网络的链路里往往藏着不少“热心肠”设备防火墙、入侵防御系统、上网行为管理、DDoS清洗设备。它们都可能在认为连接“有问题”时主动朝两个方向各发一个RST把连接掐断。这种RST和内核发的有明显的不同有时候序列号完全不在双方协商的窗口范围内属于“无效RST”但因为切断的是路径上的数据流所以还是会打断连接。特征是两个方向几乎同时出现RST且时间点可能跟某个策略比如连接空闲超时挂钩。排查手段只有一个靠谱在链路两端比如客户端机器和服务端机器同时抓包对比时间戳。RST如果两边同秒出现且不在正常协议序列里那就是中间设备干的靠单边抓包很难实锤。4.3 TIME_WAIT和RST的取舍每个做网络的人都会和TIME_WAIT打交道。TIME_WAIT是主动关闭方在发送最后那个ACK后进入的状态持续2MSLLinux上通常默认60秒。它的存在是为了确保旧连接的迟到数据包不会污染到新连接——这是TCP协议安全性的基石。可代价也肉眼可见高并发短连接场景下大量socket卡在TIME_WAIT端口不够用。于是有人动歪脑筋用SO_LINGER0强行绕过TIME_WAIT代价就是RST满天飞。这类RST危害主要体现在对端如果有数据还没发完直接被砍断业务表现为莫名其妙的重置。实际上绕开TIME_WAIT的办法很多完全不需要用RST这种“杀敌一千自损八百”的方式。客户端这边把本地端口范围调大服务端那边用连接池复用长连接都能把TIME_WAIT压力降下来。不到万不得已我不会在生成环境开SO_LINGER0。4.4 别把RST和“超时”搞成一锅粥排查问题的时候我经常看到同事把超时报错和RST混在一起讨论其实这俩性质完全不同。超时ETIMEDOUT的本质是我等了很久什么回包都没等到。可能是对端消失了、网络黑洞了、丢包没人管。真实世界里它就像你一直敲门但屋里毫无反应。RST的本质是对方存在并且明确告诉我“不行/结束”。哪怕它是中间设备伪装的“对方”它也代表一个实体回话了。判断依据很简单看抓包里有没有一个“回应包”。有回应但内容是RST那是明确拒绝完全没有回应那是超时。两个错误码分开记排查思路完全不同。5. 排查速查表和几条实在话5.1 场景-特征-处理方向速查表场景抓包典型特征常见日志/错误码处理方向端口未监听SYN后立即回RST,ACKECONNREFUSED (111)查ss -lntp确认端口和监听地址accept队列溢出握手收尾出现RSTRecv-Q堆积客户端connect重置/超时加worker、优化accept别只调backlogclose时未读完数据正常连接中突然RST替换FINConnection reset by peer改代码close前读完接收缓冲区SO_LINGER0close()立刻RST无FIN对端报reset尽量别用改用连接池/调大端口范围对端重启/崩溃RST发生在重启、部署后stuck连接reset业务心跳及时感知清理死连接重传超时后遗症先一串重传再出RSTETIMEDOUT ECONNRESET查重传时间段的丢包和负载健康检查规律RST源IP是LB无业务报错确认是LB探活不用处理中间设备注入双向同时RSTseq异常偶发reset且时间诡异双端抓包对比找网络策略5.2 我现在遇到RST会怎么处理经历了这么多轮RST排查我现在的习惯已经固化成一套流程分享出来供参考。第一步不查防火墙先看方向。RST是谁发给谁的这决定了排查往哪个方向走。第二步看序列号有效性。无效RST直接略过别浪费时间。第三步对照时间点。是不是刚部署、刚重启、刚调过网络参数时间点对上了问题基本锁定一半。第四步再查系统统计和日志。netstat -s的计数、应用error log、LB健康检查记录把这些拼起来。最后才轮到中间设备因为排查中间设备成本最高要拉多方抓包。我还特别建议团队里有条件的话把RST统计做成一个每日巡检指标。Linux的procfs提供了现成的计数器脚本定期拉一次netstat -s里的reset相关字段做趋势图哪天突然涨了就说明有网络异常或配置变更尽早发现比事后抓包轻松太多。RST说到底不是玄学它是TCP协议里一个再普通不过的控制信号。搞明白它背后对应的是哪一层的行为问题就解决了一大半。下次日志里再跳出“Connection reset by peer”按这套思路走一遍你会发现自己淡定了很多。