资讯动态

TCP重传机制全解:超时重传、快速重传与SACK实战

发布时间:2026/10/5 2:56:56 来源:尧图企业网站定制
TCP重传机制我敢说大部分人对它的理解停留在“丢了就重发”的层面。真正处理过线上问题的工程师都清楚重传背后是一整套精密的算法和计时器在运转哪个参数不合理网络一抖动就是一场事故。这篇文章我想从底层原理讲到抓包验证再结合我这些年排过的问题把超时重传、快速重传、SACK这些机制掰开揉碎讲清楚。如果你正被TCP性能问题困扰或者面试时被问得说不出话这文应该能帮上忙。1. 重传机制的底层逻辑为什么“丢了就重发”是伪命题1.1 没有重传机制互联网就转不起来TCP的设计目标很简单在两个进程之间提供可靠的字节流传输。什么叫可靠就是发送方发出去的数据接收方必须且只能收到一份而且顺序不能乱。但底下那层网络IP是“尽力而为”的路由器拥塞会丢包、网卡队列溢出会丢包、信号干扰也会丢包。既然物理链路不靠谱TCP就必须在自己的层把窟窿补上。补窟窿的手段就两个确认ACK和重传。发送方发出数据段后启动一个计时器如果在一定时间内没有收到对应的ACK就判定丢失然后把数据重新发送一次。这个逻辑听着简单但真正的复杂度全藏在“一定时间”这四个字里——设短了网络稍微慢一点就疯狂重发把带宽白白浪费掉设长了一个包丢了要等半天才能恢复业务早就超时失败了。所以重传机制的本质是一个在“及时恢复”和“避免无用重传”之间反复权衡的动态决策系统。一开始接触TCP的人容易把重传想成一锤子买卖发一次没收到ACK再发一次。实际内核里的重传远比这个复杂它至少分成两条路线一条是超时重传靠着定时器到期硬等另一条是快速重传靠着连续重复的ACK提前感知丢包不等定时器到期就立刻补发。这两条路线我在后面几节分别展开它们的触发条件和代价都不一样。1.2 数据从应用到内核重传发生在哪个环节很多刚入行的同学会在应用层自己做“重传”比如发个Socket没收到回应就再发一次然后抱怨网络有问题。其实TCP的重传是内核协议栈自动完成的应用层的send()只是把数据交给内核缓冲区后续ACK有没有回来、要不要重发全部由内核处理。这引出一个很重要的实践认知如果你在应用层发现TCP重传频繁你先要搞清楚是哪个层面引起的别急着改业务代码。数据从应用进程进入内核后会挂到发送队列sndbuf里。内核把数据切成MSS大小的段依次发出。已发送但未确认的段都放在发送窗口范围内同时每个段会登记一个重传计时器。收到ACK后对应的数据段就可以从发送队列里清掉了计时器同时也取消。如果在计时器超时前没收到确认内核就把这块数据重新推入发送路径。整个过程中应用进程是无感知的除非你用getsockopt去查TCP_INFO这类内核统计信息。值得留意的是TCP的可靠性保证的是“字节流最终完整有序”而不是“每一段只发一次”。一个报文段如果收到的ACK是累积的那前面好几段可能一起被确认了。所以写代码或者看抓包时别见到一个ACK就以为它只确认了最近一段累积确认cumulative ACK才是TCP的常态。2. 超时重传RTO这个值是怎么算出来的2.1 定时器不是定死的RTO的数学公式前面说了超时重传的难点在于确定“等多久算太久”这个等待时间在TCP协议里叫RTORetransmission Timeout重传超时时间。RTO不是拍脑袋定的它是基于历史RTTRound-Trip Time统计出来的动态值。内核会持续测量每个数据段从发出到收到ACK的往返时间用这些样本不断修正RTO。经典的RTO计算来自RFC 6298核心是两个变量SRTT平滑往返时间和RTTVAR往返时间偏差。每次测到新的RTT样本R就按下面公式更新SRTT (1 - α) × SRTT α × R RTTVAR (1 - β) × RTTVAR β × |SRTT - R| RTO SRTT max(G, 4 × RTTVAR)其中α通常取1/8β取1/4G是系统时钟粒度Linux上一般是1ms或10ms。为什么要用4倍的偏差因为网络抖动是常态如果RTO只等于平均值那稍有波动就会触发大量误判重传取4倍标准差的概念相当于给RTO留了足够宽的容忍区间只有RTT异常拔高时才重传。拿个实际数值演算一下假设第一次RTT样本是100msSRTT初始化为100msRTTVAR初始化为50msRFC规定第一次采样时RTTVAR取RTT的一半代入公式RTO 100 max(1, 4 × 50) 300ms如果下一次R变成了200msSRTT 0.875 × 100 0.125 × 200 112.5msRTTVAR 0.75 × 50 0.25 × |112.5 - 200| 37.5 21.875 59.375ms新RTO 112.5 4 × 59.375 350ms。可以看到RTO跟随样本平滑调整不会因为一次突发抖动就剧烈震荡这正是平滑算法的价值。2.2 指数退避为什么连续超时后重传间隔越来越大如果RTO到了发出去的包仍然没得到ACK那TCP不会按原始RTO傻等下一轮而是执行指数退避RTO翻倍。第一轮超时等300ms第二轮等600ms第三轮等1.2秒最多翻到60秒封顶。这个策略的背后逻辑是——如果网络已经严重拥塞你再使劲发只会雪上加霜所以退避是在自适应地给网络“降压”。真实网络里一个包连续重传两三次的场景并不少见尤其是跨运营商链路或者无线网络环境下指数退避避免了重传风暴把链路彻底打死的可能。但是退避也有代价RTO翻倍意味着恢复变慢。如果应用层同步等待响应那用户侧表现就是卡顿、超时。所以生产环境里经常看到的问题就是中间链路丢包率不高但偶尔抖一下RTO从300ms翻倍到600ms再翻到1.2s应用层的read超时已经先到了。这时候与其调TCP参数不如先解决丢包的根源或者让应用加一点合理的retry逻辑。2.3 Linux初始RTO为什么是1秒Linux内核里有个重要的默认值初始RTO是1秒。因为第一个数据段发出去时没有任何RTT样本内核不知道网络往返到底多快只能用一个保守的初始值。1秒这个数字来自RFC 6298的建议——初始RTO应不少于1秒。所以你会看到新连接的首次数据交互如果恰好丢包恢复至少要等1秒即使实际网络延迟只有10ms。这个“首包效应”在高频短连接场景里非常吃亏比如大量健康检查请求、短事务API调用一旦首次握手后第一个数据包被丢弃整个请求就要白白等上1秒。针对这个场景实际项目里我会看两种情况。如果短连接占比很高可以考虑用长连接池把跨过初始阶段的连接复用起来如果必须短连接而且网络环境可靠可以尝试调低初始RTO但这不是推荐做法因为内核的参数是全局的调低了会影响所有新连接在长距离链路上的重传容忍度。更稳妥的方案是业务层做快速失败和重试把等待控制在用户可接受范围。3. 快速重传与重复ACK不用等定时器的抢救通道3.1 三个重复ACK触发快速重传超时重传最大的问题就是慢尤其当RTO被退避到很大时一个包丢了可能一两秒都没人管。TCP发明快速重传的动机很简单不用等定时器用接收方的反馈提前感知丢包。怎么反馈接收方收到乱序包时比如期望序列号1000结果来了个1200它会立刻回一个ACK确认“我还在等1000”。之后每收到一个乱序包它都会重复这个ACK。发送方连续收到3个相同的重复ACK就断定1000这个段丢了不等定时器到期直接重传1000。这里有个细节很多资料没说透——为什么是3个而不是1个因为包在网络里可能只是被短暂延迟但没丢接收方收不到期望的数据就会回重复ACK发送方如果收到一个重复ACK就盲目重传那碰上乱序或轻微延迟就会白白重发造成不必要的带宽消耗。经过实践检验3个重复ACK能有效区分轻度的乱序和真丢包。RFC 5681也明确写的是“3 dup ACKs”Linux内核遵守这个约定。3.2 SACK和D-SACK精确告诉发送方丢了什么经典TCP只靠累积ACK发送方只能知道“1000丢了”不清楚1200、1400后面的段是否都收到了。如果窗口里有多个段丢失发送方只能一个个重传效率很低。**SACKSelective Acknowledgment选择性确认**解决的就是这个问题。它在TCP选项里携带一块“接收缓冲区的位图”明确告诉发送方哪些序列号区间收到了哪些没收到。有了SACK发送方只补丢的那几个洞不用重复发送已成功到达的数据。SACK之上还有个D-SACKDuplicate SACK它用来告诉发送方“你重传的这段我其实已经收到过了”。D-SACK能帮发送方检测到重传过头或者路径上发生了重复传输的情况避免反复重传同一个已经被确认的段。Linux默认开启了SACK可以通过socket选项TCP_NODELAY旁边的TCP_CORK、SO_REUSEADDR一起配合调优但SACK本身高端接口一般不需要改。拿真实抓包来看SACK开启后的ACK包会带类似这样的选项TCP Option - SACK permitted TCP Option - SACK: 1200-1400 1600-1800意思就是“我这边的缓存里1200到1400这段我有了1600到1800也有了中间1400到1600还没到赶紧发”。这样发送方一发一个准网损恢复效率比纯靠累积ACK高非常多。3.3 快速重传实战一条命令确认重复ACK排查TCP重传我最常用的就是tcpdump抓包。抓个几十秒过滤出带有TCP重传标志的包基本就能判断问题方向。举例tcpdump -i eth0 tcp[tcpflags] (tcp-syn|tcp-fin|tcp-rst) 0配合显示序列号能看到重复ACK的密集程度。如果发现同一个ack number在短时间内出现3次以上且后续跟着相同seq的重传包基本可以断定是快速重传在起作用。如果你看到的是长时间没动静后才出现的重传那就是超时重传这两种的链路表现完全不同定位方向也不一样。我在一次项目中遇到过一台服务器往另一台上传大文件业务反馈速度特别慢。抓包后发现重复ACK频繁出现但网络延迟很低。排查到最后问题出在中间防火墙的“TCP序列号随机化”功能上——它改写了SYN中的序列号导致对端校验失败数据不断被丢弃触发快速重传。这种问题不看抓包根本想不到。4. 重传导致的性能问题与排查实操4.1 先用Linux自带指标诊断重传不用一上来就抓包先用系统自带统计工具看整体情况。ss -s能告诉你当前TCP连接的发送队列、重传队列占用$ ss -s Total: 1234 (kernel 2345) TCP: 1023 (estab 880, closed 100, orphaned 20, synrecv 3, timewait 200) Transport Total IP IPv6 TCP 1023 900 123再看单个socket的详细重传计数可以用ss -ti输出里有个叫retrans的字段代表该连接累计重传了多少次。如果retrans数值持续增长那说明这个连接存在丢包或延迟波动。这个命令还可以看到RTO、RTT、cwnd和ssthresh对判断拥塞控制是否正常很有帮助$ ss -ti state established ( dport :443 ) cwnd:10 bytes_acked:12345 retrans:3 rtt:25/5 rto:300 mss:1460rtt字段的格式是25/5前面是平均RTT后面是偏差。rto 300ms在当前平均25ms的RTT下听起来偏大但初连接阶段这是正常的后续会慢慢收敛。如果发现单连接重传率高再配合netstat -s看全局统计$ netstat -s | egrep -i retrans|dup ack|sack 1234 segments retransmited 567 fast retransmits 89 retransmits in slow startfast retransmits和segments retransmited的比例能反映丢包是不是集中在快速重传阶段。4.2 一个真实的慢接口排查案例之前有个系统某个查询接口的P99延迟突然从50ms飙到800ms业务方怀疑数据库慢查询。我接手后先在接入层抓包发现客户端发完请求后服务端很快回了一个ACK但业务响应包隔了400ms才出来而且这段等待期间没有重复ACK。用tcpdump对比两个downstream服务发现客户端同时向两个服务建立的TCP连接中只有目标服务连接的RTT值从之前的20ms翻到了400ms同时伴随TCP重传。最终定位是云平台某个物理机上出现了CPU steal网络软中断得不到及时处理导致网卡队列积压数据包延迟大增。这台机器上的数据库本身没慢但TCP协议栈排队了表现在外就是接口慢。这个案例给了一个教训TCP层的时间消耗不等于应用层处理时间但TCP重传指标异常的根因往往在上游或基础设施层而不是服务代码。排查顺序应该是先看重传计数和RTT变化再定位是哪个链路跳变最后落到具体的硬件、云主机或防火墙配置。4.3 快速定位问题链路的小工具组合排查重传问题我的工具组合一般是这三件套ping -f测中间路径是否有丢包和碎片问题traceroute逐跳看延迟确认瓶颈在哪一段路由tcpdump Wireshark盯TCP层的具体重传行为另外提醒一个Linux细节系统开启了TIMEWAIT重用和TIMEWAIT快速回收在高并发短连接下可能出现端口假死客户端发起连接时报“Address already in use”。这本质上是连接生命周期管理和重传无关但一旦客户端在旧连接TIME_WAIT还没结束时重连内核会复用端口而旧连接的重传数据可能还滞留在网络上导致新连接被干扰。遇到这种问题先检查net.ipv4.tcp_tw_reuse、net.ipv4.tcp_timestamps这些内核参数再考虑调整应用层的连接管理策略。5. 重传机制的调优方向与常见误操作5.1 内核参数调整的边界在哪里很多运维一看到重传就调大tcp_retries1和tcp_retries2干了几年我发现无脑调参是收益最低的玩法。tcp_retries1控制“至少尝试几次重传后才通知应用层网络不可达”tcp_retries2控制“超过多少次重传就放弃连接”。默认值分别是3和15对大多数场景已经足够。如果你服务的网络环境实在差比如移动网络、卫星链路把tcp_retries2调大到20~30可能比默认值更稳但同时意味着一个死连接要占用资源更久。反过来对于内部IDC这种低延迟高可靠环境tcp_retries2可以适度调小让连接快速失败把资源释放出来供新连接使用。更值得调的是tcp_keepalive_time、tcp_keepalive_intvl这些和长连接健康检查相关的参数它们决定了一个半开连接多久能被探测出来。我见过线上服务因为没开keepalive导致大量TCP连接处于“假活”状态客户端以为连着服务端早把socket回收了一有数据就超时重传最后引发雪崩。重视连接健康检查比纠结RTO参数有用得多。5.2 缓冲区和窗口大小对重传的间接影响TCP重传率还和sndbuf/rcvbuf、窗口缩放因子直接相关。发送缓冲区设置太小会在高带宽低延迟链路下限制吞吐数据一直塞不进内核应用层被迫等待可能造成应用层超时误判接收窗口设置太大在丢包环境下会导致大量重传因为接收方缓存够发送方一股脑发出去一旦某个段丢了后续所有段都要缓存等待重传。经典的“BDP带宽延迟积”概念在这里非常实用BDP 带宽(bps) × RTT(秒) / 8例如带宽1GbpsRTT 20msBDP约为2.5MB。也就是说TCP窗口至少要2.5MB才能充分利用带宽否则吞吐会被限制在窗口大小除以RTT的“小水管”里。很多业务说“带宽明明够为什么传不快”查下来往往是socket缓冲区没调窗口永远封顶在64KB吞吐上不去。Linux下调整收发缓冲区sysctl net.ipv4.tcp_rmem4096 87380 6291456 sysctl net.ipv4.tcp_wmem4096 65536 6291456第一列是最小值第二列是默认值第三列是最大值。注意缓冲区是按需增长不是一开始就分配最大值的所以把最大值调大不影响多数连接的内存占用这条可以放心调整。5.3 应用层配合TCP重传的正确姿势最后想说一点TCP重传再高效也是被动补救应用层主动配合才能让整个系统更稳。一是消息幂等重试。如果业务确实需要在应用层做重发比如发一条指令后一段时间没收到业务响应一定要保证业务逻辑的幂等性——服务端重复收到同一条指令不能产生副作用。TCP只能保证字节可靠不能保证业务恰好执行一次数据库的唯一索引、请求ID去重都是常用方案。二是心跳与故障切换。对长连接应用心跳超时检测要设置得比重传容忍度更短否则连接已经死了数据还在等重传恢复整个系统会越拖越卡。这个取舍需要根据业务容忍度设计没有标准值。三是避免在临界区做大量小包发送。小包多意味着ACK频繁网络一抖重传的概率也会上升。能用批量读写就聚合成大包IO效率提升重传带来的相对开销也变小。我在实际调优中体会最深的一点是TCP的每个重传参数背后都是权衡没有任何一个参数放之四海皆准。你得先摸清自己的网络环境——是IDC机房里的稳定以太网还是公网上的跨国链路——再对症下药。如果连丢包发生在哪一跳都搞不清楚调参数只是碰运气。这套重传机制的细节非常多但核心的脉络抓透了再遇到网络类疑难杂症起码知道从哪儿下手。

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

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

免费获取报价 →
↑