再次谈谈TCP的Nagle算法与TCP_CORK选项

事件回放

使用OpenVPN传输虚拟桌面流量,终端上有明显逐帧刷屏现象,网络环境为百兆局域网。

分析

1.首先将OpenVPN改为TCP模式,因为局域网环境下TCP和UDP差别不大,不会引起重传叠加问题。TCP的好处在于可以任意蹂躏分析,因为它的算法巨复杂,如果换UDP,太简单了,没啥好调整的;
2.分析过程不是本文的目的,直接给结果吧。减小发送/接收缓冲区到MTU的2倍的样子,减小MTU到1000以下,OpenVPN加上tcp-nodelay选项,结果好了一些,但是还是会刷屏,继续解决,最终确认这个问题搞不定,因为虚拟桌面所在虚拟机没装显卡驱动...(TMD我最烦那些销售眼中的上帝告诉我让我可以解决一些不属于我范畴的问题,比如在连信号都TMD没有的情况下可以保证手机畅通)

TCP啊TCP

我懂TCP是因为我烦它!如果根本不懂就没有资格烦。TCP有超级多的参数可以调整,直接折腾就是了,我一向认为,抓包工具是用来分析数据的,而不是用来分析TCP行为的,你能抓个包告诉我有没有启用Nagle算法吗?你能在客户端抓包分析出来TCP timestamps导致的连接被拒绝吗?学习协议还是要理解设计原理和该协议被设计和修改的历史,这样你才能知道它为什么会这样以及当时是什么原因导致了它这样,说实话,有时看看代码也是有好处的,但是看代码会让人容易钻进死胡同,比如一些非标准的东西一旦进入代码就会让人误以为是标准的东西,然后再看其它与之不同的实现的时候,先入为主的印象就会印象你的认知...
总之,TCP很难,也很令人讨厌,一直觉得TCP设计的很好,但是发展的很乱。

网络利用率与大量小包

很多人都把Nagle算法的目的理解为“ 提高网络利用率”,事实上,Nagle算法所谓的“ 提高网络利用率”只是它的一个副作用(Side effect...),Nagle算法的主旨在于“ 避免发送‘大量’的小包”。Nagle算法并没有阻止发送小包,它只是阻止了发送 大量的小包!诚然,发送大量的小包是降低了网络利用率,但是,发送少量必须发送的小包也是对网络利用率的降低,想彻底提高网络利用率,为嘛不直接阻止小包发送呢?不管是大量小包还是少量小包,甚至一个小包也不让发送,这才是提高网络利用率的正解!是的,TCP_CORK就是做这个的。所以有人说,CORK选项是Nagle的增强,而实际上,它们是完全不同的两回事,初衷不同。
Nagle算法的初衷:避免发送大量的小包,防止小包泛滥于网络, 理想情况下,对于一个TCP连接而言,网络上每次只能一个小包存在。它更多的是端到端意义上的优化。
CORK算法的初衷:提高网络利用率, 理想情况下,完全避免发送小包,仅仅发送满包以及不得不发的小包

关于交互式应用

一直以来,人们有个误区,那就是Nagle算法会为交互式应用引入延迟,建议交互式应用关闭Nagle算法。事实上,正是交互式应用引入了大量的小包,Nagle算法所作用的正是交互式应用!引入一些延迟是必然的,毕竟任何事都要有所代价,但是它更多的是解决了交互式应用产生大量小包的问题, 不能将那么一点点为解决问题所付出的代价作为新的问题而忽视了真正的问题本身!

TCP的Nagle算法和延迟ACK

Nagle算法的操作过程请参看Wiki,它减少了大量小包的发送,实际上就是基于小包的停-等协议。在等待已经发出的包 被确认之前,发送端利用这段时间可以积累应用下来的数据,使其大小趋向于增加。这是避免糊涂窗口综合症的一种有效方法,请注意, 糊涂窗口指的是接收端的糊涂,而不是发送端的糊涂 ,接收端不管三七二十一得通告自己的接收窗口大小,丝毫不管这会在发送端产生大量小包。然而发送端可以不糊涂,你通告你的,我就是不发,你糊涂我不糊涂,你不断通告很小的数值,我不予理睬,我有自己的方法,直到收到已经发出包的ACK才会继续发送,这就是 Nagle算法的糊涂抵制方案
治疗糊涂窗口综合症有两种方式,一种是“ 你糊涂我不糊涂”的方式,即上述的Nagle算法的方式,另外一种是“ 治疗接收端的糊涂”的方式,其中一种机制是延迟ACK(还有其它机制,比如不发送小窗口通告等)。可以看出,这两种方式中都在试图减少包的发送量,二者殊途同归的解决了同一问题,对于发送方而言,不理会接收端的小窗口通告等于说不马上发送小包,小包得以有时间积累成大包,对于接收方而言,延迟ACK可以拖延ACK发送时间,进而延迟窗口通告,在这段时间内,接收窗口有机会进一步(由于应用程序处理)放大。单独理解这两种方式都是简单的,但是一旦它们混在一起使用,情况就会非常不幸!因为...
Nagle算法和延迟ACK作用在方向相反的数据包和针对该数据包的确认包上,因此它们的作用力会相悖,结果就是谁也不能发包。就像一根绳子上拴两只青蛙一样,被对方牵制谁也跑不了!关键点在于,小包的发送依赖于ACK,然而延迟ACK阻止了ACK的即时发送,形成了僵持状态。本来只是 为了减少网络上小包的数量( 再次强调Nagle算法以及延迟ACK的目的,注意,糊涂窗口综合症只是网络上小包泛滥的原因之一!),却人为引入了大量的延迟!
此处有一个通用的解释,Nagle算法的小包发送依赖于接收端对小包得快速确认,因此接收端对待ACK而言,应该朝着延迟ACK相反俄方向用力,即快速ACK;相对的,如果在接收端启用了延迟ACK,发送端就应该不断发送数据包,不管是大包还是小包(不考虑稍带ACK的影响),因为发送端已经不能指望接收端正常ACK数据包了,即发送端应该禁掉Nagle算法。以上解释背后的思想就是数据包和ACK包是相关的, 力应该往一个方向使,一边拉另一边就要推,如果两边都拉,力就会抵消掉,陷入僵持。不幸点或者说悲哀的地方在于, Nagle算法和延迟ACK机制都是“拉方案”!划船的人都知道,划桨手有两种座位布局,要么超同一个方向,要么面朝不同的方向,后者和TCP数据包和ACK很类似,要想船往前走,必须朝一个方向划,虽然他们面朝相反的方向。

编程模型的影响

在Nagle算法的Wiki主页,有这么一段话:
In general, since Nagle's algorithm is only a defense against careless applications, it will not benefit a carefully written application that takes proper care of buffering; the algorithm has either no effect, or negative effect on the application.
可见编程模型对“ 减少网络上小包数量”的影响,言外之意, Nagle算法是个有针对性的优化-针对交互式应用,不是放之四海而皆准的标准,要想有一个比较好的方案,别指望它了,还是应用程序自己搞定才是正解!要想Nagle算法真的能够减少网络上小包数量而又不引入明显延迟,对TCP数据的产生方式是有要求的,交互式应用是其初始针对的对象,,Nagle算法要求 数据必须是“乒乓型”的,也就是说,数据流有明确的边界且一来一回,类似人机交互的那种,比如telnet这种远程终端登录程序,数据是人从键盘敲入的,边界基本上就是击键,一来一回就是输入回显和处理回显。Nagle算法在上面的场景中保证了下一个小包发送之前,所有发出的包已经得到了确认,再次我们看到,Nagle算法并没有阻止发送小包,它只是阻止了发送大量的小包。
换句话说,所谓的“乒乓型”模式就是“write-read-write-read”模式-人机交互模式,但是对于Wiki中指出的“write-write-read”(很多的request/response模式俄C/S服务就是这样的,比如HTTP)-程序交互模式,Nagle算法和延迟ACK拔河的恶果就会被放大,在“TCP的Nagle算法和延迟ACK”一节中我已经说了,这二者不能混用,这个我们已经知道了,但那只是在TCP本身的原理上给与说明,本节中说的是同一个问题,但是侧重于编程层面,然而却不是仅仅用setsocket将TCP_NODELAY设置一下或者关掉延迟ACK那么简单。
有一篇很好的文章《 TCP_CORK: More than you ever wanted to know》,文章说,Nagle算法对于数据来自于user input的那种应用是有效的,但是对于数据generated by applications using stream oriented protocols,Nagle算法纯粹引入了延迟,这个观点我非常赞同,因为对于人而言,TCP登录俄远程计算机就是一个处理机,人希望自己的操作马上展示结果,其模式就是write-read-write-read的,但是对于程序而言,其数据产生逻辑就不像人机交互那么固定,因此你就不能假定程序依照任何序列进行网络IO,而Nagle算法是和数据IO的序列相关的。实际上就算接收端没有启用延迟ACK,Nagle算法应用于write-write-read序列也是有问题的,作者的意思是,平白无故地引入了额外的延迟。
难道真的有这么复杂吗?作者没有提出如何靠编程把问题解决,但是Nagle算法的Wiki页面上提到了” 尽量编写好的代码而不要依赖TCP内置的所谓的算法“来优化TCP的行为。再次引用Nagle算法Wiki上的那句话:

另一件事情

In general, since Nagle's algorithm is only a defense against careless applications, it will not benefit a carefully written application that takes proper care of buffering; the algorithm has either no effect, or negative effect on the application.
这句话引起了我对另外一件事的思考,这件事就发生在上周,由于公司搬家一切处于混乱中,无心工作,一个网络处理加速卡供应商来访,对方声称他们的协议栈实现在用户态以提高性能,我起初对此观点嗤之以鼻,但是结合我的以上分析以及后续的思考,我发现这真的是一个突破:
当我们什么事情都依赖内核的时候,可能我们真的错了!内核不可控,为何不在用户态自己搞定呢。内核提供的是基础设施上的基本服务,提供高端私人订制服务会把整个内核拖垮!如果协议栈在内核实现(宏内核的风格),那么就在用户态管理TCP buffer,解除对复杂TCP算法的依赖,如果协议栈在用户态实现,那就实现一个更好的类Nagle算法!
这件事情我准备再写一篇文章,现在回到《
TCP_CORK: More than you ever wanted to know》

回到《TCP_CORK: More than you ever wanted to know》

作者提到了一个观点,即write的语义,这背后的思想就是” 一切皆文件“。既然一切皆文件,那就要 为所有的IO都定义一套文件IO的操作集。我们知道, UNIX最初将文件IO分为了两种,即块设备IO和字符设备IO,这个观点或者说哲学观念是具有划时代意义的,它带来了编程的简洁性,为程序员带来了一丝清爽之意,那个时候不管是TCP/IP标准还是BSD socket都还没有被提出,当网路协议栈成为标准的时候,作为一种IO类型,网络IO该属于块设备IO还是字符设备IO呢?当时有两种观点,其中之一就是将网络IO从文件IO中独立成一个平级的IO类型,但是最终还是BSD socket获得了胜利,即网络IO也是一种设备IO。然而问题还是在,网络IO到底是属于块设备IO呢,还是该属于字符设备IO?
在回答这个问题之前,首先要为网络IO定义一种设备模型没有设备模型,何谈设备分类?最终,网络IO的设备模型就定义为了packet,正如作者所说,网络IO对应的设备就是网络数据包(packet),用户应用程序通过标准的文件IO接口write将网络数据写入一个设备,该设备就是packet!现在剩下的问题不是这个packet何时真正flush到wire的问题,而是如何生成这个packet的问题,如果数据可以持续积累在一个buffer,然后生成一个packet,那么它就是块设备,如果数据来一个就生成一个packet,那就是字符设备。遗憾的是不管是BSD socket还是协议栈,都没有规定到底有没有这个buffer,因此,网络IO作为一个被另类的类型,独立于块设备IO和字符设备IO之外,成为了第三种IO方式。因此我们可以说,网络IO或者说INET族的socket IO,可以定义一个buffer作为socket文件IO接口到packet(作为一种设备)的缓冲,也可以不定义。定义与否取决于什么?取决于你如何来解释网络!
John Nagle从”小包泛滥会导致网络瘫痪“这个视角出发,发明了Nagle算法,而另一些人从”buffer packet会引入延迟“这个视角出发,抵制Nagle算法,这个争斗其实是没有意义的,其关键导火索在于”一切皆文件“思想遇到了网络IO时的尴尬。
那么我们是否可以回答这一问题呢?网络IO到底应不应该在数据和packet之间有一个buffer?这还是应该取决于packet到底是一个字符设备还是一个块设备。这个问题的答案取决于网络情况和数据接收端情况,对于TCP而言,长肥管道的理想情况下,packet就是一个字符设备,而对于在慢速网络上使用telnet登录的程序而言,packet就是一个块设备,buffer存在与否取决于很多的因素而不是一个因素,存在即是合理的,Nagle算法存在,但是不是让人滥用的。举个例子结束本节,滚滚长江东逝水,顺江而下可行船,拦江截断储水可发电,到底应不应该在长江上构建一个buffer,也是有争议的....

Linux的优化

正如建立大坝存在争议一样,是否缓冲网络数据也存在争议,因此Nagle算法的实现并不存在一个哪怕是事实上的标准,更别提什么工业标准了。因此各家操作系统都有自己的实现,实现中存在有自身的风格。Linux当然也不例外,在数据发送的时候,会有一个tcp_nagle_check函数来判断是否应该将数据buffer起来,我们看一下注释就明白了其实现要旨:
/* Return 0, if packet can be sent now without violation Nagle's rules:
* 1. It is full sized
* 2. Or it contains FIN. (already checked by caller)
* 3. Or TCP_NODELAY was set.
* 4. Or TCP_CORK is not set, and all sent packets are ACKed.
* With Minshall's modification: all sent small packets are ACKed.
*/

值得一提的是TCP_CORK选项和最后的Minshall's modification,我们先看TCP_CORK,说起这个选项,很多人会把它和Nagle算法联系起来,实际上它们的关系并不是十分明确,严格说来TCP_CORK是为了提高数据包的载荷率而引入的,而我们知道,小包的载荷率非常小,所以感觉上好像是TCP_CORK和Nagle算法相关。实际上, CORK选项提高了网络的利用率,因为它直接禁止了小包的发送(再次强调,Nagle算法没有禁止小包发送,只是禁止了大量小包的发送)
CORK选项在实现上还是和文档中所说的有一些出入,比如,在超时情况TCP发送窗口探测包的时候,会将buffer的包发送过去,我们看下其注释:
/* A window probe timeout has occurred. If window is not closed send
* a partial packet else a zero probe.
*/

背后的思想很简单,反正也是要发送探测包,跑不了了,平白只是发送一个探测包,载荷率太低,是不是能稍带一些东西过去呢?允许的情况下,把buffer的packet带过去吧,虽然它们还没有buffer到一个full size的packet。如果说有些同学只是死抠full size的话就会很纠结,事实上,CORK的真正初衷是提高载荷率,提高网络利用率。关于CORK,最后要说的是,它只是Linux实现的一个选项。
最后我们来看一下所谓的
Minshall's modification,这家伙对Nagle算法做了什么修正呢?很简单,虽然Nagle算法要求发送出去的packet被确认之后才能发送小包,但是根据Nagle算法的初衷,它仅仅是避免发送大量小包,那么为何不直接忽略大包的确认呢?也就是说,只要求发出去的小包被确认就可以继续发送小包了,这确实是一种优化,但是也会带来一些额外的效果,Minshall算法保证了只有一个小包在网络上,但是Minshall算法也可能会像机关枪一样突突突的持续发送小包(只要ACK持续到大),按照减少小包数量的初衷,它确实每次只发送一个小包,但是发送间隔却短了...

后记

TCP太复杂了,任何人的任何分析都是片面的,你都能找出漏洞,因为对于TCP而言,其任何一个特性的参数调整都会对其它的特性,长肥管道是理想情况,但在某些情况下却并非最好的情况,Nagle算法有时需要关闭有时需要开启...唉,TCP啊TCP

你可能感兴趣的:(再次谈谈TCP的Nagle算法与TCP_CORK选项)