> For the complete documentation index, see [llms.txt](https://freebsd-journal-cn.bsdcn.org/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://freebsd-journal-cn.bsdcn.org/20180910-wang-luo/tcp-stack-validation-at-netflix.md).

# Netflix 的 TCP 栈验证

作者：**Jonathan Looney**

从我开始编写 TCP 代码起，对 TCP 栈的更改一直伴随着大量恐惧。这来自多个来源，但根植于在部署之前彻底测试 TCP 更改的困难。这是 Netflix 在投入大量时间和精力优化其 Open Connect Appliances（OCA）上使用的 FreeBSD TCP 栈时面临的挑战，这些服务器向 Netflix 流媒体客户端交付视频流量。在本文中，我们将描述 Netflix 如何从 TCP 开发中消除恐惧，并获得了自信地测试其更改的能力。

## 挑战

传统上对更改 TCP 栈有恐惧的原因有很多。

一个挑战是代码复杂性。TCP 栈相当成熟。这种成熟带来了价值，因为 TCP 栈已经证明自己在很长一段时间内是可靠的（并包括大量增强可靠性的错误修复）。但成熟也带来了复杂性：有尚未清理的遗留代码，多年来添加的特殊情况，以及可能有些晦涩的各种分支。这种复杂性意味着很容易无意中破坏某些东西。

使任务更具挑战性的是，一旦你编写了代码，就很难验证事情是否真正“更好”或“更差”。（事实上，某些东西可能在某方面“更好”，而在另一方面“更差”。）

例如，假设你试图修复一个问题：TCP 栈会不必要地重传 TCP 段。你可能会修复该 bug（减少不必要的重传），但不小心降低了 goodput（到客户端的有效数据吞吐率），因为你不再那么积极地重传。然而，由于互联网上存在的客户端行为和网络条件的巨大差异，在实验室中测试合理比例的可能情况都很困难。

也很容易在不知不觉中引入真正灾难性的 bug。例如，TCP 开发者做出看似无害的更改，后来发现（通常是一段时间后）某个定时器在某种边界情况下不再触发，某些 TCP 连接变得卡住，这种情况并非前所未闻。同样，我们见过 TCP 栈在应该发送数据时不发送数据的情况。

而且，使这一切更加复杂的是，错误很难修复。毕竟，如果你破坏了 TCP 连接，你可能甚至无法将修复后的内核复制到设备上。

由于所有这些原因，彻底测试 TCP 更改既重要又非常困难。

## 工具

在 Netflix，我们使用各种工具帮助我们验证 TCP 更改。在开发和开发后立即，我们可能使用各种单元测试或 Packet Drill 测试来验证我们看到的是正确的行为。然而，一旦我们进入实验室测试或现场测试，我们基本上使用三个工具帮助我们验证 TCP 更改：客户端报告和服务器报告的指标、模块化 TCP 栈和黑盒记录器。

### 指标

一旦新的 TCP 栈达到候选部署的程度，我们就开始让客户端使用它。我们的客户端和服务器都收集有关其 TCP 会话的详细统计信息。这包括 goodput、RTT 和重传次数等信息。客户端还告诉我们它们多快获得第一个数据块、数据流有多稳定，以及它们在播放期间是否遇到数据流中断。（当然，如果性能远低于客户端预期，它们会尝试将流量切换到测试之外的 OCA。这避免了对客户端流量造成过多负面影响。）

能够看到连接两端的指标对理解代码运行方式大有帮助。此外，Netflix 拥有强大的数据分析管道，允许我们检索比较使用不同 TCP 栈的 TCP 连接的详细报告，并确定差异是否具有统计显著性。通过查看这些数据，我们能够检测两个 TCP 栈之间相当小的差异。

然而，这仍然留下了一个明显的需求：你必须能够以可靠的方式比较两个 TCP 栈。为此，我们使用模块化 TCP 栈。

### 模块化 TCP 栈

Netflix 使用模块化 TCP 栈功能在单台服务器上运行多个 TCP 栈。这有许多好处。首先，我们可以为不同的应用程序选择使用不同的 TCP 栈。其次，我们可以并排彻底测试新的 TCP 栈与较旧的 TCP 栈。最后，我们（至少在理论上）可以部署 TCP 栈的修复版本，而无需升级底层操作系统。（我们目前不经常使用动态 TCP 栈更改，但我们正在测试启用该功能的基础设施。）

要理解模块化 TCP 栈如何使 Netflix 受益，可能有助于解释我们现在在 Netflix 进行 TCP 开发的方式。

我们在 TCP 代码的开发副本上进行开发。当我们发布新的候选 TCP 栈时，我们将该代码复制到其自己的目录中，并给它一个包含在名称中的版本号。复制后，我们尝试只更新该代码以处理必要的 API 更改。FreeBSD 中的模块化栈基础设施允许我们编译相同的代码，但使用不同的栈名。它混淆符号以防止同一代码不同副本之间的符号冲突，并让我们以不同的栈名安装不同版本。这让我们确实可以将开发代码复制到新的 TCP 模块目录中，更改一两个 Makefile 变量，立即拥有一个新版本的 TCP 栈。

（作为类比，你可以将这些视为类似于 FreeBSD 发布。我们在 stable/11 上开发，然后复制到 releng/11.0 分支。但我们会继续在 stable/11 上开发，最终将其复制到 releng/11.1 分支。在 Netflix，我们以类似的方式处理我们的 TCP 栈。）

这种编译未修改代码但使用新 TCP 栈名的能力是一个微妙但重要的功能。这让我们直接跟踪版本之间的代码更改，而不会被纯粹为了支持模块重命名而存在的无关非功能更改分散注意力。

当 Netflix 有候选 TCP 栈时，我们将在同一 OCA 上并排部署旧的和新的 TCP 栈。然后，我们将让一些客户端在该 OCA 上使用这些栈中的一个或另一个，并报告它们的指标。然后我们可以收集统计数据并比较两个栈的性能。

对于测试，在同一 OCA 上并排运行旧栈和新栈很重要。这消除了许多可能影响结果的变量。底层硬件、操作系统和操作环境应该以完全相同的方式影响两个 TCP 栈。通过在同一 OCA 上并排运行两个栈，我们能够专注于 TCP 栈本身性能造成的差异。

模块化 TCP 栈功能还提供了向新 TCP 栈部署的平滑过渡。一旦我们在小规模测试中验证了新的 TCP 栈，我们就会用规模逐渐扩大的测试来验证它。在我们的最终测试中，我们可能让新的 TCP 栈处理全球所有 Netflix 客户端流量的 50%，并验证该栈是否仍按预期执行。如果是，我们可以将所有客户端切换为默认使用新栈。（而且，由于可以在运行时选择栈，它甚至不需要重启 OCA。）

使用模块化 TCP 栈功能，我们还可以为每个应用程序和客户端选择最佳的 TCP 栈。例如，我们可能发现新的 TCP 栈对一部分客户端的性能明显更差。我们可以在进一步调查时将这些客户端设置为使用旧 TCP 栈，同时让其余客户端受益于新 TCP 栈的改进。同样，我们可以在我们的 OCA 上运行的每个应用程序中单独进行此测试，并按各自的计划将这些应用程序更改为使用新 TCP 栈。这让我们确保使用 OCA 上的每个应用程序时，我们使用的 TCP 栈经过验证，最能满足客户端需求。

模块化 TCP 栈功能还为我们提供了对 TCP 栈 bug 的保护。我们偶尔在新的 TCP 栈中发现非常严重的 bug。我们能够动态卸载这些模块而不中断我们的服务。OCA 和我们的客户端继续使用安装在 OCA 上的旧 TCP 栈运行。一旦我们修复了 bug，我们就可以部署新的 TCP 模块，加载它，并开始测试它。同样，这不需要重启，相当无缝。目前，我们只在开发中定期使用此功能，但我们正在测试我们编写的工具，使我们能够在我们的 OCA 网络中自动化动态 TCP 模块部署。

通过这些功能的组合，我们可以更有信心地部署新的 TCP 栈。我们能够轻松创建命名“候选发布”版本的 TCP 栈，在 OCA 上部署它们，与现有栈并排测试它们，如有必要，从严重的 TCP 栈 bug 中恢复并迭代修复 TCP 栈中 bug 的新版本。这与早先描述的恐惧文化截然不同。这是一个自信开发 TCP 栈的健康环境，它得益于构建、部署和使用模块化 TCP 栈的能力。

但到目前为止的测试主要集中在检查客户端和服务器维护的指标上。我们的测试还有一个值得一提的方面，那就是由 TCP 黑盒记录器提供的。

### TCP 黑盒记录器

TCP 黑盒记录器提供来自 TCP 连接的事件流。它以商业客机上携带的飞行数据记录器（俗称“黑匣子”）命名。最初构想时，黑盒记录器会将在 TCP 连接上发生的事件流记录到与该连接关联的环形缓冲区。如果用户空间应用程序或内核注意到连接出了“问题”，它可以转储环形缓冲区的内容供以后分析。（在内核 panic 的情况下，开发者可以从环形缓冲区中提取数据，查看导致崩溃的事件序列。）

每个事件都包含 TCP 连接内部状态的记录，以便开发者可以跟踪内部状态如何在事件之间变化。通过跟踪内部事件和 TCP 连接的内部状态，开发者可以很好地了解连接为何如此运行。事实上，这些内部信息可以产生有价值的洞察，而这些洞察对于只跟踪发送和接收内容的分析工具是隐藏的。

该功能非常有用，我们已经在 Netflix 使用它来调试问题。然而，黑盒记录器还有另一种操作模式，可用于发现问题。黑盒记录器允许我们为“持续”日志记录选择一定百分比的 TCP 连接，它试图将 TCP 连接的所有事件导出到用户空间供以后分析。这些数据可用于准确了解 TCP 连接上发生了什么——即使是我们认为看起来“正常”的连接。

在开发期间，我们可能会手动扫描这些记录的样本，以确保行为符合我们的期望。我们将特别关注我们认为应该因代码增强而改变的领域，以及我们担心可能被代码更改意外影响的领域。然而，通过手动审查我们能检测到的有限。

我们还有一个自动化工具扫描黑盒记录，验证每个会话是否符合一些基本假设。例如，我们检查当没有理由暂停（有窗口空间可用且有数据要发送）时，我们至少每一两秒发送一次数据。这让我们验证定时器是否按预期工作。我们还检查我们没有超过拥塞窗口和对端的接收窗口。这让我们验证我们没有在不应该发送数据时发送。这些基本的健全性检查很有价值，能够发现逃离实验室测试、但使用真实客户端更广泛地测试时才显现的边界情况 bug。

当此系统发现错误时，它会为 TCP 开发团队发布警报。他们可以检索跟踪并尝试确定错误发生的原因。并且，由于与事件一起记录的状态信息，通常很容易隔离问题（或至少是代码的问题区域）。

## 一个例子

下面给出一个虚构的例子，它综合了我们使用此基础设施和方法论的多种经验，可能有助于说明其工作方式。

对于这个例子，假设我们当前使用的 TCP 栈版本是 rack\_11。开发版本是 rack\_12，但自 rack\_11 创建以来没有更改。（换句话说，rack\_11 和 rack\_12 使用相同的代码。）我们想修复 rack\_11 中不必要的高重传的 bug。

我们对开发版本进行代码更改并测试它。我们认为它修复了问题。我们现在将 rack\_11 和 rack\_12 部署到 OCA 上，用少量客户端运行测试。指标看起来不错，所以我们扩大测试以覆盖更多 OCA 和更多客户端。此时，我们注意到重传下降（这是预期的，是“好的”）。但我们也注意到 goodput 下降了（这不是预期的，是“坏的”）。我们收集黑盒跟踪并发现我们在应该使用 `<` 比较的地方意外使用了 `<=` 比较。这导致我们在某些应该重传的情况下没有重传。

我们在开发版本中修复此 bug 并再次测试。我们认为它修复了新问题（同时仍然修复了原来的问题）。我们再次将 rack\_11 和 rack\_12 部署到 OCA 上，用少量客户端运行测试。指标看起来不错，所以我们扩大测试以覆盖更多 OCA 和更多客户端。指标看起来仍然不错，所以我们提交代码。

最终，rack\_12 成为候选发布版本，rack\_13 成为新的开发版本。此时，我们将 rack\_12 部署到更大的一组 OCA。指标看起来仍然不错。然而，此时，黑盒分析程序提醒我们，在少量情况下我们未能发送数据。我们意识到我们在特定的边界情况下意外未能重置重传定时器。

我们在 rack\_13（新的开发版本）中修复此新 bug 并再次测试。我们将 rack\_11 和 rack\_13 部署到 OCA 上，用少量客户端运行测试。然后我们请求我们的发布工程师将我们的提交合并到 rack\_12。为了我们的例子，我们说他同意了，所以 rack\_12 现在有了新的 bug 修复。我们将其部署到更大的 OCA 集合并继续我们的发布测试。指标看起来仍然不错，黑盒分析程序没有更多警报。

现在，我们将 rack\_12 部署到 Netflix 网络中的所有 OCA。我们用小百分比的客户端进行测试。指标看起来不错，所以我们继续对所有 Netflix 客户端的 50% 进行测试。此时，我们注意到一个客户端操作系统的指标很差，而所有其他的相同或更好。我们决定将所有 Netflix 客户端指向使用新的 TCP 栈，但我们为指标较差的一个操作系统做出例外，告诉使用该操作系统的客户端使用旧 TCP 栈。

然后我们开始为该客户端类型收集重点测试和调试数据，以努力理解为什么它在新 TCP 栈代码下比旧代码表现更差。希望我们能够在 rack\_13 中修复 bug。

此时，我们再次开始测试过程。

实际上，这个过程有时比我们希望的时间更长。例如，我们已经在 Netflix 网络中的所有 OCA 上部署了候选发布版本，但由于在早期更窄的测试阶段未检测到的微妙指标变化，在测试的后期阶段放弃了候选发布版本。但即使有时比我们希望的时间更长，我们对这一彻底测试过程产生的结果感到满意：能够自信地升级 TCP 栈中的代码。

## 自信地进行 TCP 开发

有了所有这些工具，我们能够以更大的信心进行 TCP 开发，确信我们在改进 TCP 栈。我们知道我们不会编写无 bug 的代码。我们知道我们不会在实验室中发现所有问题。但我们可以缓慢而仔细地部署 TCP 栈更改，并以确保我们可以在不受底层硬件或操作系统差异影响的情况下测试 TCP 栈更改的方式进行测试。而且我们可以通过查看服务器端和客户端指标以及检查 TCP 栈内部操作的跟踪来验证新的 TCP 栈是否正确运行。

这些事情的组合将 TCP 开发范式从受恐惧约束的范式改变为我们拥有很大创新自由的范式，确信我们拥有负责任地创新的正确工具。

注：本文中描述的大部分代码（包括 RACK TCP 栈）已在 FreeBSD 12 中可用。一些内容（如增强的服务器端统计和用户空间黑盒分析工具）仍在上游化过程中，但应该“很快”就会合入。

***

**JONATHAN LOONEY** 在 Netflix 管理一个负责维护 OCA 上操作系统的开发团队。他是 FreeBSD 提交者，活跃于传输协议领域。


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://freebsd-journal-cn.bsdcn.org/20180910-wang-luo/tcp-stack-validation-at-netflix.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
