TCP重传与超时机制:RTO、快速重传、SACK与拥塞控制实战

发布时间:2026/10/28 3:11:36
TCP重传与超时机制:RTO、快速重传、SACK与拥塞控制实战 TCP 重传与超时这套机制平时几乎没有人会主动想起它可一旦它开始频繁工作用户侧看到的就是卡顿、转圈、莫名其妙的延迟尖刺而服务端的 CPU、内存、带宽指标全都是一片祥和。搞网络排查这么多年我越发觉得这条链路是账面上最健康、体感上最糟糕的典型代表。这篇文章我想把 TCP 重传和超时机制从决策逻辑到内核参数、从抓包读法到线上踩坑完整地梳理一遍。它适合后端开发、SRE、做跨机房或移动端网络优化的同学参考也适合那些被P99 抖一下但监控全绿折磨过的朋友。我不打算复述教科书上的状态机而是按一个从业者在真实排障现场会走的路子来写先看清丢包之后内核做了什么决策再拆 RTO 的算法细节然后是快速重传与 SACK 家族接着讲重传和拥塞控制的耦合关系最后落到具体命令、参数和坑位上。1. 丢包之后到底发生了什么先把决策链路摆清楚1.1 一次典型的线上毛刺复盘先说个我印象很深的场景。某公司的交易类网关做跨机房调用两个机房之间的链路走的是运营商专线加一段公网隧道应用层超时设的 3 秒。现象是每隔几分钟出现一次 P99 从 80ms 跳到 1.2 秒的尖刺一天几十次每次持续时间很短监控上看带宽、QPS、错误率都正常唯独延迟曲线偶尔冒出一根针。这个现象折腾了差不多两周。真正定位靠的是一次高频抓包。抓下来的包里能看到非常规律的模式一串正常收发的报文之后突然有一段大约 200ms 的空白紧接着发送端把同一个序号的数据又发了一遍然后接收端回了一个覆盖范围更大的 ACK连接继续跑。这个 200ms 太可疑了它不是应用层超时不是 GC 停顿而是 Linux 默认的最小 RTO 值。换句话说这段时间里发送端根本没收到任何 ACK只能等到超时定时器烧完才决定重传。再往深里挖根因跟 TCP 本身关系不大中间某台设备在会话空闲到一个阈值后会做一次端口表刷新恰好命中了正在传输的连接导致连续几个包被丢弃。ACK 全丢发送端连一个重复 ACK 都收不到快速重传这条快车道根本没法启动只能退回超时重传这条慢路径。这也解释了为什么现象是偶发一次、持续很短——重传成功后连接就恢复了。这个案例说明一件事**看到重传先别急着调 TCP 参数先搞清楚丢的是数据包还是 ACK 包是一次性丢还是持续性丢。**丢数据包快速重传有机会接手丢 ACK 包发送端没有任何信息可用只能等超时。这两条路径的处理成本差了十几倍后面会详细说。1.2 两条腿走路超时重传与快速重传的分工TCP 的重传机制本质上是没有确认就重发但怎么判断没有确认内核用两套互补的信号来做决策我把它们对照着列一下方便建立直觉维度超时重传RTO快速重传Fast Retransmit触发信号重传定时器到期收到 3 个及以上重复 ACK判定依据时间绝对没有反馈序列号空洞有后续数据到达典型耗时200ms 起步指数退避一个 RTT 量级拥塞窗口处理收缩到 1 个 MSS最保守收缩到 ssthresh相对温和常见成因ACK 全丢、尾包丢失、链路中断单包丢失、乱序恢复代价高低这张表里最需要记住的是第四行。超时重传在拥塞控制语义里等同于网络出了大问题内核会做最悲观的假设把拥塞窗口直接压到 1 个 MSS慢启动阈值降到当前在途数据量的一半。而快速重传只是认为路上丢了一两个包窗口收缩得没那么狠。所以同样是丢一个包走快速重传可能只损失几十毫秒走超时重传可能损失好几百毫秒外加一段慢启动爬坡。1.3 为什么超时重传必须存在哪怕它很慢有人会问既然快速重传又快又温和为什么不全用快速重传因为快速重传的前提是后续的数据包能到达接收端让接收端有东西可以重复确认。如果发送端在途的数据本来就不多或者丢的是整条流最后一个包接收端拿不到任何后续报文就永远吐不出重复 ACK发送端只能干等着。这就是尾部丢失tail loss问题也是后来 TLP 机制要解决的核心场景。从这个角度看超时重传是整个可靠传输体系的兜底保险它不追求快只保证无论如何数据最终会到达。理解了这个定位就能理解为什么它的参数调优要保守——你调得越激进虚假重传带来的带宽浪费和拥塞窗口误判就越严重。2. RTO 到底是怎么算出来的别把它当成一个固定值2.1 SRTT 与 RTTVAR 的递推公式很多人对 RTO 的印象停留在200ms实际上 200ms 只是内核设定的下限。真实的 RTO 是动态计算出来的会随着链路 RTT 的均值和抖动情况变化。这套算法在 RFC 6298 里定义得很清楚核心是两个滑动统计量平滑往返时间 SRTT 和往返时间偏差 RTTVAR。公式不长但值得手推一遍。设一次新的 RTT 测量值为 R首次测量时SRTT RRTTVAR R / 2RTO SRTT 4 × RTTVAR后续测量时RTTVAR (1 - β) × RTTVAR β × |SRTT - R|其中 β 1/4然后更新SRTT (1 - α) × SRTT α × R其中 α 1/8最终RTO SRTT max(G, 4 × RTTVAR)这里 α 取 1/8 意味着新的 RTT 样本只占 12.5% 的权重历史值占 87.5%平滑力度很大而 β 取 1/4 意味着偏差的更新更灵敏因为抖动比均值变化得更快需要更快捕捉。G 是时钟粒度Linux 上一般取 1ms 量级。我拿一组具体数字算一下这样理解起来更直观。假设第一次测到 RTT 40msSRTT 40msRTTVAR 20msRTO 40 4 × 20 120ms但 Linux 的 TCP_RTO_MIN 是 200ms所以实际生效的 RTO 被抬到 200ms接着第二次测到 R 60msRTTVAR 0.75 × 20 0.25 × |40 - 60| 15 5 20msSRTT 0.875 × 40 0.125 × 60 35 7.5 42.5msRTO 42.5 4 × 20 122.5ms仍然被下限压到 200ms如果第三次突然测到一个抖动样本 R 200ms比如一次排队延迟RTTVAR 0.75 × 20 0.25 × |42.5 - 200| 15 39.4 54.4msSRTT 0.875 × 42.5 0.125 × 200 37.2 25 62.2msRTO 62.2 4 × 54.4 279.8ms看到了吧RTO 对抖动极其敏感。一个 200ms 的异常样本就把 RTO 从 200ms 拉到接近 280ms。这既是优点也是缺点优点是在真正不稳定的链路上 RTO 会自动放宽减少无效重传缺点是在平时很稳、偶尔抖一下的链路上一次抖动会把 RTO 抬高好一段时间而 SRTT 的恢复速度很慢因为 α 只有 1/8。实测中这种RTO 被抬高后迟迟降不回来的现象在跨运营商链路上非常常见。2.2 Karn 算法重传过的样本一律不算数上面这套公式有个致命漏洞它假设 RTT 测量值是可信的。可一旦发生重传问题就来了。发送端发出报文段 A超时后重发然后收到一个 ACK——这个 ACK 到底确认的是第一次发送还是第二次发送如果混淆了算出来的 RTT 会严重偏大进而把 RTO 越推越高形成恶性循环。Karn 算法的处理方式非常干脆**任何经历过重传的报文段它的 RTT 样本直接丢弃不参与 SRTT 和 RTTVAR 的更新。**这个决定在工程上完全正确避免了 RTT 污染但带来一个副作用如果一条连接持续在丢包重传那就长时间拿不到有效的 RTT 样本RTO 会一直停在旧值上失去自适应能力。为了解决这个问题后续引入了时间戳选项Timestamps。开启 tcp_timestamps 之后每个报文段都带一个发送时间戳重传时也会带新的时间戳接收端在 ACK 里原样回显。这样即使发生了重传发送端也能精确知道这个 ACK 对应的是哪一次发送RTT 测量重新变得可信Karn 算法的限制就被绕开了。这也是我在绝大多数生产环境里强烈建议保持 tcp_timestamps 开启的原因之一——它不只是防止序号回绕还直接参与 RTO 的准确性。2.3 指数退避与重试上限一条连接能等多久超时重传不是超时一次就重传一次然后失败而是一个指数退避的过程。第一次 RTO 触发后重传如果还是没确认定时器时间翻倍再翻倍如此循环。以 Linux 默认最小 RTO 200ms 为例退避序列大致是200ms → 400ms → 800ms → 1.6s → 3.2s → 6.4s → 12.8s → 25.6s → 51.2s → 102.4s → 120s封顶封顶值是 TCP_RTO_MAX通常 120 秒。注意退避是有上限的但重试次数是有终点的net.ipv4.tcp_retries2控制数据报文的重试上限默认 15。把所有退避时间加起来大约是 924 秒也就是 15 分钟出头。换句话说一条已经彻底断掉的 TCP 连接最长可能占用内核资源接近 15 分钟才被判定死亡。这个数字在很多场景下都显得太长了尤其是连接数很多的服务端。这里有个特别容易混淆的点tcp_retries2管的是已建立连接的数据重传tcp_syn_retries管的是建连阶段的 SYN 重试两个参数完全独立。我见过有同学排查连接建立超时跑去改 tcp_retries2改完发现毫无效果白白折腾半天。建连慢要去看 tcp_syn_retries默认 6 次退避下来大约 127 秒。3. 快速重传不等超时的那条快车道3.1 三个重复 ACK 背后的推理链快速重传的触发条件是被引用烂了的收到 3 个重复 ACK但很少有人讲清楚为什么是 3 而不是 1 或者 5。这个数字本质上是在误判率和响应速度之间做的折中。设想接收端收到了序号 1、3、4、5缺了 2。每收到一个后续报文它都会回一个 ACK 说我还在等 2。发送端看到第一个重复 ACK 时可能是乱序导致的——包 2 只是走得慢马上就到。看到第二个乱序的可能性降低但还没排除。看到第三个说明已经有三个不同的后续报文超车成功了包 2 大概率是真丢了。所以 dupthresh 3 是一个经验阈值容忍轻度乱序同时对真实丢包保持足够快的反应。问题在于现实网络的乱序程度差异极大。同一机房内部基本不乱序跨机房多路径可能轻微乱序而某些链路聚合或负载均衡场景下乱序能到十几个包。此时 dupthresh 3 就太激进了会导致大量虚假重传发送端以为丢了其实只是乱了重发一遍接收端收到重复数据再回 DSACK一来一回白白消耗带宽还误伤了拥塞窗口。这也是为什么现代内核在收到 SACK 信息后判定逻辑已经不再是简单数 3 个 ACK 了。3.2 SACK把丢了哪几段说清楚没有 SACK 的时候接收端只能说我在等 N发送端只能知道 N 之前都收到了、N 丢了对 N 之后的情况一无所知。如果一次丢了 5 个包发送端只能一个一个地重传每个都要等一个 RTT效率极低。SACKSelective Acknowledgment解决的就是这个问题。接收端在 TCP 选项里附带若干个我已经收到的区间发送端一看就明白哪些段到了、哪些段缺了、缺口有多大。有了 SACK一次 RTT 就能把多个空洞的重传全部安排出去在高丢包率链路上提升非常明显。Linux 的net.ipv4.tcp_sack默认开启我建议保持开启没有理由关掉。不过 SACK 有个需要注意的实现细节选项空间只有 40 字节最多塞 4 个 SACK 块取决于时间戳是否占用。如果一次丢包产生的空洞超过这个数量就只能分批通告恢复速度还是会打折。这也是为什么某些高丢包链路上SACK 的表现没有理论上那么好。3.3 DSACK 与虚假重传的自纠机制DSACK 是 SACK 的一个变种全称 Duplicate SACK用途很特别接收端用它告诉发送端你刚才发的这一段我早就收到了。这句话听着像是客套实际上是给发送端送了一条关键信息——你这次重传是多余的。发送端收到 DSACK 之后能做两件事。第一是纠正自己的乱序容忍度既然错了说明当前网络乱序比想象的严重后续应该更谨慎一些。第二是撤销之前的拥塞控制惩罚如果这次重传是虚假的那么当时把拥塞窗口压到一半就属于误伤应该把窗口恢复回去。Linux 里这套逻辑对应net.ipv4.tcp_dsack默认开和相关的撤销逻辑。我在实际排查中见过一个典型的虚假重传场景某公司的接入层做了链路聚合底层多条物理路径的延迟差有 5ms 左右导致部分报文乱序。TCP 收到 3 个重复 ACK 后判定丢包重传然后收到 DSACK。如果这条链路上乱序是常态连接的吞吐会长期被压在很低的水平上因为拥塞窗口反复被误伤。这种问题光看应用层指标根本看不出来必须靠抓包看 DSACK 出现的频率。3.4 TLP 与 RACK针对尾部丢失和乱序的两块补丁前面说的都是还有后续数据在飞的场景。如果发送端手里的数据全都发出去了就等最后一个 ACK而这个 ACK 迟迟不来尾包丢了快速重传根本无从触发只能等 200ms 以上的 RTO。这个浪费在现代短连接、小请求场景下占比很高。TLPTail Loss Probe就是来填这个空档的。它的思路是当发送端只有一个或两个未确认段时启动一个比 RTO 短的探测定时器PTO约等于 2 倍 SRTT有下限时间到了就主动发一个探测包——要么重传最后一个段要么发一个新段。这个探测包会让接收端吐出 ACK从而暴露真实的丢包情况把必须等 RTO变成等一个 PTO。Linux 通过net.ipv4.tcp_early_retrans控制值 3 表示 TLP 和 RACK 都启用这是目前的默认值。RACKRecent Acknowledgment换了另一条思路不看收到几个重复 ACK而是看时间。如果一个较早发出的报文段已经被 SACK 确认了而另一个更晚发出的段过了足够长的时间还没被确认那就判定后者丢了。这个判定天然对乱序更鲁棒因为它用时间差代替了数量差。RACK 还会动态估算乱序窗口 reo_wnd乱序严重时把判定阈值放宽避免误判。这两个机制叠在一起现代内核的实际行为已经和课本上的3 个重复 ACK 触发快速重传有了不小距离。排障时如果拿教科书模型去套真实抓包经常会对不上号原因就在这里。4. 重传一响拥塞窗口就得让路4.1 cwnd 与 ssthresh 的收缩差异重传和拥塞控制是绑在一起的这一点经常被忽略。很多人以为重传只是再发一遍实际上内核对重传的反应是整个连接降速。超时重传的处理是最狠的慢启动阈值 ssthresh 被设置为当前在途数据量flightsize的一半但不能低于 2 个 MSS拥塞窗口 cwnd 直接重置为 1 个 MSS进入慢启动。这意味着一次 RTO 之后连接从可能是几百个 MSS 的大窗口瞬间掉回一次只能发一个 MSS之后靠 ACK 逐个翻倍慢慢爬。在 RTT 100ms 的链路上从 1 个 MSS 爬到 100 个 MSS 大约需要 7 个 RTT也就是 700ms。这就是为什么一次超时重传的实际代价远不止 200ms。快速重传的处理温和得多ssthresh 同样是 flightsize 的一半但 cwnd 收缩到 ssthresh 而不是 1然后进入快速恢复阶段。窗口减半而不是归零恢复速度快了好几倍。我把两者的差异再列一下这个对比在解释为什么用户感知的卡顿时长是那个数时特别有用阶段超时重传后的状态快速重传后的状态cwnd重置为 1 MSS收缩到 ssthreshssthreshflightsize / 2最小 2 MSSflightsize / 2最小 2 MSS后续算法慢启动指数增长快速恢复拥塞避免线性增长恢复耗时RTT 100ms、原窗口 100 MSS约 7 个 RTT约 1 到 2 个 RTT4.2 为什么超时重传的惩罚必须这么重有人会觉得不公平也许只是丢了一个包凭什么把窗口打到 1这个设计背后有历史原因也有现实合理性。在早期的网络环境里超时几乎等同于链路严重拥塞。因为如果只是零星丢包后续报文总能到达并触发重复 ACK既然连重复 ACK 都没有说明要么整条路径堵死了要么链路上大量报文在排队后被丢弃。在这种判断下激进地降速是唯一安全的选择。TCP 的整个拥塞控制哲学就是宁可慢不可崩一次误判的代价是性能损失而一次激进的代价可能是整条链路雪崩。放到今天这个假设在很多场景下已经不成立了。无线网络、跨运营商链路、NAT 设备的会话表刷新都可能造成整段报文无声消失而后又迅速恢复。此时把窗口打到 1 显得过于严厉。这也是后来出现各种改进型拥塞控制算法比如基于带宽估计的 BBR的动机之一——它们不再把丢包当作拥塞的唯一信号。4.3 重传放大与上层超时的连锁反应这里有个特别值得说的雪崩模式。应用层设了 1 秒超时底层 TCP 因为一次 RTO 卡了 800ms应用层超时触发主动关闭连接然后立刻重试。重试建立新连接新连接没有历史 RTT 样本RTO 是初始值Linux 上是 1 秒RFC 建议值如果新连接上又丢了包等待时间更长。多个客户端同时这么做就形成了重试风暴。我见过更糟的情况应用层重试没有退避没有熔断一次后端抖动导致几万个客户端同时断开重连服务端的 SYN 队列被打满新建连接也开始超时然后客户端重试更频繁。这种正反馈一旦形成靠后端扩容是压不住的必须在上层加上退避和熔断。从 TCP 视角看重传是内核的自救行为但应用层的超时和重试是另一套独立的自救行为两者不协调就会互相踩踏。我的经验是应用层的超时时间应该显著大于预期的 TCP 恢复时间。如果链路 RTT 是 50ms预期最坏情况丢一次包走快速重传恢复在 200ms 以内那应用层超时设 500ms 到 1 秒比较合理如果设成 150ms等于把 TCP 的自愈能力废掉全靠应用层重试。5. 排查实录从计数器到抓包的一条完整链路5.1 先看内核计数器别急着抓包抓包成本很高尤其是高速链路包量大的时候 tcpdump 自己就会拖慢系统。我的习惯是先看计数器确认到底有没有重传、重传量大不大。用nstat看 TCP 统计最方便它读的是 /proc/net/netstat输出比 netstat -s 干净nstat -az | grep -iE retrans|timeout|sack|drop重点看几个指标。TcpRetransSegs是累计重传段数除以TcpOutSegs就是重传率超过 1% 就该关注了超过 5% 属于严重。TcpExtTCPTimeouts是超时重传次数如果这个数字在涨而TcpExtTCPLossRecovery涨得少说明大量丢包走的是慢路径问题可能出在 ACK 丢失或者尾包丢失上。TcpExtTCPSpuriousRTOs和TcpExtTCPSpuriousRtxHostQueues是虚假重传计数这两个值高说明 RTO 设置偏激进或者存在大量乱序。TcpExtTCPSackRecv和TcpExtTCPDSACKRecv能看出 SACK 是否在工作。如果想看单条连接的状态ss -ti是神器ss -tin state established ( dport :8080 )输出里会有 rtt、rttvar、cwnd、ssthresh、retrans 等字段还有retrans:0/5这种格式斜杠后面是这条连接的重传次数。我一般会反复执行几次看哪些连接的 retrans 计数在持续增长——持续增长的连接就是问题连接拿它的五元组去抓包目标非常明确。5.2 tcpdump 怎么读重传抓包的时候过滤条件要精细别一上来全量抓tcpdump -i eth0 -s 128 -nn host 10.0.0.1 and host 10.0.0.2 and tcp port 8080 -w /tmp/tcp.pcap只抓包头-s 128足够判断重传和序号能大幅降低开销。落地成 pcap 再用工具分析不要在线上直接-A打印。判断重传的方法同一个方向、同一个序列号出现了两次基本就是重传。但要区分两种情况——一种是重传同一个段seq 相同、长度相同对应真实丢包另一种是重传一个更长的段seq 相同但长度变大这叫重传合并说明内核把多个待确认的小段合并成一个大段发出去效率反而更高不是问题。更实用的方法是看时间间隔。如果两次相同 seq 之间的间隔接近 RTO 值200ms、400ms、800ms 这些特征值那是超时重传如果间隔只有半个到一个 RTT而且中间夹着几个重复 ACK那是快速重传。靠间隔反推触发机制比死记字段名有效得多。还有一个信号值得盯接收端回的 ACK 里如果带着 SACK 选项说明存在空洞如果后续的 ACK 序列号一直是同一个值不前进说明接收端窗口已经被塞满正在等缺失的段。5.3 常用可调参数与取舍参数能调但我不建议一上来就调。先把根因搞清楚绝大多数重传问题不是参数问题而是链路质量、中间设备、或者应用层设计的问题。真要调下面这几个是实际会用到的参数默认值作用调整建议net.ipv4.tcp_retries215数据重传总次数上限内网可降到 8 到 10加快死连接回收net.ipv4.tcp_syn_retries6SYN 重试次数建连慢时关注内网可降到 3net.ipv4.tcp_sack1选择性确认保持开启net.ipv4.tcp_dsack1重复 SACK保持开启net.ipv4.tcp_timestamps1时间戳选项保持开启关掉会破坏 RTT 测量和防回绕net.ipv4.tcp_early_retrans3TLP 与 RACK保持默认 3net.ipv4.tcp_thin_linear_timeouts0空闲流的超时退避改为线性长连接多、突发流量场景可考虑开启关于tcp_thin_linear_timeouts这个参数值得展开说一下。它的作用对象是流量稀薄的连接——就是那种隔很久才发一个包的连接。这类连接如果偶发丢包按指数退避的话很容易一路退到几秒甚至十几秒对交互式应用来说体验极差。开启之后前几次超时改为线性递增而不是指数翻倍能显著降低这类长连接的偶发延迟尖刺。我见过一个长轮询的场景开启这个参数后 P99 从 3 秒多降到 400ms 以内。关于 RTO 的下限Linux 硬编码为 200ms部分内核版本暴露了net.ipv4.tcp_rto_min_us可以调。理论上把它调到 50ms 能让内网重传更快但要非常小心下限调得太低在 RTT 本来就接近这个值的链路上会引发海量虚假重传。如果链路 RTT 是 30msRTO 下限设 50ms稍微抖一下就会误判丢包得不偿失。我的建议是除非你对自己的链路质量有绝对把握否则别动这个值。5.4 应用层该做的兜底设计参数之外应用层的设计对重传的实际影响更大。几个我在项目里验证过有效的做法第一是避免小包低效传输。Nagle 算法和延迟 ACK 的经典组合会导致40ms 魔咒发送端等 ACK 攒够数据接收端等数据攒够再回 ACK两边互相等实际延迟凭空多出几十毫秒。对延迟敏感的请求-响应型协议设置 TCP_NODELAY 关掉 Nagle 是常规操作代价是可能多发几个小包。第二是连接复用要谨慎。长连接能省掉三次握手但复用的连接如果中间链路发生变化比如负载均衡重新分配路径质量问题会被所有复用它的请求共同承担。我倾向的做法是给长连接设一个最大存活时间比如几分钟定期重建避免一条病连接长期拖累一批请求。第三是超时分层设计。连接超时、读超时、总超时分别设值且必须满足连接超时 读超时 总超时总超时要大于 TCP 层最坏恢复时间。这个顺序看似显然实际项目中搞反的情况非常多尤其是微服务框架里各层默认值叠加之后。第四是重试必须带退避和抖动。固定间隔重试是最糟糕的选择所有客户端会对齐到同一时刻发起重试形成脉冲式冲击。加随机抖动指数退避再配一个熔断器能把重试的放大效应压到可控范围。6. 常见问题速查与踩坑记录6.1 问题速查表这里把我这些年遇到的典型现象和对应的排查方向整理成表出问题时可以按图索骥现象可能的 TCP 层原因排查手段延迟偶发尖刺幅度约等于 RTO 特征值超时重传多半是 ACK 丢失或尾包丢失nstat 看 TcpExtTCPTimeouts 增速吞吐上不去重传率 3% 到 10%链路真实丢包或大量乱序触发虚假重传tcpdump 看 DSACK 频率单条连接特别慢其他正常该连接遭遇过多次 RTO窗口长期偏小ss -ti 看该连接 cwnd 和 ssthresh重传包在同一 RTT 内连续出现多个SACK 生效多空洞恢复抓包看 SACK 块数量服务端连接数缓慢堆积不释放tcp_retries2 过大死连接回收慢检查 tcp_retries2看 ESTABLISHED 老化长轮询偶发几十秒延迟空闲连接超时退避过快考虑 tcp_thin_linear_timeouts新建连接慢但已有连接正常SYN 重试与数据重传无关看 tcp_syn_retries 和 SYN 队列6.2 几个反直觉的坑**第一个坑重传率高不一定是链路差。**有一次排查一个重传率 8% 的集群怀疑是交换机问题换设备、换链路、换时段全都没用。最后发现是应用侧做了大量短小请求每个请求几十字节叠加延迟 ACK导致接收端迟迟不回 ACK发送端等不及就超时重传。关掉 Nagle 之后重传率直接掉到 0.3%。这个案例说明重传的成因有一大半在应用层的行为模式上不在网络上。**第二个坑抓包看到重传不代表真的丢包。**如果抓包点选在发送端网卡出方向你看到的是内核决定重传至于包有没有真的丢在路上这张网卡不知道。想确认真丢得在接收端同时抓两边比对序号。单点抓包得出的丢包结论有一半是假的。我吃过这个亏信心满满地拿着抓包去找网络团队对方在接收侧一抓包全到了只是到得晚脸丢得挺彻底。**第三个坑RTO 被抬高后很难降回来。**SRTT 的平滑系数是 1/8意味着一次大抖动需要十几个正常样本才能拉回来。在那些几分钟抖一次的链路上RTO 会长期维持在高位每次丢包的重传等待时间都是被放大的。这种链路的表现是重传次数不多但每次重传都特别慢跟重传次数多是两种完全不同的病处理方式也不同。**第四个坑把 tcp_retries2 调小来加快失败。**这个思路本身没错但它只影响数据重传不影响应用层的感知。连接被内核判定死亡后应用层要等到下一次读写才会发现中间可能还卡在某个阻塞调用里。真正想快速失败需要应用层配合设置读超时或者用 TCP_USER_TIMEOUT 这个 socket 选项它能让内核在指定时间内没进展就主动放弃并报错比 tcp_retries2 精确得多。6.3 收尾的一点个人体会TCP 重传这套机制最反直觉的地方在于它的设计目标是在极端情况下依然保证数据到达而不是在正常情况下跑得最快。所以它的每一个保守选择——RTO 下限 200ms、超时后窗口归 1、重试 15 次——都是在为最坏情况留余量。我们在做性能优化时很多时候是在和这些保守假设搏斗比如用 BBR 绕开丢包即降速的逻辑用 TLP 绕过尾包必须等 RTO 的限制。但搏斗之前先得确认敌人是谁。我自己的习惯是遇到延迟问题时先跑一遍ss -ti和nstat五分钟内就能判断出是真的有重传还是根本没有重传、问题在别处。这一步能省掉后面几天的瞎猜。真正需要抓包的时候也别全量抓先按五元组精确过滤落地成文件慢慢看线上直接打印报文这种事一次就够记一辈子了。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询