个人主页:一条泥憨鱼(欢迎各位大佬莅临)❄️《数据结构》 ❄️《AI与Agent那些事》❄️《从0开始学计算机网络》 ❄️《后端开发》前言你一定遇到过这种情况点开一个网页浏览器标签页上的小圆圈一直转啊转转了十几秒最后啪一下给你一个「无法访问此网站」。这十几秒里到底发生了什么为什么不是立刻报错而是非要等这么久要回答这个问题就得聊到一个听起来挺唬人、但其实很朴素的东西TCP 超时重传时间。英文缩写叫RTORetransmission Timeout。搞懂三件事数据包、确认和超时在聊 RTO 之前得先建立三个最基本的概念。别急我们一个一个来。第一件数据包。你在网上传的每一样东西——一张图片、一段文字、一个视频的碎片——都不是囫囵个儿发过去的。计算机发送数据的方式更像寄快递把大件东西拆成一个个小包裹每个包裹上写好「收件人地址」「寄件人地址」「这是第几个包裹」然后一个个发出去。这个小包裹就叫数据包packet。第二件ACK 确认。寄快递最怕什么最怕东西寄丢了还不知道。所以快递公司有个「签收回执」的服务——收件人签收后快递员把回执寄回来你看到回执就知道东西送到了。TCP 也是这么干的。收件方接收方每收到一个数据包就要回一个小纸条给发送方说「我收到了」。这个小纸条叫ACKAcknowledgment确认。ACK 本身也是一个小数据包只不过它里面不装你真正要传的内容只装一句「第 N 号包裹我收到了」。第三件超时。现在问题来了。发送方把数据包发出去然后开始等 ACK。如果 ACK 一直没回来有两种可能1. 数据包在路上丢了对方根本没收到2. 对方收到了但 ACK 回程的路上丢了。不管是哪种对发送方来说结果都一样没等到回执。那怎么办只能重发一遍。但「等多久才重发」是个技术活。等太短ACK 可能还在路上你就急吼吼地重发结果对方收到两份重复数据白白浪费带宽等太长真丢包了你还在那儿傻等用户体验就是浏览器转圈转半天。这个「等多久」的时间就是 RTO——超时重传时间。打个比方你在网上买了个东西商家说「三天到」。你等到第三天还没到就开始怀疑是不是丢件了决定联系客服补发。这个「三天」就是 RTO。设成一天快递可能还在路上你就催了设成三十天那你这一个月都干等着。所以 RTO 的核心问题只有一个这个值到底该设多少固定时间的坑为什么不能写死一个值早期的 TCP 实现很朴素直接写死一个值。比如规定 RTO 就是 1 秒发出去等 1 秒没收到 ACK 就重发。听起来挺简单但马上就会撞墙。想象一下寄快递同城快递可能当天就到跨洋快递得等一两周。如果你用同一个「超时时间」去判断这两种情况必然出问题。- 用同城的标准比如一天去等跨洋快递那跨洋的包裹天天「超时」你天天补发纯属浪费。- 用跨洋的标准比如两周去等同城快递那同城真丢件了你也得等两周才发现黄花菜都凉了。网络世界一模一样。同一个机房里两台服务器之间一来一回可能只要 0.1 毫秒你从家里访问美国西海岸的服务器一来一回可能要 200 毫秒差了 2000 倍。固定 RTO 根本应付不了这种差异。唯一的出路就是让 RTO 跟着实际网络状况动态调整。那怎么知道「实际网络状况」呢只有一个办法亲自测。RTT 是唯一的线索先测出「一来一回要多久」我们刚才说发送方发出数据包要等 ACK。那「发出去」到「收到 ACK」之间这段时间是可以测出来的。这个时间有个名字叫RTTRound-Trip Time往返时间。字面意思就是「一来一回」花了多久。测法非常朴素1. 发送数据包的那一刻记下时间 t12. 收到对应 ACK 的那一刻记下时间 t23. RTT t2 - t1。用伪代码写出来大概是这样# 简化版测一次 RTT t1 当前时间() 发送数据包(序号1) 等待 ACK(序号1) # 阻塞在这里直到收到 t2 当前时间() rtt t2 - t1 print(这次往返花了, rtt, 毫秒) 那是不是直接把测出来的 RTT 当成 RTO 就行了比如测得 RTT 是 100 毫秒那 RTO 就设 100 毫秒不行.因为网络是会抖动的。你连续测 5 次可能得到 100、105、98、300、102 毫秒——那个 300 就是个异常值可能是那一瞬间网络堵了一下。如果你把 RTO 设成刚刚测到的 100 毫秒下一次网络稍微抖一下ACK 稍微晚回来一点点你就误判成丢包开始重传。重传又会加重网络负担越堵越重传越重传越堵最后整个连接崩掉。所以直接拿单次 RTT 当 RTO是个坑。我们需要一个更聪明的办法既参考 RTT又不被个别抖动带偏。平滑与加权RTO 公式的进化史接下来的内容稍微有点数学但每一步我都先讲「为什么需要它」再给公式。跟着走就行。第一步把抖动的 RTT 抹平——SRTT既然单次 RTT 会抖那就多测几次取个平均。但不是简单地把所有历史值加起来除以个数而是用一种叫移动平均的方法 新的平均值 (1 - α) × 旧的平均值 α × 这次测到的 RTT这个 α是个 0 到 1 之间的小数字比如 0.125。它的作用是新测到的值只占一小部分权重大部分还是沿用之前的平均值。这样个别异常值就不会把平均值带跑偏。这个算出来的平均值叫SRTTSmoothed Round-Trip Time平滑往返时间。「平滑」两个字说的就是这个抹平抖动的过程。# 模拟连续测量 RTT并计算 SRTT alpha 0.125 srtt None rtt_samples [100, 105, 98, 300, 102, 101, 99, 103] # 单位毫秒 for rtt in rtt_samples: if srtt is None: srtt rtt # 第一次没有历史值直接采用 else: srtt (1 - alpha) * srtt alpha * rtt print(f这次 RTT{rtt}msSRTT{srtt:.2f}ms)你会发现那个 300 毫秒的异常值进来后SRTT 只是稍微往上抬了一点点很快又被后面的正常值拉回来。这就是「平滑」的威力。第二步光有平均值不够——RTTVAR但只有平均值还是不行。假设 SRTT 是 100 毫秒。可如果网络波动很大实际 RTT 在 50 到 200 之间乱跳那 100 这个平均值根本代表不了什么。你要是把 RTO 设成 100一半的包都得误判超时。所以除了「平均有多快」我们还得知道「波动有多大」。这个波动的度量叫RTTVARRound-Trip Time Variation往返时间偏差。它的算法跟 SRTT 很像也是移动平均只不过它平均的是「这次测到的 RTT 和当前 SRTT 差了多少」 新的偏差 (1 - β) × 旧的偏差 β × |SRTT - 这次测到的 RTT|β贝塔一般取 0.25。这个公式的意思就是实时盯着每次测量和平均值偏离多远把偏离程度也做个平滑。第三步合起来就是经典公式有了平均值 SRTT又有了波动范围 RTTVARRTO 就顺理成章了 RTO SRTT 4 × RTTVAR翻译一下超时时间 平均往返时间 4 倍的波动范围。为什么乘 4因为在实际网络里绝大多数 RTT 都落在「平均值 ± 4 倍偏差」这个范围里。乘 4 相当于给了一个足够宽的缓冲区既不会因为一点小抖动就误判超时也不会宽到真丢包了还傻等。这个公式在 1988 年由 Jacobson 和 Karels 提出是 TCP 沿用至今的经典算法。完整代码长这样class RTOEstimator: def __init__(self): self.srtt None self.rttvar None self.rto 1.0 # 初始 RTO 给个保守值比如 1 秒 def update(self, rtt): 每测到一次 RTT就更新一次 RTO if self.srtt is None: # 第一次测量直接初始化 self.srtt rtt self.rttvar rtt / 2 else: alpha, beta 0.125, 0.25 # 先更新波动范围 self.rttvar (1 - beta) * self.rttvar beta * abs(self.srtt - rtt) # 再更新平均值 self.srtt (1 - alpha) * self.srtt alpha * rtt # 经典公式 self.rto self.srtt 4 * self.rttvar # 实际实现里 RTO 通常有下限比如 200ms和上限比如 60s self.rto max(0.2, min(self.rto, 60.0)) return self.rto # 用一组模拟数据跑一遍 est RTOEstimator() for rtt in [100, 105, 98, 300, 102, 101, 99, 103]: print(fRTT{rtt}ms - RTO{est.update(rtt):.1f}ms) 跑一遍你会看到那个 300 毫秒的异常值进来后RTO 会适度抬高因为波动变大了但不会失控。这就是我们想要的既能跟得上网络变化又不至于一惊一乍。别忘了退避连续丢包时 RTO 要翻倍讲到这里RTO 的计算看起来已经完备了。但还有个场景没考虑如果重传之后ACK 还是没回来怎么办网络真堵死了或者链路真断了那重传一次没用重传两次也没用。这时候如果每次都按同一个 RTO 去等就会一直傻等下去效率极低。解决办法叫指数退避exponential backoff- 第一次重传等 RTO- 第二次重传等 2 × RTO- 第三次重传等 4 × RTO- 第四次8 × RTO……为什么要翻倍因为如果网络真的堵了你越频繁重传堵得越厉害。拉长等待时间相当于给网络一个喘息的机会。而且这也是在赌如果对方真的只是暂时收不到那么等久一点说不定就通了。这个策略在 TCP 里是硬性规定。Linux 默认最多重传 15 次累计等待时间会拉长到十几分钟——这也是为什么有时候你感觉网页「卡了很久很久才报错」。一个容易被忽略的坑Karn 算法这里有个坑第一次接触的人基本都会踩。刚才我们说RTT 是靠「发出去」和「收到 ACK」两个时间点相减测出来的。那问题来了如果这个数据包被重传过你测到的 RTT 到底算哪一次的假设你发了数据包等了一会儿没等到 ACK于是重传。重传之后收到了 ACK——这个 ACK 是对第一次发的响应还是对第二次重传的响应你没法确定。如果是第一次的那真实 RTT 很短如果是对重传的响应那 RTT 要算上超时等待的时间会大得多。用这种模糊的测量值去更新 RTO会把平均值带跑偏。解决办法叫Karn 算法规则很简单 被重传过的数据包它的 ACK 不拿来测 RTT。宁可少测几次也不能测错。这个规则在实现里就是加个标记重传过的包收到 ACK 时只做「确认收到」的处理不更新 SRTT 和 RTTVAR。顺带一提退避之后什么时候把 RTO 重置回正常值答案是当收到一个新数据的 ACK 时。因为这说明网络已经恢复正常之前的超时是偶发事件。收尾一张全景图把整条链路串起来RTO 的计算大概是这样的1. 发出数据包记时间 t12. 收到 ACK记时间 t2算出这次 RTT3. 用移动平均更新 SRTT平均往返时间4. 用移动平均更新 RTTVAR波动范围5. 算出 RTO SRTT 4 × RTTVAR6. 如果没等到 ACK超时后重传并且 RTO 翻倍7. 重传过的包不参与 RTT 测量Karn 算法8. 收到新数据的 ACK 时RTO 重置。作为开发者你不需要自己手算 RTO——操作系统内核全帮你搞定了。但理解这套机制能帮你在排查问题时读懂现象- 用 ss -ti 看连接的 RTO、RTT、重传次数一眼就能看出是不是网络在抖- 用 Wireshark 抓包看到「TCP Retransmission」标记你就知道是超时重传触发了- 遇到「接口偶尔慢一下」先看看是不是 RTO 被拉长了而不是急着去优化代码。网络这东西很多「玄学」问题拆开看其实都是很朴素的机制在起作用。RTO 就是其中一个。下次再看到浏览器转圈十几秒你大概能猜到它背后经历了什么了。