TCP RTO 是它 40 多年前唯一丢包检测策略,也是当前最后的丢包检测兜底策略,它几乎从没变过。
有个咨询挺有趣,以其案例为背景写篇随笔。大致意思是,嫌 TCP RTO 太大,游戏场景丢包卡顿怎么办?我提供了几行代码,果见成效。
不得不承认一个事实,如今绝大多数 TCP RTO 均在 200ms 左右,因为 minrto 规定为 200ms,而大多数 TCP 连接均为就近而稳定的 CDN 连接,排除少量贫困偏远地区(他们也不在乎网络质量~),裸测 RTT 中位数不超过 50ms,按照 VJ RTO 公式 srtt + 4·rttvar 计算,远小于 200ms。
依旧将 minrto 设置为 200ms,此中原因在于对 Delayed ACK 的适应,而 Delayed ACK 在 RFC1122 规定,其最大 timeout 为 500ms,但一般而言,Linux 系统设置为 40ms,Windows 系统为 200ms。因此 minrto = 200ms 足以覆盖大多数场景而不至于 RTO 过于激进产生的不必要重传加重网络拥塞,参见 TCP MIN_RTO 辩证考。
大家都以为 Linux 内核有个参数可以配置 minrto,就像以为有个参数可以配置 TCP timewait 时间一样,然而直到 6.x 内核,minrto 的 sysctl 配置才被支持,但早在 2014 年前后,各大 CDN 厂商就纷纷私下里支持了该配置,每当业务咨询连接超时问题时,该配置可谓神器,可想而知,如今 Linux 6.x 对其正式支持,也是这些厂商所为,提个派池而已。
我没有以 “配置一个更小的 minrto” 回应该咨询,因为这种配置往往会因为众口难调的原因而弄巧成拙:
- 使用 sysctl 配置全局 minrto 无法适应所有尺度的 TCP 连接;
- 使用 iproute2 配置单路由 minrto 同上,只是范围缩小些;
- 使用 sockopt,ebpf 配置单连接 minrto 无法适应连接内的尺度变化;
我提供的是根据连接当前性质实时计算 RTO 的细粒度方案,专门针对 thin TCP stream,也就是拥塞无害的细长流,它有一份古老的文档:Thin-streams and TCP,早在 2.6 时代就部署。修改 tcp_rearm_rto 和 tcp_retransmit_timer:
- 两个函数为 thin TCP stream 提供与 TLP 一致的超时时间以替换 RTO;
- 重新定义 thin TCP stream;
可以简单理解上述修改,即放宽了 RFC8985-RACK-TLP “1.2. Motivation” 的约束,加上 “when the entire flight is lost and the stream is thin” 也能享受 TLP。
传统意义上,packet_out < 4 的连接被认为 thin TCP stream,因为 dupack/sacked 无法超过 3 而触发 fast retransmit,3 同样是传统意义上的阈值。我将其修改:
- inflight < tp->reordering + 1 即为 thin TCP stream;
配合现有 TCP 重传策略,这就是一整套丢包检测和重传算法:
- RFC6675 提供传统快速重传方案;
- RACK/TLP 提供 tail drop 重传,兜底 RFC6675;
- RFC5682 提供传统 RTO 兜底 RACK;
- 我的算法为 thin TCP stream 优化 RTO;
当然,RTO 本身的计算方式无需改变,若不是有 Delayed ACK 捣乱,它还是要比 TLP PTO = 2·SRTT(若 inflight == 1,还要和 max_ack_delay 胶着) 更加优秀,参见 VJ 对 SRTT/RTO 公式参数的推导。
后面是关于丢包检测和重传的形而上话。
TLP 的细节我今年过年期间专门说过,它覆盖了 AAAL,AALL,ALLL,LLLL,>=5 L 等所有 packet_out > 1 的丢包场景,与 RACK 天然搭配。
我一向觉得 RACK 具有里程碑意义,因为它的引入,TCP 丢包检测变得更简单直观。RACK 取消了 FACK,ER(Early Retrans),thin Fast Retrans,NCR 等一众票凌乱离散的丢包检测和重传策略,全部统一到了自己的时间序中。代码简化了不少,统一于 net/ipv4/tcp_recovery.c。
可能正因为 RACK 有如此威力,RTO 变得极少被触发,反而被遗忘了,没人单独优化它。
但一旦涉及 RTO,就必须重新审视关于 TLP 和 RACK 的整个背景和适用前提。
首先,Fast Retrans 和 TLP 均假设有足够的 seg 触发重传,RACK 取消掉的 thin Fast Retrans 亦依赖 ACK-selfclock,如果丢失 ACK-selfclock,还是要掉入 RTO。tail drop 可用 TLP 优化,但 pingpong 模式的应用遭遇 drop 则不能,在 RACK: a time-based fast loss detection algorithm for TCP 有个场景:
Structured request-response traffic turns more losses into tail drops. In such cases, TCP is application-limited, so it cannot send new data to probe losses and has to rely on retransmission timeouts (RTOs).
典型的很 thin 的 stream,很容易全部丢失,如此一来 ACK-selfclock 丢失,RACK,TLP 全都派不上用场。
其次,自定义 TCP 相关参数时代以来,网络环境已经发生重大变化,但这些参数的缺省值却很少发生改变,而这些缺省值往往影响 TCP 的性能。其中以 RTO 为严重,因为 RTO 由本地 clock 触发,它不像 ACK-selfclock 可在闭环上玩很多花活,行为和参数相互依赖,只需改变算法,不太依赖参数。
是时候重新测量新的统计值了,但如今庞大的互联网已经变得难以测量,虽然很难给出一个更合适的 minrto,但可以确定的是,哪些场景下可以不受 minrto 约束,这是我这个优化的初衷。
回到原点,minrto 保守化的原因在于避免不必要的重传而加重拥塞,但它与丢包不能及时恢复相比,要两权相害取其轻,对于 block 传输,自然要优先考虑避免拥塞问题,但对于 thin stream,比如游戏,远程登录等非 capacity-seeking 传输,其 inflight 不随带宽而增加,不足以对现代网络的负载产生可观测影响,因此它们并不是拥塞控制的目标。这是我这个优化的背景。
基于 “对 thin stream 要激进探测丢包并重传” 的观点,拥塞无害的 thin stream 不必矜持保守地等待超时,我给出该优化。在此之前,我曾经以三三两两倒序发送,尾部跟随乱序包等花活儿被经理表扬。
此外,有一个关于 TLP && inflight == 1 的讨论,标准的做法是 “PTO = 2 srtt + delayed_ack 容忍期”,但本着 “thin stream 拥塞无害” 的观点,代之以 “在唯一的报文后紧紧尾随一个曾经发送过乱序字节(或整个报文)以取消 receiver 的 delayed ack” 则更高尚,以无所谓的空间代价换宝贵的时间收益。
最后,说说 RFC4653-Non-Congestion Robustness (NCR) for TCP,从两个视角看看对它的态度:
- 从 RFC 的视角,NCR 倾向于保守重传,以有机会区分乱序和丢包,充满了对 Robustness 的憧憬及展望;
- 从 Linux Kernel 社区的视角,NCR 提供当 reordering 太高不能及时诱发 Fast Retrans 的接力,倾向激进;
换句话说,RFC 嫌现有 Fast Retans 太激进,容易错把乱序当丢包,以至于不必要降速,因此提出了 NCR 尽量不进入 Fast Retrans,而 Linux Kernel 社区则嫌现有 reordering-based Fast Retrans 太墨迹不能及时重传,以至 RTO,因此实现了 NCR 尽量进入 Fast Retrans。它们说的同一个东西,但目标不同。
显然,以 Linux Kernel 社区的视角,一旦 RACK 被引入,很多旨在优化 “不能及时重传” 的策略都没了继续存在的必要,NCR 自然下课了:tcp: remove RFC4653 NCR。
再看 RFC 的视角,下面的引述道出了我一直想表达的意思:
The requirement imposed by TCP for almost in-order packet delivery places a constraint on the design of future technology. Novel routing algorithms, network components, link-layer retransmission mechanisms, and applications could all be looked at with a fresh perspective if TCP were to be more robust to segment reordering.
如果 TCP 再健壮一点,能识别乱序,网络设备便无需对它做按序假设了,没有保序约束,并行处理便大行其道,这可以极大提高交换吞吐。
为了尽力实现这种健壮性,或者至少提供一个实验性的探讨,NCR 在确切进入 Fast Retransmit 之前给算法一定的时间来从重复应答中甄别出乱序。算法很简单,cwnd 个 seg 离开网络后仍然没有得到应答,就开始进入 Fast Retrans,而不再是 “收到 3 个重复应答”(早非如此了)。
如 RFC 正文,通过使 TCP 对非拥塞事件更具 Robustness,TCP-NCR 可能为未来互联网组件的设计开辟空间,其中就包括多路径传输。
NCR 提供两种策略,它们可以归为一类,即在进入 Fast Retrans 之前,以多大的兑换比进行 seg 守恒兑换,假设该比值是 p,积累 S 个 SACKed 段进入 Fast Retrans,简单推导一下实现:
S≥α⋅(W+p⋅W)\text{S}\geq \alpha\cdot(\text{W}+p\cdot\text{W})S≥α⋅(W+p⋅W)
解得 α=1p+1\alpha=\dfrac{1}{p+1}α=p+11。因此,如 RFC4653 所述的建议,对于 Careful Limited Transmit,兑换比为 1:2,则 α = 2/3,而对于 Aggressive Limited Transmit,兑换比为 1:1,则 α = 1/2。除此之外,你可以给定任意兑换比,获得任意程度的 “关于 dupack 原因的最佳决策和响应性之间进行权衡”,当然,我觉得这个跟 PRR 有些许重合,只不过 PRR 是决定降速之后的策略,而 NCR 是降速之前的坚持窗口。
浙江温州皮鞋湿,下雨进水不会胖。