资讯动态

TCP重传机制全解:RTO、dup ack与SACK实战排障指南

发布时间:2026/10/5 2:56:56 来源:尧图企业网站定制
做网络抓包这些年我没少被同事拉着看同一张截图干净的 TCP 流中间突然冒出一溜红色标记的重传包。网络应用一慢大家第一反应是看丢包率丢包率一高TCP 重传机制就免不了被拉出来反复讨论。但真正能把它讲清楚、用明白的人说实话不多。TCP 重传机制是 TCP/IP 协议栈里保证可靠传输的核心手段。说直白一点TCP 一条连接就是一个“承诺我不会丢东西”的管道而这个承诺不是靠网络本身是靠重传机制来兜底。无论你是在做后端服务、写嵌入式网络程序还是在调网关和负载均衡都绕不开它。这篇文章我会从协议原理、参数计算、抓包验证到故障排查完整拆一遍 TCP 重传机制希望能帮你在看到“红色包”时不慌也能在面试聊到 dup ack 时多说几句有用的东西。1. 重传机制到底在解决什么问题1.1 TCP 的可靠性承诺从哪来先回到最基础的问题为什么 TCP 要重传因为底层网络是不可靠的。IP 层的职责只是尽力把数据包送到对端丢包、乱序、重复、损坏都可能发生。TCP 为了让上层应用感觉不到这些“底层噪音”设计了一套确认-重传机制。发送方发出数据包后接收方如果正常收到就回一个 ACK告诉发送方“这个序号的数据我收到了”。发送方如果在一段时间里没收到 ACK就会合理地怀疑数据在中间丢了然后把数据重新发一遍。这个动作就是 TCP 重传机制的核心。“确认”和“重传”是成对出现的没有 ACK 做锚点重传就无从谈起。这里有个常见的认知误区很多人觉得重传只是为了“丢包补偿”其实它还承担着更重要的信号作用。重传本身是对网络拥塞的一种反馈发送方看到超时就会把发送速率降下来避免把链路打得更死。所以 TCP 重传不是一个独立模块它是可靠传输和拥塞控制两条线的结合点。1.2 重传不是孤立机制它和窗口、乱序纠缠在一起TCP 是滑动窗口协议。发送方不用每个包等一个 ACK而是可以连续发一堆数据窗口大小决定了“还没确认但已经发出去”的数据上限。重传机制就建立在这个滑动窗口框架里。窗口的存在让“重传谁”这个问题变得微妙起来。比如你发了 seq 1 到 seq 10 共 10 个包中间丢了一个 seq 5。如果接收方收到乱序包也照样确认那发送方能收到的 ACK 大概率是“seq 4 之前的都到了”而 seq 5 之后那一堆包虽然到了却因为没有连续到达而无法被确认。这时候发送方怎么判断是靠超时重发全部还是只补发 seq 5这就是重传策略要回答的问题。乱序也会干扰重传判断。假如某一段数据只是走了不同路径迟到了一小会儿并没有真的丢失发送方却急着重传就会造成重复数据又增加网络负担。所以说重传机制必须精细控制“什么时候重传、重传哪一段、重传多少次”粗一点就会额外浪费带宽太保守又会让应用等得太久。1.3 最容易触发重传的几类场景根据我实际处理过的故障常见的重传触发场景大概有这么几类链路丢包物理线路质量差、光模块衰减、无线信号干扰都会造成数据包在中间被丢弃。网络拥塞路由器或交换机缓存打满来不及转发的包会被主动丢掉。中间设备误删安全设备、防火墙、负载均衡误判把符合某些策略的报文丢弃。缓冲区溢出接收端处理不过来内核缓冲区装满后来到的数据只能被丢弃。路径切换和乱序冗余链路倒换、ECMP 哈希变化导致同一连接的包走了不同路径顺序乱了接收端的窗口机制会触发送方的一些误判。重传多了不一定是坏事但大面积、持续性的重传一定是网络健康度下降的信号。排障的第一件事就是在抓包里把重传的类型、时机和分布看清楚而不是先上来调参数。2. RTO 估算与定时器重传引擎的核心2.1 RTT 采样是怎么做的重传机制里最关键的参数是 RTORetransmission Timeout重传超时时间。RTO 定得短一丢包就重传但容易造成重复报文RTO 定得长能容忍网络波动但应用等待时间会变长。RTO 不能是一个拍脑袋的固定值它必须跟随网络实际状态动态调整这就产生了 RTTRound Trip Time往返时间采样。RTT 怎么测发送方记下某个数据包发出的时间等对应的 ACK 回来这段时间差就是一次 RTT 样本。听起来简单实际操作有一堆脏活如果报文本身是重传的它的 ACK 对应的是哪一次发送算错了会把 RTT 样本污染掉。所以协议栈在采样时会有过滤规则收到 ACK 时如果对应报文存在“之前重传过”的情况这个样本就不能直接用于 RTT 计算。经典的 Karn 算法就是用来处理这种重传二义性的我在调优时也很关注这一点因为很多协议栈行为差异其实就出在样本筛选逻辑上。2.2 RTO 计算过程与 RFC 6298现代 TCP 对 RTO 的估计算法来自 RFC 6298继承了 Jacobson 提出的经典方法。它维护两个状态平滑后的 RTT 估值 SRTT 和 RTT 抖动 RTTVAR。连接刚建立时拿到第一个 RTT 样本 R初始化公式如下SRTT - R RTTVAR - R / 2 RTO - SRTT max(G, 4 * RTTVAR)这里的 G 是系统时钟粒度。很多系统把初始 RTO 定为 1 秒就是为了容纳最差情况。之后每收到一个新的 RTT 样本 R按下面两个公式更新RTTVAR - (1 - beta) * RTTVAR beta * |SRTT - R| SRTT - (1 - alpha) * SRTT alpha * R RTO - SRTT max(G, 4 * RTTVAR)其中 alpha 取 1/8beta 取 1/4。这段公式的意思用大白话讲就是SRTT 是“最近一段时间往返时长的加权平均”RTTVAR 是“这种平均值的波动程度”。RTO 不仅看平均网络延迟还要把抖动也考虑进去网络延迟越不稳定RTO 就会越大避免因为抖动而频繁超时。RTO 还有个下限设置Linux 内核里通常是 200ms 左右。这是为了避免在局域网低延迟场景下 RTO 缩到不合理的范围。但也别追求超低 RTORTO 太激进反而会制造一堆没必要的小重传。2.3 超时重传的指数退避如果超时了重传后还是没收到 ACK下一个 RTO 不是维持原值而是指数退避RTO 每重传一次就翻倍直到一个上限标准里一般建议最大不超过 60 秒。这个退避逻辑是为了避免“重传风暴”。假设网络已经拥塞发出去的包全部丢失如果发送方还按原节奏快速重传只会把拥堵搞得越来越严重。指数退避相当于告诉协议栈情况不妙别莽等一等再说。具体到 Linux 语境tcp_retries1和tcp_retries2就是两个控制重传次数的关键参数。注意这里的“次数”和实际等待时间不是简单的乘法关系因为每次超时等待时间会翻倍所以同样 15 次重试系统实际等待时长可能远超你想象。这也是我经常提醒开发同事的一句话如果你在应用层设了 10 秒超时但内核还在耐心地按指数退避重传应用层早就报错了两边感知完全对不上。2.4 Linux 协议栈里的相关参数调整实际操作中我建议先了解这几个参数再决定要不要动它们。它们的默认值对绝大多数场景是合理的改之前必须有抓包数据和业务场景做支撑。参数默认值作用net.ipv4.tcp_syn_retries6客户端 SYN 重传次数上限net.ipv4.tcp_synack_retries5服务端 SYN-ACK 重传次数上限net.ipv4.tcp_retries13超过该次数协议栈认为路由可能有问题会尝试刷新路由net.ipv4.tcp_retries215超过该次数协议栈彻底放弃连接向应用层报错net.ipv4.tcp_sack1是否启用选择性确认默认开启net.ipv4.tcp_timestamps1是否启用 TCP 时间戳选项用 sysctl 查看也很简单sysctl net.ipv4.tcp_syn_retries sysctl net.ipv4.tcp_retries2网上很多教程喜欢直接把tcp_retries2调大或调小。我的建议是如果不是特殊业务先别动。默认值是经验平衡下来的结果。你真正需要关注的是重传定时器和 RTO 下界的配合以及应用层超时设得是否合理。3. 快速重传、dup ack 和 SACK如何把重传做快、做准3.1 什么时候走快速重传超时重传有一个天然缺陷等待 RTO 的时间可能太久。特别是在 RTO 已经因为抖动被拉大的时候丢一个包可能要等好几秒才能补回来这对交互型应用是灾难。TCP 因此设计了快速重传机制用接收方的反馈来提前触发重传。快速重传的前提是接收方能感知“数据断了”。由于 TCP 是累积确认接收方收到乱序数据时会先缓存下来但 ACK 里只能确认连续到达的部分。比如发送方发了 1 到 100 个包其中 50 号包丢了51 号到 100 号包都到了。接收方不会单独确认 51 到 100它会反复回复“我最后连续收到的是 49 号”也就是 ACK 序号停在 49。发送方看到多个这样的重复确认自然能推测 50 号包丢了于是不需要等定时器超时直接补发 50 号。3.2 dup ack 怎么数才算数这里就是热门搜索词“TCP dup ack机制”的核心。一个重复 ACK 可能只是乱序造成的毕竟网络中一个包稍微迟到几毫秒接收端也会先发出重复确认。为了过滤这种假象协议栈规定发送方连续收到 3 个相同的重复 ACK 时才判定数据丢失并启动快速重传。注意是“累计 3 个”也就是说在原始 ACK 之外再收到 3 个相同的 ACK。这 3 次的阈值不是随便定的它相当于给乱序留了缓冲。在实际抓包中你会看到这些重复 ACK 的包都被 Wireshark 标记为TCP Dup ACK。如果只看到一两个 dup ack 后就恢复正常通常只是路径上有轻微乱序不值得紧张。快速重传触发后还会连带着做一件事拥塞窗口减半进入快速恢复阶段。这不是单纯地把丢了的包补上就完事了而是告诉发送方“网络可能遇到问题你少发点”。所以从这个角度看快速重传不只是一个可靠机制也是拥塞控制的一部分。3.3 SACK 把重传粒度缩小到数据块如果没有 SACK发送方收到重复 ACK 之后只能猜测“到底从哪个序号开始重传”。它往往会把 50 号之后所有没确认的包全部重发一遍即使 51 号到 100 号其实已经安全到达接收端。这种低效在窗口中大量数据在途时尤其明显。SACKSelective Acknowledgment选择性确认选项解决的就是这个问题。接收方会在 ACK 里附带 SACK 块告诉发送方“哪些非连续的段我已经收到了”。比如 51-60 和 70-80 都到了SACK 会把这两段的位置信息带回来。发送方拿到之后只重传真正缺失的 61-69极大节省带宽也缩短了数据恢复时间。抓包里看到SACK标记通常出现在 ACK 选项区域。Linux 默认tcp_sack1但某些中间设备或老版本系统会把它移除。如果排查时发现所有机器都没有启用 SACK先确认是不是两端配置或中间 NAT 设备把 TCP 选项剥掉了。3.4 DSACK 与重传误判DSACK 是 SACK 的扩展接收方用它告诉发送方“你重传的数据我已经收到过了这是重复的”。发送方看到 DSACK 后能意识到可能是自己误判了丢包或者网络路径上存在复制包。DSACK 一个很大的作用是帮助协议栈调整对乱序的容忍度。如果系统频繁因为乱序触发假重传DSACK 反馈会让重传触发阈值变得更保守。这算是一种自学习机制。作为排障者你看到 DSACK 出现就应该思考网络是不是存在乱序或负载均衡路径不均衡的问题。4. 三次握手、四次挥手里的重传细节4.1 SYN 重传连接一上来就卡住怎么办很多人只关心数据传输阶段的重传但连接建立阶段同样有重传问题。三次握手的第一步是客户端发 SYN如果服务端没有回应客户端内核会按照tcp_syn_retries指定的次数重新发 SYN并且等待时间同样是指数退避。一个容易被忽视的事实是TCP 重传机制在这些阶段依然在底层自动运转应用进程不需要参与也感知不到“我刚才发了几个 SYN”。所以你在应用层看到“连接超时”但内核可能已经默默尝试了好几次。抓包时如果一直看到相同序列号的 SYN 重发说明服务端根本没有响应。此时问题大概率出在中间网络设备丢弃 SYN 包、服务端监听队列满或者防火墙拦截了 SYN-ACK 回包。服务端半连接队列满也是 SYN 重传的一个常见诱因。当 accept 队列和半连接队列都被占满内核会丢弃新来的 SYN。这时候客户端看到的不是立刻拒绝而是长时间的 SYN 重传。不少 Nginx 反向代理在高峰期出现连接建立慢原因就在这个队列上。解决思路不是调tcp_syn_retries而是要调整 backlog 和net.core.somaxconn把队列容量提上去让服务端能更快地响应新连接。4.2 SYN-ACK 重传与半连接队列服务端发出 SYN-ACK 后如果没收到客户端的最终 ACK也会重发 SYN-ACK次数由tcp_synack_retries控制。这个阶段对服务端特别折磨因为每个半连接都会占着一个小小资源如果攻击者故意不完成握手服务端会一直为这些半连接保留状态直到重试耗尽或者超时。排查时我常用ss -lnt看监听端口的 Recv-Q 是否有堆积。如果 Recv-Q 持续大于 0说明应用 accept 的速度跟不上内核完成握手的速度如果 Send-Q 堆积更多是发送缓冲的问题。对于半连接队列溢出调整net.ipv4.tcp_max_syn_backlog和net.core.somaxconn会有帮助但更核心的是应用要尽快把已完成的连接 accept 出来别让握手队列长期积压。4.3 FIN 阶段的重传与 TIME_WAIT四次挥手阶段重传同样存在。主动关闭方发出 FIN 后要等待对端 ACK再等对端 FIN然后回复最后一个 ACK。最后这个 ACK 如果丢了对端会重发 FIN主动关闭方会再补一个 ACK。这也是重传机制在保护连接关闭过程的可靠性。被动关闭方如果一直收不到期望的报文也会按照 RTO 规则重传 FIN直到重试次数耗尽才把连接强制关闭。实际故障里经常可以抓到“一堆 FIN 重传 RST”的组合这种组合往往意味着对端应用已经提前崩溃内核在反复尝试优雅关闭失败后才转用 RST 强制断开。四次挥手之后主动关闭方会进入 TIME_WAIT 状态等待 2MSL两倍最大报文段生存时间。这期间端口不会立刻释放。如果你是写高并发应用的频繁主动关闭连接就会积累大量 TIME_WAIT立刻重连时可能遇到“地址已在使用”。Java 的 Socket 或者 Netty 客户端重连时报 “Address already in use”大多就是这个原因。解决方案通常是在创建 Socket 时设置SO_REUSEADDR让端口允许重用。4.4 “地址已在使用”和“端口不可用”两个高频报错聊到端口问题这里顺便展开说说两个常被搜索到但容易被弄混的报错。Java 客户端重连时报 “地址已在使用”和重传没有直接关系但它出现在 TCP 连接生命周期的边界上。客户端主动断开连接后会进入 TIME_WAIT在 2MSL 时间内同样的四元组不能被重用。如果不设置SO_REUSEADDR客户端端口选择器会避开这些“还在冷却”的端口一旦端口资源被 TIME_WAIT 占满自然就报地址已被使用了。处理办法是启用SO_REUSEADDR或者调整tcp_tw_reuse让内核更主动地复用 TIME_WAIT 连接。另一个是 Docker 启动容器时常见的报错Error response from daemon: ports are not available: exposing port tcp 0.0.0.0:8080这个一般是宿主机端口已经被别的进程或容器占用。TCP 层面它对应的是“服务端无法绑定监听端口”如果你此时去抓服务端的包会收到 RST 或根本没有监听者在回应。解决方法也不复杂先看宿主机端口占用情况再改映射端口或停掉冲突进程。ss -lntp | grep 80805. 重传故障的抓包定位与排查实录5.1 用 tc 模拟丢包五分钟复现重传想真刀真枪地观察重传机制不必非得等生产故障。Linux 自带的 netem 模块可以在本地模拟网络丢包几分钟就能复现一整个重传过程。在测试机上执行# 给 eth0 增加 10% 丢包 sudo tc qdisc add dev eth0 root netem loss 10%然后在两台机器之间跑一个大文件传输比如用 nc 或者 scp。再去抓包你会看到非常明显的 TCP 重传包同样的序列号出现在多条记录里时间戳间隔接近 RTO。想恢复正常网络sudo tc qdisc del dev eth0 root注意别在正式生产环境上乱敲这条命令我曾经见过有人在线上服务器用 netem 模拟延迟忘了删除规则结果整个业务平白无故多出几十毫秒延迟。动手之前先把环境和恢复命令准备好这是基本功。5.2 Wireshark 里看重传的关键过滤方式抓到包之后别用肉眼在一堆列表里找红字直接用 Wireshark 的显示过滤器更高效。过滤器含义tcp.analysis.retransmission超时重传tcp.analysis.fast_retransmission快速重传tcp.analysis.duplicate_ack重复 ACK 包tcp.analysis.out_of_order乱序到达的包tcp.analysis.acks_lost疑似 ACK 丢失比如我想看某一条 TCP 流里的所有重传可以这样tcp.stream eq 12 and tcp.analysis.retransmission需要说明的是Wireshark 的“重传”判定不一定完全准确。抓包点如果在交换机镜像口而不是业务主机上Wireshark 可能把一些重复传输的包误判为重传比如负载均衡的多份拷贝。所以判断重传前先确认抓包位置是否足够靠近真正的收发端最好能同时抓两端再对照两边的时序来下结论。5.3 一个实际排查案例重传激增从哪来之前有一次排查内部中间件延迟飙高的问题现象是应用侧大量超时后端日志里全是连接建立失败。第一轮抓包发现客户端到服务端之间有大量快速重传确认序列号集中在某一段看起来就像是链路丢包。但进一步抓两端数据发现服务端其实早就收到了完整数据只是返回的 ACK 在中间被丢弃了。这时问题不再是单向丢包而是链路另一半存在问题。调整排查方向后我们在机房交换机的策略路由配置里找到了原因一对 ECMP 链路里有一条拥塞严重流量哈希不均衡导致 ACK 走了一条差链路。这个案例想说明两点一是不能看见重传就去怀疑带宽和物理线路要结合重传的方向来判断二是抓包最好两端一起抓。重传的确认方向和丢包方向比单纯看重传次数更能定位问题。6. 重传机制带来的几个调优空间6.1 RTO 下界与抖动容忍内核里tcp_rto_min有一个下限值默认在 200ms 左右。对跨机房专线或者同城双活的 RTT 只有几毫秒的场景这个下限偏大。RTO 太大意味着丢包后要干等 200ms 才反应业务感知不到但性能提升有限RTO 太小又容易误判。这个参数一般不建议全局调因为不同目标 IP 的距离差异很大。你要是真想优化某条特定链路的 RTO 表现更靠谱的手段是启用 BBR 或其他对丢包容忍度更高的拥塞控制算法它们对重传行为的干预更平滑。6.2 SACK 开启情况和中间设备SACK 已经是现代 TCP 的标配但一些硬件负载均衡和老版本系统可能会把 TCP 选项剥掉。你可以在抓包里看 ACK 的 Options 区域有没有 SACK 字段。如果没看到且重传效率明显低需要检查两端系统是否都开了tcp_sack。还要警惕某些中间设备对 SACK 选项处理不当导致报文被误判为畸形包而丢弃。这种问题在跨运营商传输时偶尔遇到排查手段就是两端直接打流对比。6.3 连接级与全局级视角最后想再分享一个排障视角。我们常说重传率是网络健康度的晴雨表但单看某一个连接的重传没有任何意义。重传必须放到流量总量、并发连接数、时延分布里去判断。比如一个连接里重传了 10 个包在总发送 1000 个包的情况下重传率只有 1%完全可以接受。但如果一个连接总共发了几十个包却重传了 10 次那就要紧张起来。另外还要区分 SYN 重传和业务数据重传前者更多指向连接建立路径的问题后者更可能与数据面链路质量有关。我通常先把基础指标拆成“建连阶段重传”和“传输阶段重传”两个维度去看这比单看一个总数合理得多。做网络协议栈相关的工作重传机制不可能绕开。它不是教科书里那些冷冰冰的公式而是所有 TCP 可靠传输行为的地基。理解了 RTO 怎么算、dup ack 怎么触发、SACK 怎么节省带宽你排查网络问题时的思路会清晰很多。下一次抓包再看到满屏红色先别急着甩锅给链路看眼重传种类和方向很多答案其实就在里面。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑