DGP:一种新的数据包控制方法
作者:Randall Stewart
本系列的上一篇文章集中讨论了支持 TCP 堆栈的 FreeBSD 基础设施中的 pacing。本篇文章继续探讨 FreeBSD 中的 pacing,重点介绍目前在 FreeBSD 开发版本中的 RACK 堆栈中可用的 pacing 方法。这种 pacing 方法被称为动态有效吞吐量 pacing(Dynamic Goodput Pacing,DGP),它代表了一种新的 pacing 形式,可以提供良好的性能,同时仍然保证网络的公平性。为了理解 DGP,我们首先需要讨论拥塞控制,因为 DGP 是通过结合两种传统上没有一起使用的拥塞控制形式来工作的。因此,本篇文章将首先讨论拥塞控制是什么,以及 DGP 结合的两种拥塞控制方式,这两种方式共同构成一个无缝的 pacing 机制。
拥塞控制
当 TCP 最初被引入到新兴的互联网时,它并没有包含所谓的拥塞控制。它有流量控制,即确保发送方不会超出接收方的处理能力,但完全没有考虑 TCP 对网络的影响。这导致了一系列被称为“拥塞崩溃”的故障,并促使对 TCP 进行修改,加入了一个“网络感知”组件,旨在确保 TCP 的操作不会对互联网造成问题。这个“网络感知”组件被称为拥塞控制。
基于丢包的拥塞控制
最早引入互联网的拥塞控制是基于丢包的拥塞控制。今天,它是最广泛部署的拥塞控制形式之一,尽管它也有一些缺点。基于丢包的拥塞控制主要有两种算法(虽然也有其他算法),一种叫做 New Reno[1],另一种叫做 Cubic[2]。New Reno 和 Cubic 都共享一个基本的设计理念——加法增大和乘法减小(Additive Increase and Multiplicative Decrease,AIMD)。我们将更详细地看看 New Reno,因为它更容易理解。
TCP 会首先设置一些基本变量,默认值如下:
拥塞窗口(cwnd) — 允许向网络发送的最大数据量,而不会造成拥塞。初始化为初始窗口的值。
慢启动阈值(ssthresh) — 当 cwnd 达到该阈值时,增加机制将从“慢启动”切换到“拥塞避免”。ssthresh 初始设置为无穷大,但在检测到丢包时会设置为当前 cwnd 的一半。
飞行大小(FS) — 向对端发送的尚未确认的字节数。显然,它从零开始,并在每次发送数据时增加,在每次数据累计确认时减去。
初始窗口(IW) — 设置到 cwnd 的初始值,通常为 10 个数据段(10 x 1460),但可以更多或更少(最初 TCP 将其设置为 1 个数据段)。
算法 - “慢启动”(SS) — 慢启动算法是加法增大部分使用的算法之一。在慢启动中,每当收到确认时,cwnd 会增加已确认数据的数量。
算法 - “拥塞避免”(CA) — 拥塞避免算法会在每次确认完一个完整的拥塞窗口的数据后,将 cwnd 增加 1 个数据包。
因此,最初 cwnd 设置为 IW,增加算法设置为 SS,飞行大小设置为 0 字节。假设有无限量的数据要发送,实施过程会向对端发送 IW 大小的数据,同时将 FS 移动到 IW 大小。对端会对每个数据包发送确认。(某些实现,如 macOS,可能会每第八个数据包或每个数据包进行确认。)这意味着每收到一个确认,FS 会减少两个数据包,cwnd 会增加两个数据包,这意味着我们可以再发送四个数据包(如果在确认到达之前的飞行大小已经达到最大值,即 cwnd)。这个序列将持续进行,直到检测到丢包。
我们可以通过两种方式检测丢包:一种是通过返回确认中的丢包指示(即累计确认点没有前进),另一种是通过超时。在前一种情况下,我们将 cwnd 减半,并将这个新值存储在 ssthresh 变量中。如果是后一种情况,我们将 cwnd 设置为 1 个数据包,并再次将 ssthresh 设置为丢包前旧 cwnd 值的一半。
在任意一种情况下,我们开始重传丢失的数据,一旦所有丢失的数据都被恢复,我们就开始以新的较低的 cwnd 值发送新数据,并更新 ssthresh。注意,每当 cwnd 超过 ssthresh 点时,我们会将用于增加 cwnd 的算法更改为拥塞避免。这意味着一旦整个 cwnd 的数据被确认,我们将 cwnd 增加 1 个数据包。
在简要总结基于丢包的拥塞控制是如何工作的过程中,我跳过了一些更细节的部分(例如,如何识别丢包)和其他一些细微之处。但我希望给你一个概念上的了解,这样我们就可以将注意力转向互联网中的路由器,专注于这些基于丢包的机制引起的后果。
路由器通常与它们的链路关联有缓冲区。这样,如果一阵数据包到达(这种情况很常见),它们就不需要丢弃任何数据包,而是可以将数据包通过连接到下一个跳点的链路转发出去,尤其是当入站和出站链路速度不匹配时。让我们看一下下面的图 1。
图 1:一个具有缓冲区的瓶颈路由器
在这里,我们看到 P3 以 100Mbps 到达,它将被放入路由器缓冲区的第三个插槽。P1 当前正在传输到 10Mbps 的链路上,P2 正在等待传输。假设每个数据包大小为 1500 字节,P3 需要大约 120 微秒才能通过 100Mbps 的网络进行传输。当它轮到传送到 10Mbps 的目标网络时,它将需要 1200 微秒(即 120 微秒的 10 倍)。这意味着,路由器缓冲区中的每个数据包都会给刚到达的包增加 1200 微秒的额外延迟。
现在让我们回过头来考虑一下我们的拥塞控制算法将要优化的目标。它会尽可能快地发送数据包,直到发生丢包。如果我们是网络上唯一的发送者,这意味着我们必须完全填满路由器的缓冲区,直到发生丢包。这就意味着我们在优化路由器,使其缓冲区始终保持满。当内存便宜时,路由器的缓冲区变得相当大。这就意味着,当通过基于丢包的拥塞控制算法进行大规模传输时,会出现长时间的延迟。在图 1 中,我们只看到 6 个数据包的插槽,但在实际的路由器中,路由器的缓冲区可能会有数百个甚至数千个数据包在等待发送。这就意味着,TCP 连接看到的往返时间可能会从几毫秒(当没有数据包在队列中时)变为几秒钟,原因是路由器的缓冲和 TCP 的 AIMD 拥塞控制算法总是试图保持缓冲区满。
你可能听说过“缓冲膨胀”(buffer bloat)这一术语,它会影响任何实时应用(如视频通话、语音通话或游戏),这正是基于丢包的拥塞控制所导致的问题。
基于延迟的拥塞控制
很长一段时间以来,研究人员和开发者都知道基于丢包的拥塞控制会导致缓冲区的填满。在关于缓冲区膨胀的讨论之前,已经提出了替代性的拥塞控制方法来解决这个问题。其中一个最早的提案就是 TCP Vegas[3]。TCP Vegas 的基本思想是,堆栈会跟踪它所见过的最小 RTT,称为“基础 RTT”(Base RTT)。它在拥塞避免阶段使用这个信息来确定预期带宽,即:
预期带宽 = cwnd / BaseRTT
实际带宽也被计算出来,即:
实际带宽 = cwnd / 当前 RTT
然后进行简单的减法来计算差值,即:
差值 = 预期带宽 - 实际带宽
差值 Diff 然后用来决定是否应根据两个阈值 α < ꞵ 来增加或减少 cwnd。这些阈值帮助定义瓶颈处的缓冲区应有多少数据。如果差值小于 α,则增加 cwnd;如果差值大于 ꞵ,则减少 cwnd。每当差值在这两个阈值之间时,就不会对 cwnd 进行任何改变。这个聪明的公式,使用低值(通常是 1 和 3),使瓶颈处的缓冲区保持非常小,优化连接,使缓冲区始终仅足够满,以达到连接的最佳吞吐量。
在慢启动(Slow Start)阶段,TCP Vegas 修改了增长方式,通过每隔一个 RTT 来交替进行。第一个 RTT,慢启动按 New Reno 或其他基于丢包的拥塞控制机制的方式增加。然而,在下一个 RTT,TCP Vegas 不增加 cwnd,而是使用 cwnd 测量差值,再次计算路由器的缓冲区是否已饱和。当实际速率低于预期速率,且低于一个路由器缓冲区时,慢启动就会退出。
混合两者的危险
使用 TCP Vegas 进行测试表明,RTT 和吞吐量都有所改善。那么为什么我们没有完全部署 TCP Vegas,从而获得所有的好处呢?
答案就在于,当基于丢包的拥塞控制流量与基于延迟的流量竞争时会发生什么。假设你的 TCP Vegas 连接忠实地调整连接,以保持瓶颈路由器缓冲区中只有 1 或 2 个数据包。RTT 低,吞吐量达到了最大份额。然后,一个基于丢包的流开始了,它当然会填满路由器的缓冲区,直到发生丢包,这是它唯一学会减速的方式。对于 TCP Vegas 流来说,它不断接收到一个信号,表示它的速度过快,导致它继续减少 cwnd,直到几乎没有吞吐量。与此同时,基于丢包的流占据了所有带宽。基本上,当这两种类型的拥塞控制混合时,基于延迟的机制总是会出现不良结果。由于基于丢包的拥塞控制在互联网中广泛部署,这就提供了一个巨大的阻力,阻止了基于延迟的拥塞控制的部署。
在 DGP 中混合丢包和延迟基方法
DGP 尝试在选择其流量控制速率时整合基于丢包和基于延迟的方法。对于基于延迟的部分,选择了 Timely[4](并进行了适应性修改以适应互联网),尽管可以辩称,任何基于延迟的方法(包括 TCP Vegas)都可以为此目的进行适配。Timely 使用延迟梯度来计算一个乘数,该乘数与当前基于丢包的拥塞控制计算(无论是 New Reno 还是 Cubic)结合,从而得出总体流量控制速率,使用以下公式:
我们将在接下来的子节中讨论上述公式的每个部分,以便让你了解 DGP 的工作原理。有关 Timely 的详细信息,我们建议阅读相关论文[4]。
良好吞吐量(GPest)
DGP 跟踪的基础测量之一是良好吞吐量(goodput)。这与 BBR 的[5]传输速率相似,但在细微之处有所不同。传输速率计算的是到达 TCP 接收端的所有数据的到达速率。当没有丢包时,传输速率和 DGP 的良好吞吐量是相同的。但在丢包的情况下,DGP 的速率会减小。这是因为良好吞吐量是严格根据累积确认(cum-ack)的进展来衡量的,当丢包发生时,cum-ack 停止前进。恢复丢失数据包所需的时间会折合进良好吞吐量的估算中,从而降低 GPest 的值。
为了初步测量良好吞吐量,允许 IW 以突发方式发送,这开始了第一个测量窗口。良好吞吐量通常在 1 到 2 个往返时间的数据内进行测量,并根据该期间 cum-ack 的进展来计算。在测量期间,还会计算一个独立的 RTT,即 curGpRTT(稍后将作为输入传递给 Timely)。一旦 IW 被确认,我们就有了第一个测量的种子。接下来的三个测量会对估算值进行平均。进行第四次测量后,未来的估算将使用加权移动平均来更新当前的 GPest。每次开始新的 GPest 测量时,都会将 curGpRTT 保存到 prevGpRTT 中,并且也会开始新的加权移动平均 RTT,这将成为新的 curGpRTT(请注意,这个 RTT 是与 TCP 继续进行的平滑往返时间测量不同的独立测量)。当数据在传输过程中时,发送方会不断测量 GPest。如果发送方变为应用程序限制,当前测量会结束。请注意,若实现变为拥塞窗口限制,当前测量不会停止。这个描述比较简略,可能需要未来的文章详细阐述 RACK 堆栈如何测量良好吞吐量。
长期带宽 (LTbw)
DGP 还跟踪另一个带宽测量,称为 LTbw。LTbw 是所有被累计确认的字节总和,除以数据在网络中等待的总时间。这个值通常会比当前的良好吞吐量(goodput)值小,但在带宽测量急剧下降的情况下,它可以为当前带宽估算提供稳定性。
延迟梯度与 Timely(TimelyMultiplier)
Timely 提供了一个乘数,一般在估算带宽的 50% 到 130% 之间。Timely 使用以下公式(来自论文):
Timely 是为数据中心环境设计的,其中不同点的 RTT 和带宽是已知的。在 DGP 中使用时,情况并非如此,因此我们将上面公式中的 new_rtt
和 prev_rtt
分别替换为 curGpRTT
和 prevGpRTT
。我们只在完成良好吞吐量估算后进行 Timely 计算。计算出的乘数将保持连接不变,直到下一个良好吞吐量估算完成,并且乘数会在良好吞吐量更新时再次更新。请注意,Timely 使用了一个 minRTT
,即最小预期 RTT。与数据中心中的已知 RTT 不同,互联网上的 RTT 不是已知的,因此它是通过在过去 10 秒内观察到的最低 RTT 推导出来的,类似于 BBR。此外,就像 BBR 一样,为了定期重新建立最小 RTT,DGP 会进入“probeRTT”模式,将 cwnd 降至 4 个分段,以便找到一个“新的”低 RTT。请注意,添加类似 BBR 的 probe-RTT 阶段也有助于 DGP 更好地与它所竞争的 BBR 流兼容。
通过这些调整,Timely 算法被适配进 DGP。有关 probeRTT 或 Timely 的详细信息,建议阅读相关论文。
基于丢包的节流或填充拥塞窗口(FillCwBw)
为了进行基于丢包的拥塞控制节流,存在一种简单的方法。取当前的 RTT(在任何执行 Recent Acknowledgement[6] 的堆栈中保存)并将其除以拥塞窗口。这告诉节流机制应该以什么速率发送数据,以便将当前的拥塞窗口分布在当前的 RTT 上。任何基于丢包的拥塞控制方法,如 New Reno 或 Cubic,都可以与这种方法结合使用,简单地推导出一个由拥塞控制算法决定的节流速率。我们将此速率称为“填充拥塞窗口速率”(Fill Congestion Window rate,FillCWBw),因为它旨在在一个 RTT 内填满拥塞窗口。
需要注意的是,通过在整个拥塞窗口上进行节流,发送方很可能会减少丢包。这是因为通过在每次数据包微爆发之间留出时间,可以减轻瓶颈的压力,允许瓶颈在下一个微爆发到达之前排空一些数据。较少的丢包意味着拥塞窗口会增加,因为丢包是导致 cwnd 减少的唯一原因。
支点:填充拥塞窗口上限 (FCC)
到目前为止,DGP 基于良好吞吐量估算结合 Timely 算法,增加或减少速率,基于 RTT 梯度(即基于延迟的组件)。我们还根据拥塞窗口的值(通过使用任何正在使用的拥塞控制算法)和当前 RTT(基于丢包的组件)计算了节流带宽。这给了我们两个不同的带宽值,可以用来进行节流。
这就是填充拥塞窗口上限(FCC)发挥作用的地方。如果设置了 FCC(可以设置为零以始终获得最快的带宽),它将成为我们允许基于丢包的速率应用的限制。FreeBSD 中的当前默认值设置为 30Mbps。因此,例如,如果计算出的 FillCwBw 为 50Mbps,而 Bw(考虑了 Timely 乘数后估算带宽)为 20Mbps,那么我们将在 FCC 限制下进行节流,即在默认设置下为 30Mbps。如果 Timely 计算的 Bw 为 80Mbps,那么我们将以 80Mbps 进行节流。
在这里,FCC 充当了支点,并限制了名义上基于丢包的拥塞控制算法对节流速率的影响。FCC 声明,如果可能的话,你的连接将与其他基于丢包的流量竞争,以保持至少 FCC 的速率,基于拥塞控制值。如果两个值都未达到 FCC 限制,那么较大的一个值将占主导地位。
在互联网上测试时的总体性能
在我之前公司的一次大规模实验中,DGP 在设置 FCC 限制为 30Mbps(现在为默认设置)时,将 RTT 减少了最多 100 毫秒以上,并且没有导致质量体验(QoE)指标的实际下降。如果将 FCC 设置提高到 50Mbps,QoE 指标得到改善,例如播放延迟和缓冲情况有所改善,同时 RTT 的减少几乎可以忽略不计。由于 RTT 减少表明路由器的缓冲行为得到了很好的改善,因此在这次测试后,30Mbps 设置被作为默认设置。
在 FreeBSD 中启用 DGP
在已加载 RACK 堆栈并设置为默认堆栈的 FreeBSD 系统上,至少有两种方法可以启用 DGP。如果应用程序的源代码可用,可以将以下代码添加到源代码中,以将套接字选项 TCP_RACK_PROFILE
设置为值 1
:
上面的代码片段将在与 sd
相关的套接字上启用 DGP。
如果无法访问源代码,另一种机制是使用 sysctl
将所有使用 RACK 堆栈的 TCP 连接的默认配置文件设置为值 ‘1’。可以按如下方式操作:
请注意,一旦设置此值,所有使用 RACK 堆栈的 TCP 连接将使用 DGP,默认的 FCC 值为 30Mbps。你还可以使用 sysctl
更改此默认值(FCC),以更好地匹配你的网络条件。sysctl
变量 net.inet.tcp.rack.pacing.fillcw_cap
保存 FCC 值,以字节每秒为单位。例如,如果我想将值设置为 50Mbps,可以使用以下命令:
默认值为 3750000,即 30Mbps,你可以将你想设置的值(以比特每秒为单位)除以 8。所以,50,000,000 / 8 = 6,250,000。
如果你有源代码的访问权限,还可以使用套接字选项 TCP_FILLCW_RATE_CAP
,如下所示:
请注意,这只会更改指定连接的 FCC 值,而不会影响整个系统。
你还可以通过将 FCC 值设置为 0
来关闭 FCC 功能,并始终以 Timely 或拥塞控制允许的最大速度进行分配。这可能会提供最佳性能,但不会减少路由器缓冲区的使用,因此不会减少缓冲膨胀。
如何设置参数?
那么,哪些设置适合你的网络?在大多数情况下,瓶颈出现在你的家庭网关,因此了解你的互联网连接带宽可以为你提供一个合理的 FCC 值设置。例如,我管理的两个站点,一个是对称的 1Gbps 连接,我为该机器设置的 FCC 值保持默认的 30Mbps。当然,这只影响使用 RACK 堆栈的出站 TCP 连接,其中服务器正在发送数据。保持默认设置意味着大部分延迟基性能将从我的服务器中出现,每个连接只会推送以维持 3% 的网络上传带宽,并使用基于丢包的机制。
我的第二个系统是一个非对称的有线调制解调器,上传速率只有 40Mbps。在这种情况下,我将 FCC 设置为 5Mbps。如果有超过 7 个连接,它们将开始相互推挤,使用基于丢包的机制,所有连接都会尽量获得至少 5Mbps 的带宽。
后续工作
目前,FCC 值在整个系统中是静态设置的。这意味着该值通常不是最优的,可能存在可以选择的更好的值(可能会同时提高性能和降低 RTT)。作者目前正在研究一种更动态的机制来设置 FCC 值。基本想法是连接在一段时间内测量实际路径容量。然后,一旦获得“路径容量测量”(PCM)值,系统将该值的一定百分比作为 FCC 值。理论上,这将使 DGP 在调整网络路径时更加动态,同时保留并争取为每种网络类型分配一定的带宽。希望这项工作将在 2025 年完成。待完成,RACK 堆栈将更改其默认设置以启用 DGP。
参考文献
S. Floyd, T. Henderson: “The NewReno Modification to TCP’s Fast Recovery Algorithm”, RFC 6582, April 1999.
S. Ha, I. Rhee, L. Xu: “Cubic: A New TCP-Friendly High-Speed TCP Variant”, in: ACM SIGOPS Operating Systems Review, Volume 42, Issue 5, July 2008.
L. Brakmo, L. Peterson: “TCP Vegas: End to End Congestion Avoidance on a Global Internet”, in: IEEE Journal on Selected Areas in Communications, Volume 13, No. 8, October 1995.
R. Mittal, V. Lam, N. Dukkipati, E. Blem, H. Wassel, M. Gohabdi, A. Vahdat, Y. Wang, D. Wetherall, D. Zats: “TIMELY: RTT-based Congestion Control for the Datacenter”, in: ACM SIGCOMM Computer Communication Review, Volume 45, Issue 4, August 2015.
N. Cardwell, Y. Cheng, C. Gunn, S. Yeganeh, V. Jacobson: “BBR: Congestion-Based Congestion Control”, in: Queue, Volume 14, Issue 5, December 2016.
Y. Cheng, N. Cardwell, N. Dukkipati, P. Jah: “The RACK-TLP Loss Detection Algorithm for TCP”, RFC 8985, February 2021.
RANDALL STEWART (rrs@freebsd.org) 已从事操作系统开发 40 余年,自 2006 年以来一直是 FreeBSD 开发者。他专注于传输协议,包括 TCP 和 SCTP,但也曾涉足操作系统的其他领域。目前他是独立顾问。
最后更新于
这有帮助吗?