资讯动态

TCP超时重传与RTO算法:从丢包确认到内核参数调优实践

发布时间:2026/10/19 5:06:17 来源:尧图企业网站定制
有一次线上服务半夜超时报警客户端上传文件频繁失败从监控上看TCP重传率奇高。连续调了几天最终的结论不是带宽不够、也不是服务器负载高而是两个细节叠加链路里有偶发丢包加上内核RTO估算跟不上抖动TCP在“该重传的时候犹豫、不该重传的时候乱动”。这让我把《TCP/IP详解 卷一协议》第21章重新翻了一遍。很多人对TCP可靠性的理解停留在一句话发出去的包没收到确认就重传。可实际上“什么时候重传、隔多久重传、重传几次”才是这套机制真正的核心。1. 先搞清楚为什么“丢包”这么难确认1.1 ACK是唯一的反馈信号但信号本身会失真TCP没有“网络层送达回执”它拿到的唯一正向反馈就是接收方的ACK。但ACK本身也是一条IP包同样可能被丢、被延迟、被乱序。更麻烦的是接收方发出ACK的时机由本端协议栈策略决定而不是“收到一个数据包必须立刻回一个ACK”。这带来的直接后果是发送方看到“ACK没来”并不能确定是数据包丢了、ACK丢了、对端太忙没来得及处理还是单纯网络延迟波动。TCP的超时重传本质上就是对这一堆不确定信息做最大概率的推断。把这点想透后面看RTO算法就不会觉得复杂。1.2 三种容易误判的经典场景先看几个在实际抓包里经常遇到的场景。第一种是延迟ACK。TCP的接收方默认不会对每个包都回ACK而是把多个包合并确认最多拖几百毫秒。协议栈通常会在收到连续两个完整数据段时主动取消延迟、立刻发送ACK。但在弱网或高负载情况下ACK间隔偶尔达到几百毫秒是很正常的不等于丢包。第二种是乱序。网络路径存在多队列、多路径时后发的包可能先到。接收方会把乱序的包缓存下来但ACK号不前进并且每收到一个乱序包就重复发送最后一个有序ACK。这就是Dup ACK的由来。一个Dup ACK可能只是乱序两个也大概率是乱序收到第三个时才认为丢包触发快速重传。为什么是3而不是1或2因为普通乱序极少产生超过两个重复ACK这是经验阈值。第三种最隐蔽数据包其实到了回程的ACK丢了。发送方超时重传接收方看到重复数据会立刻回复ACK于是连接恢复但带宽被白白浪费了一次。这种场景靠肉眼很难发现需要靠DSACK重复段确认等机制暴露出来。我把三类现象整理成一张表方便排障时对照抓包信息现象可能原因判断依据ACK滞后几百毫秒延迟确认后面出现合并ACK顺序正常连续1到2个Dup ACK乱序随后ACK号恢复前进无重传第3个Dup ACK后立刻重传真实丢包或严重乱序重传包伴随SACK块出现超时后重传但接收端数据完整ACK丢包或RTO过小DSACK告知收到重复数据2. RTO估计算法从拍脑袋到均值偏差的进化2.1 固定RTO为什么不可用早期TCP实现曾使用固定超时时间。同机房内可能只有1毫秒延迟跨洋链路几百毫秒移动网络更夸张抖动经常到秒级。固定值设小了高抖动链路上会出现大量“假重传”设大了遇到真丢包时又迟迟不重传用户体验非常糟糕。所以问题的本质是RTO必须自适应要跟随当前链路的真实延迟和波动走。这就引出了后来被广泛采用的自适应估算方法。2.2 均值偏差自适应性估算的核心思想现在通行的RTO估算核心就两个量平均RTTSRTT和RTT的偏差RTTVAR。公式是RTTVAR (1 - β) × RTTVAR β × |RTT - SRTT| SRTT (1 - α) × SRTT α × RTT RTO SRTT max(G, 4 × RTTVAR)α一般取1/8β取1/4。这个公式看着吓人其实逻辑很简单SRTT表示“平均来回要多久”RTTVAR表示“平均来回时间抖动的幅度”。RTO不是在平均值上加一点而是把平均加上4倍的抖动幅度。为什么加4倍偏差这就是“大概率不会误判”的边界。网络延迟近似钟形分布均值加4倍方差几乎覆盖了正常范围内的所有波动只有真丢包导致的确认缺失才会突破这个阈值。如果你只加0.5倍偏差正常抖动稍微大一点就会触发重传结果形成重传风暴。为什么用指数加权移动平均而不是简单平均因为网络状态是时变的。简单平均把五分钟前的样本和现在的样本等权看待RTT从10毫秒跳到100毫秒时旧样本会把平均值拖住很久。EWMA让新样本以固定权重进入估计旧信息逐步衰减RTO才能跟上当前网络。生活里也一样判断回家的平均时长今天晚高峰的路况显然比上周的数据更有参考价值。2.3 重传歧义重传后的ACK不能直接用来测RTT这里有个教科书里讲得不太透的坑是“重传歧义”。发送方发出A超时后重传A这时候收到一个ACK号为Alen的ACK请问它是确认A还是A只看ACK号是分不出来的。如果直接把这段等待时间当作RTT样本会得出一个异常大的RTT。这个脏样本进入SRTT和RTTVAR之后RTO会被严重污染后面好几轮都会失真。所以经典实现的做法是一旦发生重传就暂时停止更新RTT估算同时把RTO翻倍进入指数退避直到收到确认“新数据”的ACK才恢复采样。这就是常说的重传歧义消除策略。指数退避还有一个额外的好处如果网络彻底断了发送方的重传间隔会变成1秒、2秒、4秒、8秒……而不是以固定频率轰炸已经感知不到的对端。这种“每轮重传间隔指数增长”的节奏也被坚持定时器和SYN重传沿用。2.4 时间戳摘除歧义以及RTO的上下限约束现代TCP其实已经几乎根治了重传歧义靠的是时间戳选项。发送方在每个包的时间戳字段里填入自己的时钟接收方回ACK时把它原样回显发送方就能认出这个ACK是对哪个包做出的响应哪怕是重传后的ACK也能配对。代价是TCP选项头多了12字节但对绝大多数网络路径是值得的。RTO还有一个容易被忽略的边界约束不能太小也不能太大。传统实现的下限是1秒上限60秒。下限防止RTT估算偶尔偏小时出现无谓重传上限防止连接彻底断开时高频发包轰炸网络。现代内核允许通过路由参数单独设置链路级rto_min在内网低抖动场景可以显式改小但不建议一上来就动它原因在第5章说明。3. 定时器家族重传定时器之外TCP还藏着三个闹钟3.1 重传定时器一个包发出后它就开始走重传定时器是整个超时重传体系的执行者。它的运行规则其实很简洁发送一个数据段时如果定时器还没启动就启动它收到确认新数据的ACK时如果还有未确认段就重新启动定时器如果没有未确认段就停止定时器。这个规则带来的微妙之处是窗口内同时有很多包在飞但TCP只为整个连接维护一个重传定时器只记录最早未确认段的超时时间。这个细节很重要。排障时看到“突然一个RTO定时器到期重传了某个很老的包”通常就是它成为最早未确认包并超时了。3.2 坚持定时器专治零窗口死锁接下来是最容易被忽略的坚持定时器。它的背景是“零窗口死锁”接收方窗口满了回一个ACK宣告win0发送方按要求停发。然后接收方腾出空间发出窗口更新包但这个更新包在网络上丢了。于是发送方还在等窗口变大接收方在等数据两边互瞪眼永远不会有人主动行动。为什么重传定时器救不了因为没有数据在传输重传定时器压根不会启动。所以TCP需要另一套机制发送方在窗口为0时启动坚持定时器定期发出一个1字节的窗口探测包要求对方重新回应当前窗口值。经典退避节奏是1.5秒起然后3秒、6秒、12秒上限60秒。设计成指数退避而不是固定频率是有道理的一对连接为了等窗口更新不该以固定节奏持续占网络资源同时也避免探测包和已丢失的窗口更新包反复碰撞。如果窗口迟迟不为正这套探测会一直跑下去直到连接被应用层关闭。3.3 保活定时器两小时才关心一次连接是否还活着保活定时器是用来检测“对端机器是否突然不在线”的。经典默认是连接空闲2小时之后才开始发保活探测每75秒发一次连续9次无响应才判定连接死亡。加起来大约2小时11分钟。很多初学者看到这个参数会很意外TCP居然默认不主动探活确实。原因在于保活探测本身会消耗资源如果每个空闲连接都频繁探测大规模服务器上会白白增加负担。所以TCP把“快速探测”的责任让给应用层私有的心跳机制通常以秒级间隔活跃TCP保活只是最后的兜底。这里有一条实战经验NAT、负载均衡等中间设备的空闲超时通常远小于2小时。如果你在维护长连接服务不能指望TCP保活帮你维持中间设备的映射一定要有应用层心跳否则连接会被中间设备静默剪断两端TCP却完全不知道。3.4 2MSL定时器主动关闭方必须付出的代价最后是2MSL定时器对应TIME_WAIT状态。主动关闭的一方发送最后一个ACK后必须等待2倍MSL才能彻底释放连接。经典MSL取2分钟所以TIME_WAIT通常持续4分钟部分操作系统实现会短一些但原理一致。为什么要等这么久两个原因。第一最后一个ACK可能丢失对端会重发FIN本端必须在TIME_WAIT内接收并重新回应ACK否则对端无法确认关闭完成。第二2MSL足够让旧连接的所有报文在网络中消失这样新连接复用同一地址端口时不会把旧连接残留的报文误接收为自身数据。TIME_WAIT和超时重传的关系在于如果系统因为TIME_WAIT表太多导致新连接建立失败很多人会想清掉TIME_WAIT。但先要想清楚牺牲的是谁的可靠性保护。服务端短连接压力大时正确方向是让客户端尽量复用连接而不是粗暴清理TIME_WAIT。4. 抓包实战如何区分正常重传、误判重传和伪重传4.1 抓包里的两种重传画像排障第一步永远是抓包。看Wireshark里的重传标识关键是区分两种完全不同类型的重传。超时重传的画像很清晰重传包的序列号和长度与某个原始包完全一致重传发生在RTO到期的时刻间隔大致按1秒、2秒、4秒这样的节奏放大。快速重传则是另一幅样子在收到第3个重复ACK之后立刻重传间隔通常只有一两个RTT而不是指数退避。举个例子。发送方发完一个Seq1000 Len1400的段随后发Seq2400 Len1400。如果中间这段丢了接收方会回ACK1000并不断重复ACK1000。当窗口内后续数据到达时Dup ACK里会带SACK标记。等第3个Dup ACK出现你就会看到Seq1000 Len1400再次出现这就是快速重传。只要把两类重传区分开排障方向就完全不一样快速重传主要找“丢包源”超时重传除了找丢包还要看RTO估算和ACK延迟策略的问题。4.2 伪重传陷阱TSO、乱序和延迟ACK抓包里最坑的是看到“伪重传”。Wireshark会把同一序列号再次出现的包标成TCP Retransmission但它未必真的发生了重传。常见的坑有三个。第一个是TSO和GRO大段数据交给网卡分段抓包工具看到的是网卡重新处理后的包序列号显示重复并不代表协议栈重传最好结合IP ID和时间戳判断。第二个是乱序产生的Dup ACK只有连收3个Dup ACK之后才算疑似丢包看到一两个重复ACK就断言丢包是新手常犯的错误。第三个是延迟ACK与RTO抖动叠加弱网场景下ACK本来就慢RTO又偏小于是出现“重传刚发出延迟的ACK就到了”这说明不是丢了包而是RTO设置和链路特征不匹配。我在一个内网环境里调远程同步工具时遇到过上传偶发卡顿抓包看到稳定的3个Dup ACK加快速重传但没有一个超时重传。一开始怀疑网卡多队列把同一条流的包打散到不同CPU调了RSS哈希还是复现然后把两台测试机直连现象消失。最后定位到某台接入交换机在突发流量时静默丢了少量包。整个过程里TCP参数一个都没动问题出在网络设备策略而不是协议栈。4.3 排查链路与自检清单把上面看到的碎片合起来我习惯按这样的顺序过一遍先分类型是超时重传还是快速重传有没有SACK块。再测RTT看重传前ACK的平均返回时间和方差。再做对照实验绕开中间设备直连或者关闭网卡TSO/GRO看重传是否消失。最后动参数确认链路抖动确实大再往RTO方向调。这个顺序能避免把时间浪费在错误的方向上。很多线上“TCP重传率高”的告警最后查下来根本不是协议栈问题而是网络设备丢包、多队列乱序或应用层发送模式不理想。5. 现代内核的补丁与新参数TLP、SACK/DSACK与调参姿势5.1 快速恢复改进从全窗口重传到按需重传传统Go-Back-N式重传效率很差窗口里丢一个段发送方要把丢失点之后的所有段重新发一遍。后来引入选择性确认SACK接收方把已收到的乱序范围通过SACK块告诉发送方发送方只需要重传真丢的那几段。对丢包率不高的互联网场景SACK能让有效吞吐提升非常明显。另外还有一个对“多段丢失”的改进。早期Reno遇到快速重传后进入快速恢复但如果窗口内丢了多个段后面还是会触发RTO。后续的改进版本利用部分确认一口气把已丢的段逐个重传完尽量不触发RTO。这个改进对真实网络非常重要尤其在丢包随机的无线链路上。5.2 TLP尾丢包探测在RTO之前先打草惊蛇现代Linux内核还加入了一个更精细的机制尾丢包探测。它专治“尾部丢包”。什么叫尾部丢包想象一个请求-响应模型服务器发完最后一个数据段就等ACK了。这个段如果丢了接收方因为没有后续数据不会产生Dup ACK发送方只能干等RTO到期。对搜索、RPC这种对延时期望极高的应用这往往意味着数倍于RTT的延迟。尾丢包探测的做法是在RTO快要到期之前主动发一个探测包——可以是新数据段也可以重发最后一个未确认段。只要这个探测包能到达接收方接收方就会回ACK发送方据此判断链路还活着如果ACK推进了说明只是丢包提前恢复如果探测包也丢了再触发后续RTO重传。实测下来开启了尾丢包探测的弱网场景尾延迟能显著降低。5.3 常用内核参数对照表与调优建议调参数前先看表别急着动手。参数默认值含义什么时候动tcp_retries13重传超过3次后TCP认为网络有严重问题会切换慢启动并尝试路由重估一般不动tcp_retries215重传超过15次后通知应用层连接已失效并最终关闭需要控制断连时间时调整但别指望秒级tcp_syn_retries6建立连接时SYN的重传次数总等待约2分钟应用层有更短超时时可调小tcp_keepalive_time7200保活探测启动前的空闲时间愿意用CPU换快速断连时可调短tcp_keepalive_intvl75保活探测间隔同上tcp_keepalive_probes9保活探测失败次数上限同上tcp_rto_min视内核版本而定单条连接的最小RTO可通过路由级rto_min设置低抖动局域网可调小高抖动无线链路建议调大这些参数的共同点是它们都是“最后防线”不是性能调优的常规手段。做应用层优化时更应该关注连接池、心跳、业务超时重试把TCP的重传留给真正需要兜底的场景。5.4 什么情况下真的需要改参数我见过不少团队把tcp_retries2从15调到3期望“断连更快”结果应用层反而出现一堆连接半开的状态。原因是TCP还没来得及把最后一次错误返回给应用应用已经自作主张做了重试最终加重了系统负载。真正值得改参数的就两种场景。第一种是内网或数据中心低抖动环境RTT稳定在亚毫秒级rto_min可以调小让重传响应更快。第二种是高波动无线网络此时宁可RTO大一点也不要频繁触发无谓重传。改之前先测一下RTT的方差再看看重传类型分布两个数据都齐了再动手。这套机制读下来最让我服气的不是某个公式而是TCP始终在“不精确的网络”里做“精确的推断”。它知道ACK不可靠、对端策略会变、网络会抖动所以宁可多用几次退避等待也不盲目重传宁可保守一点也不让网络出现重传风暴。排障时我习惯先看是哪种重传再测RTT均值和方差最后才考虑动参数。大多数时候问题不在参数而在一些别的因素——多队列乱序、限速丢弃、接收窗口太小——让TCP做了大量无用的“保险动作”。把这些机制理解透之后再看监控里的重传率你会开始区分“正常的控制面信号”和“真正的故障信号”。最后分享一个小技巧在Linux上手动调参前先用ss -ti看每一条连接当前的RTO、RTT和cwnd再配合netstat -s里的TCP重传统计基本能比纯抓包更快锁定问题方向。TCP的复杂不在于算法本身而在于它同时要和网络、对端协议栈、中间设备三方博弈理解了这一点很多“玄学”问题都会变得有迹可循。

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

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

免费获取报价 →
↑