目录 ← 首页
CS168

拥塞控制问题

混淆数据包损坏与拥塞

TCP 通过检测数据包丢失来判断拥塞,但数据包丢失并非只有拥塞这一个原因。数据包也可能因损坏而丢失,而 TCP 无法区分丢失是由损坏还是拥塞导致的。如果数据包损坏,即使网络并未拥塞,TCP 仍会降低其发送速率。

这一点在我们将吞吐量与丢包率相关联的公式中也能体现:吞吐量与丢包率成反比,即便对于非拥塞导致的丢失也是如此。该公式有助于估算有损链路(例如频繁损坏数据包的无线链路)对 TCP 的影响。

短连接

现实中,大多数 TCP 连接的生命周期都很短。50% 的连接发送的数据少于 1.5KB,80% 的连接发送的数据少于 100KB。这些连接在存续期间发送的数据包很少(可能只有一个)。

假设某个连接中,发送方只需发送 3 个数据包。TCP 拥塞控制会如何操作呢?首先从窗口大小为 1 开始,发送第一个数据包。然后,等待确认(ack),将窗口大小增加到 2,再发送剩下的两个数据包。之后,等待另外两个确认,连接便完成了。

img

这个连接用了两个往返时间(RTT)发送 3 个数据包,导致吞吐量极低(每 RTT 1.5 个数据包)。

更普遍的情况是,这些短连接始终处于慢启动阶段,从未达到其应得的公平带宽份额。这使得短连接的传输时间不必要地延长。

短连接的另一个问题是处理丢失。回想一下,我们通过 3 个重复确认来检测丢失,但在短连接中,可能没有足够的数据包来触发这些重复确认。例如,如果要发送 4 个数据包,且第二个数据包丢失了,我们永远不会收到 3 个重复确认。相反,我们必须等待超时触发。在现实世界中,典型的超时值约为 500ms,这也会导致短连接的时间不必要地延长。

如何解决这两个问题呢?一个部分解决方案是采用更大的初始窗口(例如从 1 个数据包改为 10 个数据包)。这样,包含 10 个或更少数据包的连接可以在连接开始时就发送所有数据。

TCP 填满队列

TCP 通过丢失来检测拥塞,而拥塞控制算法会故意提高速率,直到触发丢失。为了触发丢失,队列需要被填满。这意味着 TCP 会在整个网络中引入排队延迟,而这些延迟会影响网络中的所有用户。

假设我们有一个传输 10GB 文件的重度连接,之后启动一个传输单个数据包的小型连接。这两个连接共享同一个瓶颈链路。重度连接会提高其速率,直到瓶颈链路的队列被填满。此时,当小型连接启动时,它会被困在队列中,排在重度连接的数据包后面。

如果路由器保持极大的队列,这个问题会更加严重。路由器因拥有过多内存而导致队列过长的现象被称为缓冲区膨胀。缓冲区膨胀的一个例子是家用路由器,它可能有一个巨大的队列,但只有少数连接(仅家庭内部的连接)使用该队列。这样,任何新建立的连接都会给其他连接带来较大的排队延迟。

为避免队列被填满,我们可以寻找一种不通过故意触发丢失来衡量拥塞的方法。具体来说,我们可以在往返时间(RTT)开始增加时检测到拥塞,因为 RTT 增加意味着延迟。这正是谷歌最近推出的 BBR 算法(2016 年)的设计思路。发送方会记录最小 RTT,若发现 RTT 超过最小 RTT,则降低发送速率。

img

作弊行为

没有任何机制强制发送方必须遵循 TCP 拥塞控制算法。发送方可通过作弊来不公平地占用大量带宽。

例如,发送方可以更快地增加窗口大小(例如每 RTT 增加 2,而不是 1)。如果我们将图形模型应用于一个作弊发送方和一个诚实发送方,AIMD(加性增 multiplicative 减)更新实际上会收敛到一条不公平的公平线上,此时作弊发送方获得的带宽是诚实发送方的两倍。

img

还有许多其他修改协议的方式,例如使用非常大的初始拥塞窗口。

实际上,由于 TCP 是在操作系统中实现的,发送方要作弊就必须修改操作系统中的代码,而绝大多数互联网用户不会这样做。

如果少数发送方滥用系统,这些发送方会获得更多带宽。如果大量发送方滥用系统(例如微软发布了一个滥用 TCP 的 Windows 版本),数百万 Windows 用户仍会相互竞争,最终不太可能有人获得更多带宽。

另一种无需修改 TCP 即可作弊的方式是打开多个连接。TCP 仅确保每个连接获得公平份额。如果作弊发送方打开 10 个连接,而诚实发送方打开 1 个,那么作弊发送方将获得 10 倍的带宽。许多应用程序会故意打开更多连接以提高带宽。

既然作弊是可能的,为什么互联网没有再次陷入拥塞崩溃呢?事实证明,研究人员也不确定答案。一种可能是:修改拥塞控制算法的作弊者可能会获得不公平的带宽份额,但如果他们仍然遵循拥塞控制的基本原则(例如在发生丢失时降低速率),就不会使网络不堪重负。相比之下,在 20 世纪 80 年代最初的拥塞崩溃中,发送方会以高速率不断重发数据包,完全没有调整速率的概念。

如果作弊是可能的,那么实际中作弊行为有多普遍呢?同样,我们也不确定。很难衡量作弊行为(例如,你无法知道每个发送方使用的窗口大小)。

拥塞控制与可靠性相互交织

拥塞控制机制和可靠性机制紧密耦合。如我们所见,拥塞控制是通过修改 TCP 可靠性的代码并调整几行代码实现的。

我们也能在算法本身中看到这种依赖性。窗口会根据确认和超时进行更新,因为可靠性代码就是为了响应这些事件而编写的。我们通过重复确认来检测丢失,因为可靠性实现使用了累积确认。

将可靠性和拥塞控制结合是一种设计选择。其一个好处是,拥塞控制只需对代码进行少量修改,就能在 20 世纪 80 年代拥塞崩溃问题出现时广泛部署。然而,从那以后,这两个功能的结合使得算法的演进变得复杂。例如,如果我们想修改拥塞控制算法的某些部分,可能也必须修改可靠性代码。或者,如果我们想修改可靠性实现(例如从累积确认改为全信息确认),就必须同时更新拥塞控制。

从设计角度来看,这是模块化的失败,而非分层的失败。拥塞控制和可靠性都在正确的抽象层(传输层)运行。然而,在传输层内部,我们没有将不同的功能清晰地分离到代码的不同部分。

由于拥塞控制依赖于可靠性,因此很难在没有可靠性的情况下实现拥塞控制。一些应用(例如视频流)可能不需要可靠性,但仍然需要拥塞控制。但目前无法禁用可靠性而只保留拥塞控制。

同样,在没有拥塞控制的情况下实现可靠性也很困难。例如,如果有一个轻量级连接,每 10 分钟发送一个数据包,这样的连接可能不需要拥塞控制。但我们无法轻易地只为某些连接禁用拥塞控制。

(注:文档部分内容可能由 AI 生成)