☰
TCP重传与超时机制:RTO计算、快速重传及线上长尾排查实践
2026/10/28 20:04:05 网站建设 项目流程

1. 从一次“莫名卡顿三秒”说起:重传到底在管什么

先说个我印象很深的排查经历。某内部服务做数据同步,客户端跑在一台跨机房的长链路上,业务方反馈的现象是:绝大多数请求都在 20ms 内返回,但每隔几分钟就会冒出一次 1 秒到 3 秒的“长尾”,日志里没有任何异常,应用层也没报错,纯粹就是慢。第一反应是查应用、查数据库、查 GC,全都正常。后来把ss -ti挂上去看,问题一目了然:那条连接上retrans计数器一直在涨,rto字段跑到了 1200ms 以上,再配合抓包,发现是对端发来的某个数据段在中途丢了,发送端硬生生等完了整个超时时间才重发。

这就是 TCP 重传与超时机制要解决的核心问题。TCP 把自己包装成一条“可靠有序的字节流”,但底下跑的 IP 网络只承诺“尽力而为”——包可能丢、可能重复、可能乱序、可能比预期晚很久才到。可靠性的兜底,全靠发送端自己盯着:发出去的每一段数据,在没收到确认(ACK)之前都得留着,一旦判断“对方大概没收到”,就重新发一遍。判断依据有两条路,一条是靠定时器(超时重传,RTO),一条是靠后续数据触发的重复确认(快速重传)。整个机制的难点不在于“重发”这个动作,而在于何时重发:判早了就是无谓重传,白白浪费带宽还可能把拥塞搞得更糟;判晚了就是长尾延迟,业务侧看到的就是那几秒卡顿。

这篇内容适合三类人看。一类是日常要和线上性能问题打交道的后端或运维同学,遇到“偶发慢”“长尾抖动”时知道该往哪个方向看;一类是做网络库、RPC 框架、代理中间件的开发同学,需要理解内核的重传行为才好设计应用层超时;还有一类是准备面试或者补基础的同学,RTO 的计算公式、Karn 算法、SACK、快速重传这些概念,光背结论很容易忘,得知道它们各自在解决什么麻烦才记得住。我会从公式推导讲到内核参数,从抓包实证讲到踩坑经验,尽量让每个结论都有个“为什么”。

2. 整体设计思路:两条重传路径与三个计时器

2.1 为什么不能“发出去就不管”:重传队列的存在意义

理解重传的第一步,是搞清楚数据发出去之后并没有立刻被丢弃。内核在发送端维护一个重传队列(在 Linux 里体现为sk_write_queue,配合snd_una到snd_nxt之间的未确认区间),每一段已发送但未确认的数据都挂在这上面。只有当收到 ACK、确认号越过某段的末尾序列号时,这段数据才会被真正释放。这个设计带来一个直接后果:发送缓冲区并不只是“应用写进来的待发数据”,它还兼着“已发出但对方可能没收到”的备份职责。所以当你在应用层看到send()一直返回成功、但连接其实已经在丢包重传时,缓冲区可能正在被这些“悬而未决”的数据占着,写阻塞会悄悄出现。

这里有个很多人忽略的细节:重传队列的头部决定了 RTO 定时器该怎么设。TCP 不会为每个数据段单独开一个定时器(那是早期实现的做法,开销太大),而是只对“最早那个未被确认的段”设一个定时器。收到新 ACK 时,如果确认号前进了,就把定时器按新的头部段重新计算;如果定时器真的到期了,就重传头部段,然后重新计时。这个“单一定时器”的设计是理解后面所有超时行为的钥匙——它意味着 RTO 的精度天然是粗粒度的,也意味着重传顺序总是从最老的未确认数据开始。

2.2 RTO 与快速重传:一条慢路和一条快路

两条重传路径的分工很清晰。RTO 超时重传是兜底路径,它不依赖任何后续数据,只要定时器到期就触发,代价是等待时间长(最小 200ms,长的能到秒级甚至分钟级)。快速重传是快路径,它依赖“后续数据已经到达但对端没收到前面那段”这个信号——接收端每收到一个乱序段,就会重复发送同一个 ACK(确认号还是那个缺口),发送端连收三个重复 ACK 就认定缺口处丢了,立刻重传,不用等定时器。

用个生活化的类比:你给同事发了一箱编号 1 到 10 的快递,对方每收到一件就回一条“我收到第几件了”。如果 3 号在路上丢了,对方收到 4、5、6 时会一直回“我还差 3 号”。你连着收到三条“还差 3 号”,立刻就知道 3 号丢了,马上补发——这就是快速重传。但如果对方只收到 1、2 号就再也没动静(后面的全丢了,或者压根没发那么多),你手上没有任何“还差 3 号”的反馈,只能干等,等自己设的“最晚多久没回音就重发”的闹钟响——这就是 RTO。

2.3 三个计时器之外,还有几个容易混淆的超时

新手常把 TCP 的超时都叫成“重传超时”,实际上内核里有好几套独立计时,触发条件和后果完全不同,混着看容易误判。我整理成一张表,排查时对着看会快很多:

计时器触发条件后果常见现象
重传定时器(RTO)最早未确认段超时重传该段,RTO 指数退避retrans增长,长尾延迟秒级
持续计时器(persist)对端通告零窗口定期发窗口探测,防止死锁连接“活着但不动”,应用写阻塞
保活定时器(keepalive)连接空闲超过设定时长发探测包确认对端还在空闲连接被清理或断链
TIME_WAIT 计时主动关闭方进入 TIME_WAIT等待 2MSL 后释放端口占用、无法立即复用
SYN 重传握手阶段 SYN 未获响应指数退避重发 SYN连接建立慢、握手超时

注意:排查“连接卡住”时先分清是哪一类。如果对端窗口是 0,那根本不叫丢包,重传计数器不会涨,抓包看到的是周期性的窗口探测,别往丢包方向查。

3. 超时重传的核心:RTO 到底是怎么算出来的

3.1 从一次 RTT 采样到平滑公式

RTO 不能拍脑袋定个固定值。定 1 秒,那跨洋链路(RTT 本身 200ms 以上)会频繁误判重传;定 10ms,那稍微抖一下就是一堆无谓重传。所以 TCP 的做法是动态测量:每次收到 ACK,就用“发出时刻到收到确认的时刻”算一个 RTT 样本 R,然后用这个样本去更新两个平滑量——平滑往返时间 SRTT 和 RTT 偏差 RTTVAR。

公式来自 RFC 6298,我用工程视角解释一下它为什么长这样:

首次采样: SRTT = R RTTVAR = R / 2 RTO = SRTT + max(G, 4 * RTTVAR) // G 是时钟粒度 后续采样(alpha = 1/8,beta = 1/4): RTTVAR = (1 - beta) * RTTVAR + beta * |SRTT - R| SRTT = (1 - alpha) * SRTT + alpha * R RTO = SRTT + max(G, 4 * RTTVAR)

关键在最后那个4 * RTTVAR。SRTT 是“平均要多久”,RTTVAR 是“抖动有多大”,RTO 等于平均值加上四倍抖动,本质是给一个宽容区间:抖动越大,我等你越久,避免把正常的网络抖动误判成丢包。这个 4 倍系数是经验值,能在误判率和响应速度之间取得一个还算不错的平衡。

另外注意max(G, ...)这一项。时钟粒度 G 在早期实现里是个实打实的限制(比如 500ms 一跳的定时器),现代内核用微秒精度,G 基本可以忽略,但公式保留着是为了在粗粒度环境下不至于算出个荒谬的小值。

3.2 手算一遍:RTO 是怎么被抖动“撑大”的

光看公式没感觉,我拿三个样本手算一遍,你会立刻明白为什么抖动大的链路 RTO 会飙得很快。

假设第一个 RTT 样本 R = 100ms。首样本按规则:SRTT = 100ms,RTTVAR = 50ms,RTO = 100 + 4×50 = 300ms。

第二个样本 R = 120ms(网络略慢一点):

  • RTTVAR = 0.75×50 + 0.25×|100 - 120| = 37.5 + 5 = 42.5ms
  • SRTT = 0.875×100 + 0.125×120 = 87.5 + 15 = 102.5ms
  • RTO = 102.5 + 4×42.5 = 272.5ms

RTO 反而降了,因为这次偏差不大,抖动估计被拉低。继续。

第三个样本 R = 400ms(明显抖动,可能排队了):

  • RTTVAR = 0.75×42.5 + 0.25×|102.5 - 400| = 31.875 + 74.375 = 106.25ms
  • SRTT = 0.875×102.5 + 0.125×400 = 89.69 + 50 = 139.69ms
  • RTO = 139.69 + 4×106.25 = 564.7ms

一个 400ms 的样本,直接把 RTO 从 272ms 顶到 564ms。这就是“宁可多等”的代价:一次异常大的 RTT 样本会让 RTO 在接下来一段时间内变钝。如果你的业务链路上经常出现偶发的大 RTT(比如跨可用区、共享带宽、虚拟化环境里的调度抖动),RTO 会被反复撑大,真丢包时的恢复就会变慢。这也解释了为什么有些“偶发长尾”问题看起来毫无规律——它取决于历史 RTT 样本的分布。

3.3 边界处理:下限、上限、退避与 Karn 算法

RTO 算出来之后还要过几道闸门,这几条规则都是踩过坑才定下来的。

下限 200ms。Linux 的TCP_RTO_MIN是 200ms。为什么不许更小?因为在同机房内网这种 RTT 只有几十微秒的环境里,算出来的 RTO 会小到几毫秒,那么任何一次正常的网卡调度延迟、中断合并、甚至对端延迟 ACK,都会造成大量虚假重传。虚假重传的害处不只是浪费带宽,它还会把拥塞窗口打下去,让连接长期跑不到满速。所以内核干脆设个地板,宁可慢一点也不要乱判。

上限 120 秒。Linux 的TCP_RTO_MAX是 120 秒(RFC 6298 建议 60 秒,Linux 更宽容一些)。这个上限的意义在于:如果对端真的彻底没了,你也别没完没了地重试,早点放弃反而好。

指数退避。每次超时重传后,RTO 翻倍,直到上限。这样一来,一条连续丢包的连接,重传间隔会走成这样(从 200ms 起步):

200ms → 400ms → 800ms → 1.6s → 3.2s → 6.4s → 12.8s → 25.6s → 51.2s → 102.4s → 120s → 120s → ...

累加到十几分钟量级就对应上了内核的tcp_retries2默认值 15——超过这个次数,内核直接判定连接死亡,向上层报错。

Karn 算法。这一条最容易被忽略。问题在于:如果一个段被重传了,你收到 ACK 时,怎么知道这个 ACK 确认的是原始发送还是重传那次?如果算错了,就会把一个含重传等待的、被严重放大的时间当成 RTT 样本喂给公式,SRTT 立刻虚高,后续所有超时判断都会被带偏。Karn 算法的做法简单粗暴:凡是发生过重传的段,一律不采样 RTT。这条规则保住了 SRTT 的准确性,代价是丢包频繁的链路上采样点变少,收敛变慢。

而后来时间戳选项(TCP Timestamps,net.ipv4.tcp_timestamps)解决得更优雅:每个段带上发送时间戳,ACK 里回显,这样即使重传了也能精确算出是哪一次被确认,重传歧义直接消失。所以现代内核默认开着时间戳,Karn 算法的限制实际上被绕过了。

3.4 快速重传的阈值与它的先天短板

快速重传的触发条件是收到 3 个重复 ACK(dupthresh,内核里对应net.ipv4.tcp_reordering,默认 3)。为什么是 3 而不是 1?因为网络会乱序。如果只差 1 个重复 ACK 就重传,那轻微乱序(后发的包先到)就会引发大量无谓重传。3 这个数字是个折中:容忍掉大多数乱序,同时不至于错过真实丢包。

但快速重传有三个先天短板,理解这三条能省掉很多排查时间:

第一,发送窗口太小的时候触发不了。如果窗口里只有一两个段在飞,你根本凑不出三个重复 ACK。这就是为什么短连接、小请求(比如某些 RPC 场景)表现和长连接完全不一样。

第二,尾部的包丢了最难发现。一批数据发的最后几个段丢了,后面没有新数据去触发重复 ACK,只能等 RTO。这是典型的“长尾延迟”来源,而且尾部丢包在小批量传输里概率还不低。

第三,丢包太多时重复 ACK 也可能被吞。如果接收端后续的段也丢了一大半,重复 ACK 数量不够,同样触发不了。

针对第二条,后来出现了 TLP(Tail Loss Probe,尾丢包探测):在“快要超时但还没超时”的时刻(约 2×SRTT 左右),主动发一个探测包(新数据或者重传最后一个未确认段),逼对端回一个 ACK,从而把 RTO 才能发现的问题提前到快速重传的时间尺度上解决。Linux 里net.ipv4.tcp_early_retrans控制这个行为。TLP 解决的是“等 RTO 太久”的核心痛点,把几秒的等待压到几十毫秒。

4. 一台机器上的完整观测链路:从计数到抓包

4.1 先用 ss 看连接级状态,快且直观

排查重传,我一般不问业务方要日志,第一步永远是ss。它足够快,而且能直接看到单个连接的 RTO 和重传计数:

# 只看有重传的 TCP 连接,带详细计时器信息 ss -tin state established '( sport = :8080 )' # 输出里几个关键字段(不同内核版本略有差异): # rtt:0.35/0.08 —— 当前 SRTT / RTTVAR,单位 ms # rto:201 —— 当前生效的 RTO,单位 ms # ato:40 —— ACK 超时(延迟 ACK 计时),单位 ms # retrans:0/3 —— 当前未确认段的重传次数 / 连接累计重传次数 # backoff:3 —— 当前 RTO 退避次数 # lost:12 —— 被标记为丢失的段数 # cwnd:10 —— 拥塞窗口

我一般盯两个东西:retrans的分母(累计重传数)和backoff。分母在涨说明确实有重传发生,但还不能说明是“真丢包”还是“误判”;backoff大于 0 说明已经进入指数退避了,那这段连接现在的恢复速度是成倍下降的,属于要紧问题。

ato:40这个字段也值得多说一句。它反映的是延迟 ACK 计时。Linux 接收端在没有开启 quickack 的情况下,会等一小会儿(典型 40ms)看有没有第二个段到达,好合并成一个 ACK。这个 40ms 和 Nagle 算法撞在一起,就是经典的“小包 40ms 延迟”问题:发送端 Nagle 等着前面小包的 ACK,接收端延迟 ACK 等着攒够数据再回,两边互相等,硬生生耗掉 40ms。这个不算重传问题,但很多“偶发慢 40ms”的案例都是它,排查时别走错门。

4.2 再看协议栈全局计数,判断影响面

单条连接看ss,要判断“是不是全网都在丢”,就得看全局计数器:

# netstat 的 TCP 扩展统计,重点看这几项 nstat -az | grep -E 'TcpRetransSegs|TcpExtTCPTimeouts|TcpExtTCPLostRetransmit|TcpExtTCPSynRetrans|TcpExtTCPSpuriousRTOs|TcpExtTCPFastRetrans' # 或直接读 proc cat /proc/net/netstat | awk '/^TcpExt:/{for(i=1;i<=NF;i++) if($i ~ /Retrans|Timeout|Spurious|Lost/) print $i}'

这些计数器的含义需要分清楚,不然容易读错:

计数器含义怎么解读
TcpRetransSegs重传的报文段总数绝对值没意义,要看它占总发送段数的比例
TcpExtTCPTimeoutsRTO 超时次数涨得快说明快速重传没兜住,尾部丢包或窗口太小
TcpExtTCPFastRetrans快速重传次数和 Timeouts 比值能反映丢包位置特征
TcpExtTCPSpuriousRTOs虚假超时(重传了但其实没丢)有 DSACK 才能识别,涨说明 RTO 设得太激进或抖动大
TcpExtTCPLostRetransmit重传的包又丢了严重信号,说明链路质量很差或拥塞严重
TcpExtTCPSynRetrans握手阶段 SYN 重传高的话说明是建连问题,不是数据传输问题

我自己的经验阈值:如果TCPTimeouts / TcpRetransSegs这个比值明显偏高(比如超过一半),说明大部分重传是靠超时触发的,而不是快速重传——这基本指向“小包、短流、尾部丢包”这三种场景之一。反过来如果快速重传占绝对多数,那就是大流量传输中途丢包,属于比较“健康”的丢包形态,恢复会很快。

4.3 抓包确认:区分真丢包、假重传与乱序

计数只能告诉你“有重传”,抓包才能告诉你“为什么”。我常用的过滤表达式就这么几个:

tcp.analysis.retransmission # 所有重传 tcp.analysis.fast_retransmission # 快速重传 tcp.analysis.spurious_retransmission # 被判定为虚假的重传 tcp.analysis.lost_segment # 抓包点看到序列号空洞 tcp.analysis.duplicate_ack # 重复 ACK tcp.analysis.zero_window # 零窗口通告

看抓包的时候有几个判断要点,都是被坑出来的:

先确认抓包点的位置。在发送端抓,你看到的重传是“内核真的重发了”;在接收端抓,你看到的重传可能是“包在路上乱序了,看起来像重传”。同一个现象,两个位置的结论完全相反。理想情况是两端同时抓,比对时间戳。

看重复 ACK 的数量。只出现一两个重复 ACK 然后数据就补齐了,那是乱序,不是丢包。连续三个以上重复 ACK 之后紧跟一个重传段,才是标准快速重传。

看虚假重传。Wireshark 标了spurious_retransmission,说明原包其实到了,只是到得晚(被乱序或者被中间设备拖了)。这类重传对性能的伤害其实比真丢包更大,因为它会不必要地砍拥塞窗口。如果虚假重传频繁,通常说明链路上有严重的乱序源(多路径、链路聚合、某些中间设备),这时候调大tcp_reordering反而比调 RTO 更对症。

看时间差。重传段和原段的发送间隔,如果刚好接近当时的 RTO 值,那是超时重传;如果间隔只有几毫秒到几十毫秒,且前面有重复 ACK,那是快速重传。这个时间差是最直接的判据,比看标记还可靠。

4.4 关键内核参数:改哪个、为什么改、改完什么后果

参数能调,但我一贯的态度是:先搞清楚现象属于哪一类,再决定要不要动参数。下面这张表是我实际调过的,按“调整收益/风险比”排序:

参数默认适用场景风险
net.ipv4.tcp_sack1保持开启,几乎无理由关闭关闭后退化明显
net.ipv4.tcp_timestamps1保持开启,解决重传歧义关闭后 RTT 采样变糙
net.ipv4.tcp_reordering3链路乱序严重时适当调大调太大延迟发现真丢包
net.ipv4.tcp_retries215内网可适当调小加快失败太小会误杀慢连接
net.ipv4.tcp_syn_retries6建连超时时间敏感时调小太小跨洋建连易失败
net.ipv4.tcp_early_retrans3保持默认,启用 TLP关闭后尾部丢包恢复变慢
net.ipv4.tcp_rto_min200极低延迟内网可尝试下调假重传风险明显上升

重点说两个容易改错的。

tcp_retries2不要随便调大。它决定了一条连接在连续重传失败多少次后放弃。默认 15 对应十几分钟,很多人觉得“太短了,我的任务要跑半小时”,于是调大。问题是这个计时是按指数退避走的,调大之后连接死亡的时间会被拉到小时级,而应用层如果没设自己的超时,就会一直阻塞在那里,占着线程和连接资源。正确的做法是应用层设超时,而不是把内核的兜底时间拉长。

tcp_rto_min慎调。有同学看到内网 RTT 只有 0.1ms,觉得 RTO 最小 200ms 太浪费,改成 10ms。短期看长尾消失了,但只要链路上有一点排队或者对端有一次延迟 ACK,立刻就是成片的假重传,吞吐反而掉下去。这个下限不是拍脑袋定的,是大量实践折中出来的。

5. 疑难场景实录:六类典型问题怎么定位

5.1 问题一:RTO 频繁超时但抓包看不到明显丢包

这是最典型的一类。现象是TcpExtTCPTimeouts持续增长,但两端抓包对比下来,数据其实都到了,只是到得比 RTO 晚。可能的成因有三个:一是链路本身有规律性的大延迟毛刺(比如共享带宽被别的流量挤了一下),导致 RTT 样本忽大忽小,RTO 跟不上;二是存在严重的乱序,乱序被误判成丢包;三是发送端和接收端的时钟/定时器精度问题。

定位方法是看TcpExtTCPSpuriousRTOs。这个计数需要 DSACK 支持才能统计——接收端发现收到的段是重复的,就通过 DSACK 告诉发送端“你刚才那个重传是多余的”。如果虚假超时占比高,优先查乱序源,其次考虑把tcp_reordering从 3 调到 6 甚至更高试试,观察SpuriousRTOs是否下降。

5.2 问题二:只有尾部几个包反复重传

如果你在抓包里看到每次传输的最后 1 到 2 个段总是要等 RTO 才被重发,那就是典型的尾部丢包。这类问题在“小请求、大响应”或者“分块传输”的场景里特别常见。

对策上,TLP 是内核层面已经做好的补丁,确认tcp_early_retrans处于开启状态(默认 3)。应用层能做的是减少“最后一批数据发完就干等”的模式,比如在协议设计上让接收端对最后一个段也尽快回 ACK,或者干脆用应用层心跳/确认来兜底,别把可靠性完全压在 TCP 的尾巴上。

5.3 问题三:连接活着但一动不动,重传计数不涨

这种情况别查重传,先看窗口。ss -ti里如果看到send-q满了但retrans不动,八成是对端通告了零窗口。抓包会看到发送端周期性发一个字节的窗口探测,接收端回“我窗口还是 0”。

原因通常是接收端应用读得太慢,接收缓冲区被塞满。这时候该查的是应用层的消费速度、读循环有没有阻塞、接收缓冲区大小(net.core.rmem_max、net.ipv4.tcp_rmem)是否合理。把重传参数调来调去是南辕北辙。

5.4 问题四:重传的包又丢了

TcpExtTCPLostRetransmit增长是很严重的信号。它意味着第一次丢包触发的重传,自己也丢了。这种情况下 RTO 会快速退避,延迟直接上台阶。

成因一般是链路质量差或者已经严重拥塞。这时候要做的是查拥塞控制算法的行为(ss -ti里的cwnd和ssthresh,看是不是反复被打到最低),以及确认发送速率是否远超链路承载能力。盲目调大重传次数没用,重传的包还是会丢。

5.5 问题五:握手阶段就慢

TcpExtTCPSynRetrans高位,说明问题在建连。SYN 的重传次数由tcp_syn_retries控制,默认 6 次,退避间隔是 1、2、4、8、16、32 秒,累加约 127 秒才彻底放弃。如果你的业务观察到“建连偶尔要等十几秒”,那基本就是 SYN 丢了一次两次。

要区分是客户端侧还是服务端侧的问题:客户端看 SYN 重传,服务端看tcp_synack_retries。还要注意 SYN 队列(net.ipv4.tcp_max_syn_backlog)和 accept 队列(net.core.somaxconn)是否溢出,溢出会表现为“握手看起来完成了但应用 accept 不到”,和纯丢包的现象很像但根因完全不同。

5.6 问题六:虚假重传导致吞吐上不去

这条前面提过,但它值得单独列出来,因为它的表象是“吞吐上不去”,很容易被误判成带宽问题。虚假重传会触发拥塞窗口收缩,连接反复从慢启动爬,永远跑不满。

判断方法:ss -ti连续采样几次,看cwnd是不是在大幅波动(比如从几十掉到 10 又慢慢涨)。如果同时SpuriousRTOs在涨,那基本确诊。处理方向是减少乱序、增加tcp_reordering,以及在应用层尽量用长连接而不是频繁短连接,让拥塞窗口有机会长大。

我把上面这些整理成一张速查表,方便对着现象找方向:

现象首要怀疑先看什么常见处置
秒级长尾,重传计数涨RTO 超时ss的rto、backoff查尾部丢包、确认 TLP 开启
重传多但抓包无丢包乱序误判SpuriousRTOs、DSACK调大tcp_reordering
连接停滞,重传不涨零窗口ss的send-q、窗口字段查应用消费速度、缓冲区
重传的包又丢链路拥塞cwnd、ssthresh查发送速率、拥塞算法
建连慢SYN 丢包TCPSynRetrans查队列、调tcp_syn_retries
吞吐低且cwnd抖动虚假重传连续采样cwnd减少乱序、长连接复用

注意:所有参数调整都应该有“改前基线—改动—改后对比”的闭环。我见过太多人凭感觉改了tcp_retries2和tcp_rto_min,结果把原本清晰的现场搅浑了,最后连问题是什么都说不清。

6. 工程上的取舍:应用层超时该怎么和内核配合

6.1 别把可靠性完全外包给 TCP

这一段是我最想强调的。很多人有个默认假设:用了 TCP 就自动可靠,应用层不用管超时。这个假设在单条短连接、小请求的场景下基本成立,但一旦进入长连接、跨机房、批量传输的场景,就会被现实打脸。

原因是 TCP 的超时时间尺度是内核定的,而且它会退避。一条已经进入退避的连接,恢复可能是几十秒甚至分钟级,而你的业务能容忍的是几百毫秒。两个尺度差了两三个数量级,硬等只会把线程池占死。

正确的分层是这样的:内核的 RTO 负责“数据最终一定会到或者连接最终一定会死”,应用层的超时负责“在我能接受的时间内给我一个明确结果”。应用层超时触发时,不要指望这条连接还能继续用——大概率它正卡在重传里。稳妥的做法是直接关闭连接、重新建连、换一条路径重试。这不是浪费,因为一条正在退避的连接,留着它的收益很可能低于重新握手的成本。

实践中我会设三层超时:连接建立超时(几百毫秒到一两秒,取决于跨不跨机房)、单次请求超时(业务能接受的上限)、总重试超时(防止重试把延迟放大)。三层都要有,缺一层就会出现“某一层无限等待”的坑。

6.2 长连接的两个隐性代价

长连接能避免握手开销、能让拥塞窗口积累,但它带来两个和重传相关的隐性代价,得心里有数。

第一,长连接会被中间设备的状态影响。有些中间设备会记录连接状态,一段时间没流量就悄悄把表项删了,之后发过去的包直接丢,而两端都以为连接还在。这种“半死连接”是重传机制的噩梦:发送端开始重传、退避,最后可能十几分钟才报错。对策是开 keepalive 或者应用层心跳,把探测间隔设在中间设备的老化时间之内。

第二,拥塞窗口的收益是有条件的。窗口能积累,前提是这条连接持续有流量、没有被丢包打下去。如果连接上本来就偶发丢包,那么长连接反而会一直带着一个偏小的ssthresh,表现还不如新建一条从慢启动重新爬的连接。这也是为什么有些团队在抖动链路上会主动做“连接轮换”,逻辑上看着反直觉,但实测有效。

6.3 我踩过的两个具体坑

第一个坑:早期做数据同步,用的是单条长连接串行推送。一次跨机房抖动,触发了连续重传加退避,整条链路卡了两分多钟,业务侧看到的是“同步完全停了”。当时第一反应是查对端,结果对端好好的。后来加上ss -ti才看到backoff已经到 5 了。这个问题的根因不是参数,而是单连接串行这个架构——一条连接卡住,整个通道全停。改成多连接并行、并且对单条连接的连续超时做主动剔除后,这类问题就再没出现过。

第二个坑:把tcp_retries2从 15 调到 8,想着“快点失败快点重试”。结果发现在弱网环境下,一些本来只是慢一点的连接被提前掐死,重试次数上升,总体成功率反而下降。回滚之后才想明白:这个参数该按“最坏情况下我愿意等多久”来设,而不是按“平均情况”。后来改成应用层设超时、内核参数保持默认,问题消失。

6.4 关于新机制的一点实践感受

内核这几年在重传上有不少改进,比如基于时间的丢包判定(RACK 那套思路)、TLP、还有更精细的重传队列管理。它们的共同思路是:不再单纯依赖“收到几个重复 ACK”这种计数信号,而是用“某个包比已经确认的包早发了多久还没被确认”这种时间信号来判断丢失。这个转变的意义在于,它对乱序的容忍度更好——乱序只会让包晚到,不会让它比已确认的包“早发很久”,所以误判率下降。

实际的感受是:在同样一条有抖动的链路上,新内核的重传行为明显更“克制”,虚假重传少了,吞吐曲线也更平。但要注意,这些机制依赖于时间戳选项(tcp_timestamps)。如果因为某些原因关掉了时间戳,这些能力基本都用不上,行为会退化回比较老的那一套。这也是我一直建议保持时间戳开启的原因之一——它不只是省几个字节的 RTT 采样精度问题。

排查顺序上我现在的习惯是:先ss看单连接状态和backoff,再nstat看全局比例,然后抓包确认是超时重传还是快速重传、是真丢包还是乱序,最后才考虑参数。这个顺序能把绝大多数“疑似重传”的问题在十分钟内分类清楚。反过来,一上来就翻参数、凭感觉调,往往是把简单问题复杂化。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询