与Nagle的算法和延迟的ACK有关的TCP性能问题(2005年)

2020-11-24 13:05:58

本页描述了由于Nagle的算法与延迟的ACK之间鲜为人知的相互作用而导致的TCP性能问题。至少,我认为它并不为人所知:我在其他地方都没有看到它的记载,但是在我的苹果职业生涯中,我一再遇到它引起的性能问题(第一次是在PPCToolbox中早在1999年我就写过TCP代码),所以我认为现在是时候对其进行记录了。

总的来说,当TCP状态机需要持续不断的数据流运行时,使TCP如此出色的许多机制(例如快速重传)都将发挥最佳作用。如果发送数据块,然后停止并等待另一端的应用程序层确认,则状态机可能会失败。这有点像一台水泵失去了它的主要动力—只要整个管道都充满水,水泵就会运转良好并且水会流动,但是如果您有少量的水和一束空气,水泵就会发生故障,因为叶轮没有什么可推崇的。

如果您要通过TCP编写请求/响应应用层协议,则这意味着您需要使实现至少具有双缓冲:发送第一个请求,而您仍在等待响应,生成并发送您的第二个。然后,当您获得第一个请求的响应时,生成并发送您的第三个请求。这样,您始终会有两个未完成的请求。在等待请求n的响应时,请求n + 1在返回管道中位于请求的后面,从概念上讲会推动数据的传递。

有趣的是,从单缓冲变为两倍,三倍,四倍等等所获得的性能收益不是线性斜率。实施双缓冲通常可以带来几乎所有的性能提升;进行三倍,四倍或n路缓冲通常会产生更多的收益。这是因为重要的不是流水线中的大量数据,而是在您当前正在等待的数据包之后流水线中有东西的事实。只要您等待的响应中至少有一个数据包的数据价值,就足以避免此处所述的病态变慢。只要至少有四五个数据包的数据价值,就足以在您正在等待的数据包丢失时触发快速重传。

在本文档中,我描述了Nagle算法与“延迟的ACK”(WiFi一致性测试中使用的测试程序)之间的这种不良互动,最近遇到的情况。该程序通过在TCP上重复发送100,000字节的数据,然后等待对方的应用程序层确认以确认其接收,来测试WiFi实施的速度。 Windows达到了通过测试所需的3.5Mb / s; Mac OS X仅获得2.7Mb / s的故障。天真的(错误的)结论是Windows速度快,而Mac OS X速度慢,仅此而已。事实并非如此简单。真正的解释是测试存在缺陷,而Mac OS X恰好暴露了问题,而Windows靠运气却没有。

工程师发现,将缓冲区大小从100,000字节减少到99,912字节,使测量的速度跃升至5.2Mb / s,轻松通过了测试。在99,913字节处,测试速度为2.7Mb / s,但失败了。显然,这里发生的事情不仅比慢速的无线网卡和/或驱动程序更有趣。

下图显示了使用tcptrace和jPlot生成的失败传输的TCP数据包跟踪,仅达到2.7Mb / s:

下图显示了使用99912字节块进行传输的TCP数据包跟踪,该跟踪达到5.2Mb / s并通过:

在失败的情况下,代码显然是在0-200ms内成功发送数据,然后在200-400ms内不执行任何操作,然后在400-600ms内再次发送,然后在600-800ms内不再执行任何操作。为什么一直停下来?要了解我们需要了解Delayed ACK和Nagle的算法:

延迟的ACK表示TCP不会立即确认收到的每个TCP段。 (阅读以下内容时,请考虑交互式ssh会话,而不是批量传输。)如果接收到一个单独的TCP段,则假设接收应用程序可能会生成某种响应,则等待100-200ms。 (例如,每次sshd每次收到击键,它通常都会在响应中生成一个字符回显。)您不希望TCP协议栈每次在1ms之后发送一个空ACK和一个TCP数据包,因此您要延迟一点,因此您可以将ACK和数据包合并为一个。到现在为止还挺好。但是,如果应用程序不生成任何响应数据怎么办?那么,在那种情况下,稍稍延迟会带来什么变化?如果没有响应数据,那么客户端将无法等待,可以吗?嗯,应用程序层客户端什么都不能等待,但是最后的TCP堆栈可以等待:这是Nagle的算法输入故事的地方:

为了提高效率,您希望发送完整尺寸的TCP数据包。 Nagle的算法表示,如果您要发送的字节数少,但没有完整的数据包的价值,并且您已经有一些未确认的数据在传输中,则请等待,直到应用程序为您提供了更多的数据,足以制作另一个完整的数据。 TCP数据包或另一端会确认您的所有未清数据,因此您不再有任何数据在传输中。

通常这是一个好主意。 Nagle的算法旨在保护网络免受愚蠢的应用程序的攻击,这些愚蠢的应用程序会执行此类操作,因为一个幼稚的TCP堆栈可能最终会发送100,000个1字节数据包。

不良的交互是,现在发送端有些东西在等待服务器的响应。那就是Nagle的算法,它在发送之前等待其飞行中的数据被确认。

接下来要知道的是,延迟ACK适用于单个数据包,如果第二个数据包到达,则会立即生成ACK。因此,TCP将立即对第二个数据包进行ACK。发送两个数据包,您将立即收到ACK。发送三个数据包,您将获得覆盖前两个数据包的即时ACK,然后在第三个数据包之前暂停200毫秒。

有了这些信息,我们现在可以了解发生了什么。让我们看一下数字:

99,900字节= 68个完整的1448字节数据包,外加1436字节100,000个字节= 69个完整的1448字节数据包,外加88个字节

使用99,900字节,您发送68个全尺寸数据包。 Nagle保留最后1436个字节。然后:

延迟的ACK将一个字节与其待处理的ACK数据包结合在一起,并立即发送合并的TCP ACK +数据包,

现在考虑100,000字节的情况。您发送69个全尺寸数据包的流。 Nagle保留最后88个字节。然后:

延迟的ACK表示,接收者在收到(a)来自本地进程的某些响应数据,或(b)来自发送者的另一个数据包之前,不会对该数据包进行ACK。

本地进程不会生成任何响应数据(a),因为它还没有完整的100,000字节。

发送者将不会发送最后一个数据包(b),因为Nagle直到收到来自接收者的ACK才会允许它。

Nagle在收到ACK之前不会发送最后一点数据

因此,在每100,000字节传输结束时,我们会得到一个尴尬的暂停。最终,延迟的ack定时器关闭,死锁解除,直到下一次。在千兆网络上,所有这些巨大的200ms暂停可能会对遇到此问题的应用程序协议造成毁灭性的破坏。这些暂停可以将请求/响应应用程序层协议限制为每秒最多5个事务,而该网络链路应该每秒可以处理1000个事务或更多。在进行性能测试的这种特定尝试的情况下,原则上应该能够在短至1ms的时间内通过本地千兆位以太网链路传输每个100,000字节的块。相反,因为它停止并在每个块之后等待,而不是按照上面的建议进行双缓冲,所以发送每个100,000字节的块需要1毫秒+ 200毫秒的暂停= 201毫秒,这使测试的运行速度比实际运行速度慢了大约200倍。

在Windows上,TCP段大小为1460个字节。在Mac OS X和其他添加了TCP时间戳选项的操作系统上,TCP段大小减小了十二个字节:1448个字节。

这意味着在Windows上,100,000个字节是68个全尺寸的1460个字节的数据包加上720个额外的字节。由于68是偶数,因此,靠运气,应用程序避免了Nagle / Delayed ACK交互。

在Mac OS X上,100,000个字节是69个完整的1448字节数据包,外加88个字节。由于69是奇数,因此Mac OS X暴露了应用程序问题。

解决该问题的一种粗略方法(尽管仍不如双缓冲方法有效),是确保应用程序使用单个大写操作发送每个语义消息(通过将消息数据复制到连续缓冲区中,或通过使用分散/聚集类型的写操作(如sendmsg),并设置TCP_NODELAY套接字选项,从而禁用Nagle的算法。这避免了这个特殊问题,尽管它仍然会遭受不使用双重缓冲的其他固有问题,例如,如果响应的最后一个数据包丢失,则紧随其后的数据包将不会触发快速重传。

尽管这里提倡的双缓冲方法(保持进步而不是阻塞并等待每个操作一次完成)仍然是最好的解决方案,但是网络代码的进步现已缓解了这一特定的Nagle算法死锁。

Mac OS X v10.5“ Leopard”(于2007年10月发布)以及后来的更新(包括iOS)实现了Greg Minshall的“ Nagle's Algorithm的拟议修改”,他于1998年12月记录了该文件。我不知道其他操作系统的实现状态系统。

Nagle的算法说,如果您有未确认的任何未完成数据,则无法发送欠缺数据包。 Minshall的Modification修改了此规则,只是说如果您已经有未确认的欠缺数据包,那么您将无法再发送。换句话说,您可以免费获得一张通行证。您一次只能飞行一个矮子包,但只能一个。由于会对网络造成危害,因此仍然不允许多个欠缺数据包。一个写得不好的应用程序会接连执行大量小写操作,这些写操作仍会合并到更有效的大数据包中,但是执行单个大写操作的应用程序将不会延迟该写操作的结尾。大写末尾的单个欠幅数据包不会延迟,因为前面的未完成数据包都是全尺寸数据包。

Greg Minshall的电子邮件阐明了不禁用Nagle算法的重要性:

请注意,尽管在某些情况下,当前的Nagle算法可能会对某些应用程序产生负面的性能影响,但关闭Nagle算法可能会对互联网造成非常严重的负面影响。因此,此电子邮件消息或随附的草稿中的任何内容均不应作为建议,禁止任何应用程序开发人员禁用Nagle算法。当前的Nagle算法对于保护互联网的健康非常重要。提出的修改(希望)提供相同级别的保护。

页面由Stuart Cheshire维护