作者:Richard Scheffenegger
自上次报告以来已经过去了近 3 年。
自从上次报告我关注的 FreeBSD 项目领域以来已经过去了近 3 年,具体来说,是有关 TCP 协议实现的内容。对于那些不太了解的朋友来说,FreeBSD 并非仅一种 TCP 栈,而是有多款 TCP 栈,并且主要的开发工作集中在 RACK 栈和基础栈上。目前,默认使用的是基础栈,它是一款由 BSD4.4 演变而来的,长期开发的栈。而自 2018 年起,我们推出了一款完全重构的栈(即“RACK 栈”——以 Recent ACKnowledgement 机制为名),它提供了许多在基础栈中缺失的高级功能。例如,RACK 栈提供了细粒度的流量控制能力。也就是说,RACK 栈能够精确地控制数据包的发送时机,从而平滑地消耗网络资源。相对而言,当应用程序向基础栈发送突发数据时,通常会在网络接口的接近线路速率(假设 CPU 和内部总线不是瓶颈)的情况下将数据以大块突发的方式发送出去。尤其在应用程序在 IO 操作后短暂停顿几十毫秒时,这种现象的发生情况最为常见。(关于 RACK 栈的更多细节超出了本文的范围,可以参考 Michael Tuexen 和 Randall Stewart 的附带文章。)
在本文中,我想重点介绍一些新功能,这些功能已经被引入到基础栈中——其中许多功能默认已启用,而有些功能则可能需要专门开启。所有功能都将通过详细的介绍来帮助改善网络体验。
总体而言,自 FreeBSD 13.0 发布以来,sys/netinet
目录下(传统上所有传输协议所在的地方)已经有近 1033 次提交。这为基础栈的选定变更提供了一个概述,改善了以下几个功能:
首先引入到基础栈中的功能是 PRR(比例速率降低,RFC 6937)。为了明白 PRR,我们首先需要了解 SACK 在丢包恢复中的作用。标准 SACK 丢包恢复的一个问题是,当进入丢包恢复时,虽然会调整拥塞窗口(例如,NewReno 时将拥塞窗口减少到原来的一半,Cubic 时减少到 70%),但在单个数据包丢失后,初始的估算会导致在返回第一个 ACK 时不发送任何数据包。这样,在拥塞窗口的前半部分(NewReno)或初始的 30%(Cubic)内,不会有数据包发送。只有等到这个限制被突破后,每个返回的 ACK 才会促使发送一个新的数据包,但这可能会在网络中造成拥堵,导致传输速度过快,从而导致后续的数据包丢失。初期的静默期可能帮助清理队列,但随后的传输速度过快,可能会导致网络丢包(甚至是重传包丢失——后面会详细讨论)。
为了快速调整有效的发送速率,并且在丢失多个数据包和 ACK 包时更加适当地处理,PRR 会根据每个新到达的 ACK 计算应该发送多少数据,并尽可能多地发送适当大小的包。例如,在 NewReno 中,假设拥塞窗口减少到一半,且只有一个数据包丢失,这时每返回两个 ACK,PRR 会发送一个新的数据包。这样,发送速率将瞬间调整到原来的一半,从而避免网络设备过载。如果丢失了多个数据包或 ACK 被丢弃,PRR 在收到一个 ACK 后,可能会发送多个数据包,弥补这些错过的发送机会。总体来说,这种行为确保了在丢包恢复过程中,窗口(RTT)结束时的有效拥塞窗口尽可能接近预期的拥塞窗口,并确保即使在多个数据包或 ACK 丢失的情况下,也不会错过任何发送机会。
希望通过几个图表能更清晰地解释这个细节。以下是从 Wireshark 和 tcptrace 结合 xplot 获取的时间序列图。小的蓝色垂直条表示发送特定数据序列的数据包的时间,左侧的坐标轴标示了这一点。下方绿色的水平线表示接收方接收到的连续数据。红色的垂直线表示接收到的任何不连续的数据包范围。
请注意,在一个窗口(往返时间)内只能恢复一个数据包,图中的长绿色水平线表示接收应用程序处理更多数据之前所引起的延迟。
如此例所示,SACK 极大地改善了情况,因为所有丢失的数据包一般都可以在一个往返时间(RTT)内重传。然而,请注意,在每个 ACK 后发送的暂停和恢复。这种行为导致数据的有效发送速率过高,导致一些数据包被网络丢弃。通常,这会导致一个或多个重传数据包到达得太快,网络丢弃了重传包。此时唯一的解决办法就是等待重传超时(RTO)。
图中所示的 PRR 改进是微妙的。之前,在一个窗口的前半部分没有数据发送,而后半部分则以旧的、可能过高的速率发送数据,PRR 则通过大约每接收到一个 ACK 就注入一个数据包,直到新的发送速率达到预期,然后几乎在每个后续的 ACK 到达时都发送一个数据包。这样做有助于降低重传的有效发送速率,减少它们被网络丢弃的可能性。结果是减少了 RTO(重传超时)并改善了延迟。
图中显示的内容不完全正确,但试图传达 PRR 在接收到的 ACK 上“抖动(dithering)”数据包的方式,以适当地发送它们——在这种情况下,平均每个 ACK 发送 0.7 个数据包,包括那些可能已被网络丢弃的包。
该领域的最终更新是,PRR 现在会自动切换到较不保守的模式,除非在丢包恢复过程中出现额外的丢包。这样做有效地提高了丢包恢复期间的传输速度,类似于在常规的拥塞避免阶段发生的情况。PRR 最好(自然地)与 SACK 配合使用,但即使只有非 SACK 的重复 ACK 可用时,它也能改善传输时机。即使只有 ECN(显式拥塞通知)反馈,PRR 也能提高传输时序。
近年来,基础栈对 RFC6675 中定义的 SACK 丢包恢复的实现得到了改进。但是,尽管在估算网络中仍未确认数据量的某些部分得到了改进,RFC6675 的其他方面仍然有所缺失。
在这一领域的改进现在包括使用所谓的救援重传——这是 RACK 栈中实现的尾丢包探测(Tail-Loss Probe)的前身。简而言之,当传输的最后几个数据包与之前的数据包丢失一起丢失时,栈能够检测到这个问题,并重传最后的数据包,以执行及时的丢包恢复。
并且,通过在处理任何传入的 SACK 块时增加额外的记录,协议栈可以更好地追踪特定数据包是否已离开网络,不论是因为已被接收还是极有可能被丢弃。
最终的增强功能是追踪重传包是否也可能被网络丢弃。然而,与使用时间域的 RACK 不同,基础栈观察的是序列号域。虽然这种丢失的重传检测未在 RFC 系列中规定,但它是对任何使用 TCP 栈的请求-响应协议(例如 RPC)极具价值的补充,有助于减少流完成时间/IO 服务响应时间。默认情况下尚未启用丢失重传的追踪和恢复功能。在 FreeBSD 14 中,可以通过 net.inet.tcp.do_lrd
开启该功能,而在 FreeBSD 15 中,该功能移动到了 net.inet.tcp.sack.lrd
,并会默认启用。
总体而言,这些改进使基础栈在面临 IP 网络中常见的病态问题时更加健壮。
最后,基础栈(以及 RACK 栈)在接收到错误的重复数据包时生成 DSACK(RFC2883)响应。尽管接收这样的 DSACK 信息不会改变栈的行为,但将其提供给远程发送方可能使该发送方能够更好地适应特定的网络路径行为——例如,Linux 可以因此增加重复确认阈值(dupthresh),或检测到由于路径往返时间(RTT)突增而导致的错误重传。
几十年来,基础栈积累了几种在实时系统上进行调试的机制。最不为人知的工具之一是 trpt
,它在 FreeBSD 14 中已移除。不过,仍然存在许多其他功能(如 dtrace
、siftr
、bblog
等)。
BBRlog 是在 RACK 栈中引入的,并扩展到涵盖更多的基础栈内容。当前正在准备工具,以从运行中的系统中提取内部状态变化,并从核心转储中提取它们——以及数据包追踪本身。(参见 https://github.com/Netflix/tcplog_dumper 和 https://github.com/Netflix/read_bbrlog)
如我在上一篇文章中所述, 目前几乎在所有地方使用的事实标准拥塞控制算法是 TCP Cubic。最近,Cubic 也被设置为 FreeBSD 的默认算法——无论使用何种 TCP 栈。
这里的一个可见扩展是添加了 HyStart++。当一个 TCP 会话启动时,拥塞控制机制会在所谓的慢启动阶段迅速增加带宽。传统上,慢启动阶段会在收到第一次拥塞指示(丢包或显式拥塞通知(ECN)反馈)时结束。使用 HyStart++(作为 Cubic 模块的一部分并始终启用),会监控 RTT(往返时间)。当 RTT 开始上升时——可能是由于网络队列开始形成——进入一个较为保守的阶段(保守的慢启动),并且继续监控 RTT,因为基于时间的信号通常难以可靠地获取。如果 RTT 在此保守的慢启动阶段内再次下降,则恢复常规慢启动。如果没有,则在 CSS(保守慢启动)中较为缓慢的发送节奏会限制所谓的超调——即由于不可避免的丢包需要恢复的数据量。
如上文所述,ECN 是一种避免仅通过丢包来指示拥塞事件的机制。在过去十年中,互联网工程任务组(IETF)在改进这一信号方面付出了很大努力。最初,ECN 被视为与丢包等价的信号,但后来发现一种更频繁、语义不同的信号更适合维持大范围带宽下的浅队列(快速队列)。完整的架构被命名为低延迟、低丢包、可扩展(L4S)。虽然 FreeBSD 中的并非所有组件目前都已准备好实施完整的“TCP Prague”实现,但许多独立的特性——如 DCTCP 拥塞控制模块,以及在这里相关的准确 ECN(AccECN)——已成为 FreeBSD 14 栈的一部分。
在经典 ECN 中,每个 RTT 只能信号化一次拥塞事件标记。这就需要拥塞控制模块采取强硬的管理措施。事实上,当在 RFC3168 模式下调整 TCP 带宽时,CE 标记被视为与丢包指示等价。相比之下,使用 AccECN 时,接收方可以向数据发送方回传任意数量的显式拥塞标记。这使得可以从网络中提取更细粒度的信号。这在使用 DCTCP 时变得尤为重要,DCTCP 在中间交换机上采用了修改过的、更具侵略性的标记阈值。它也是低延迟、低丢包、可扩展(L4S)架构的关键组成部分——也称为 TCP Prague。
最近,RACK 栈获得了完全处理 TCP 数据包 MD5 认证的能力。这一改进使得 RACK 栈可以与 BGP 一起使用——这是使 RACK 栈更加完备并可在各种通用场景中使用的又一进步。
长期以来,RFC7323(RFC1323)中的两个特性——窗口缩放(Window Scaling)和时间戳(Timestamp)参数——是紧密耦合的。在这方面,现在能单独启用这两个选项,而默认设置仍然允许两个选项都处于启用状态。现在,可以通过设置 net.inet.tcp.rfc1323
不仅启用(1)或禁用(0),还可以设置为 2(仅窗口缩放)或 3(仅时间戳)。此外,按照 RFC7323,现在可以通过要求在所有情况下正确使用 TCP 时间戳,进一步增强 TCP 会话的安全性。这可以用设置 net.inet.tcp.tolerate_missing_ts
为 0 来实现。
虽然 TCP 特性各方面的改进已经进入收益递减阶段,但仍有一些进一步的增强在讨论中。
例如,与之前不同,RFC2018(Selective Acknowledgments,选择性确认)现在能在重传超时(RTO)期间保留信息。原始标准的主要动机是允许接收方“reneging”。除非明确 ACK,否则后续数据可能仍然会被丢弃,例如由于内存压力。在实践中,这种 reneging 几乎从未发生,但在 SACK 丢包恢复阶段,重传超时却相当常见。保留这些信息可以在 RTO 后实现更高效的重传。挑战在于,基础栈与重传超时后应发生的其他操作(例如从非常小的拥塞窗口慢启动)有着紧密的耦合关系。此外,需要评估此更改对 RTO 后的影响——这可能需要在 dummynet 路径模拟器中增加一些功能,以便以更可控的方式模拟丢包。
Richard Scheffenegger 自 2020 年 4 月以来一直是 FreeBSD 的提交者,致力于改进 TCP 栈的特性和功能,主要关注慢路径(丢包恢复、拥塞控制处理),并积极与 IETF 合作开发如准确 ECN 等增强功能。