TCP_NODELAY和TCP_NOPUSH的解释
⼀、问题的来源
今天看到 huoding ⼤哥分享的 lamp ⾯试题,其中⼀点提到了:
Nginx 有两个配置项: TCP_NODELAY 和 TCP_NOPUSH ,请说明它们的⽤途及注意事项。
初看到这个题⽬时,感觉有点印象:
1、在f 中确实有这两项,记得就是配置on或者off,跟性能有关,但具体如何影响性能不太清楚
2、在之前看过的huoding另⼀篇将memcache的⽂章中,有提到过tcp DELAY算法,记得说是当tcp传输⼩于mss的包时不会⽴即发⽣,会缓冲⼀段时间,当之前发⽣的包被ack后才继续发⽣缓冲中的⼩包。
⼆、问题的研究
1、从nginx模块中来查看:
语法: tcp_nodelay on | off;
默认值: tcp_nodelay on;
上下⽂: http, rver, location
开启或关闭nginx使⽤TCP_NODELAY选项的功能。这个选项仅在将连接转变为长连接的时候才被启⽤。(译者注,在upstream发送响应到客户端时也会启⽤)。
语法: tcp_nopush on | off;
默认值: tcp_nopush off;
上下⽂: http, rver, location
开启或者关闭nginx在FreeBSD上使⽤TCP_NOPUSH套接字选项,在Linux上使⽤TCP_CORK套接字选项。选项仅在使⽤ndfile的时候才开启。开启此选项允许
在Linux和FreeBSD 4.*上将响应头和正⽂的开始部分⼀起发送;
⼀次性发送整个⽂件。
从模块指令的解释中带出来⼏个问题:
(1)tcp_nodelay的功能是什么?为什么只有在长连接的时候才启⽤?Only included in keep-alive conn
ections.
(2)tcp_nopush在unix上影响TCP_NOPUSH,在linux上影响TCP_CORK,但估计这只是不同系统上的命名区别,但作⽤是什么?为什么只在ndfile中才启⽤?This option is only available when using ndfile.
这些问题我们需要逐⼀解决...
2、tcp_nodelay的功能是什么
Nagle和DelayedAcknowledgment的延迟问题
⽼实说,这个问题和Memcached没有半⽑钱关系,任何⽹络应⽤都有可能会碰到这个问题,但是鉴于很多⼈在写Memcached程序的时候会遇到这个问题,所以还是拿出来聊⼀聊,
在这之前我们先来看看Nagle和DelayedAcknowledgment的含义:
在⽹络拥塞控制领域,我们知道有⼀个⾮常有名的算法叫做Nagle算法(Nagle algorithm),这是使⽤它的发明⼈John Nagle的名字来命名的,John Nagle在1984年⾸次⽤这个算法来尝试解决福特汽车公司的⽹络拥塞问题(RFC 896),该问题的具体描述是:如果我们的应⽤程序⼀次产⽣1个字节的
数据,⽽这个1个字节数据⼜以⽹络数据包的形式发送到远端服务器,那么就很容易导致⽹络由于太多的数据包⽽过载。⽐如,当⽤户使⽤Telnet连接到远程服务器时,每⼀次击键操作就会产⽣1个字节数据,进⽽发送出去⼀个数据包,所以,在典型情况下,传送⼀个只拥有1个字节有效数据的数据包,却要发费40个字节长包头(即ip 头20字节+tcp头20字节)的额外开销,这种有效载荷(payload)利⽤率极其低下的情况被统称之为愚蠢窗⼝症候群(Silly
Window Syndrome)。可以看到,这种情况对于轻负载的⽹络来说,可能还可以接受,但是对于重负载的⽹络⽽⾔,就极有可能承载不了⽽轻易的发⽣拥塞瘫痪。
通俗来说
Nagle:
假如需要频繁的发送⼀些⼩包数据,⽐如说1个字节,以IPv4为例的话,则每个包都要附带40字节的头,也就是说,总计41个字节的数据⾥,其中只有1个字节是我们需要的数据。
为了解决这个问题,出现了Nagle算法。它规定:如果包的⼤⼩满⾜MSS,那么可以⽴即发送,否则数据会被放到缓冲区,等到已经发送的包被确认了之后才能继续发送。
通过这样的规定,可以降低⽹络⾥⼩包的数量,从⽽提升⽹络性能。
再看看DelayedAcknowledgment:
假如需要单独确认每⼀个包的话,那么⽹络中将会充斥着⽆数的ACK,从⽽降低了⽹络性能。
为了解决这个问题,DelayedAcknowledgment规定:不再针对单个包发送ACK,⽽是⼀次确认两个包,或者在发送响应数据的同时捎带着发送ACK,⼜或者触发超时时间后再发送ACK。
通过这样的规定,可以降低⽹络⾥ACK的数量,从⽽提升⽹络性能。
3、Nagle和DelayedAcknowledgment是如何影响性能的
Nagle和DelayedAcknowledgment虽然都是好⼼,但是它们在⼀起的时候却会办坏事。
如果⼀个 TCP 连接的⼀端启⽤了 Nagle‘s Algorithm,⽽另⼀端启⽤了 TCP Delayed Ack,⽽发送的数据包⼜⽐较⼩,则可能会出现这样的情况:
发送端在等待接收端对上⼀个packet 的 Ack 才发送当前的 packet,⽽接收端则正好延迟了此 Ack 的发送,那么这个正要被发送的 packet 就会同样被延迟。
当然 Delayed Ack 是有个超时机制的,⽽默认的超时正好就是 40ms。
现代的 TCP/IP 协议栈实现,默认⼏乎都启⽤了这两个功能,你可能会想,按我上⾯的说法,当协议报⽂很⼩的时候,岂不每次都会触发这个延迟问题?
事实不是那样的。仅当协议的交互是发送端连续发送两个 packet,然后⽴刻 read 的时候才会出现问题。
现在让我们假设某个应⽤程序发出了⼀个请求,希望发送⼩块数据。我们可以选择⽴即发送数据或者等待产⽣更多的数据然后再⼀次发送两种策略。
如果我们马上发送数据,那么交互性的以及客户/服务器型的应⽤程序将极⼤地受益。
例如,当我们正在发送⼀个较短的请求并且等候较⼤的响应时,相关过载与传输的数据总量相⽐就会⽐较低,⽽且,如果请求⽴即发出那么响应时间也会快⼀些。
以上操作可以通过设置套接字的TCP_NODELAY选项来完成,这样就禁⽤了Nagle 算法。
另外⼀种情况则需要我们等到数据量达到最⼤时才通过⽹络⼀次发送全部数据,这种数据传输⽅式有益于⼤量数据的通信性能,典型的应⽤就是⽂件服务器。
应⽤Nagle算法在这种情况下就会产⽣问题。但是,如果你正在发送⼤量数据,你可以设置TCP_COR
K选项禁⽤Nagle化,其⽅式正好同TCP_NODELAY相反(TCP_CORK 和 TCP_NODELAY 是互相排斥的)。
假设客户端的请求发⽣需要等待服务端的应答后才能继续发⽣下⼀包,即串⾏执⾏,
好⽐在⽤ab性能测试时只有⼀个并发做10k的压⼒测试,测试地址返回的内容只有Hello world;ab发出的request需要等待服务器返回respon时,才能发⽣下⼀个request;
此时ab只会发⽣⼀个get请求,请求的相关内容包含在header中;⽽服务器需要返回两个数据,⼀个是respon头,另⼀个是html body;
服务器发送端发送的第⼀个 write 是不会被缓冲起来,⽽是⽴刻发送的(respon header),
这时ab接收端收到对应的数据,但它还期待更多数据(html)才进⾏处理,所以不会往回发送数据,因此也没机会把 Ack 给带回去,根据Delayed Ack 机制,这个 Ack 会被 Hold 住。
这时服务器发送端发送第⼆个包,⽽队列⾥还有未确认的数据包(respon header),这个 packet(html)会被缓冲起来。
此时,服务器发送端在等待ab接收端的 Ack;ab接收端则在 Delay 这个 Ack,所以都在等待,
直到ab接收端 Deplayed Ack 超时(40ms),此 Ack 被发送回去,发送端缓冲的这个 packet(html)才会被真正送到接收端,
此时ab才接受到完整的数据,进⾏对应的应⽤层处理,处理完成后才继续发⽣下⼀个request,因此服务器端才会在read时出现40ms的阻塞。
4、tcp_nodelay为什么只在keep-alive才启作⽤
TCP中的Nagle算法默认是启⽤的,但是它并不是适合任何情况,对于telnet或rlogin这样的远程登录应⽤的确⽐较适合(原本就是为此⽽设计),但是在某些应⽤场景下我们却⼜需要关闭它。
在Apache对HTTP持久连接(Keep-Alive,Prsistent-Connection)处理时凸现的奇数包&结束⼩包问题(The Odd/Short-Final-Segment Problem),
这是⼀个并的关系,即问题是由于已有奇数个包发出,并且还有⼀个结束⼩包(在这⾥,结束⼩包并不是指带FIN旗标的包,⽽是指⼀个HTTP请求或响应的结束包)等待发出⽽导致的。
我们来看看具体的问题详情,以3个包+1个结束⼩包为例,可能发⽣的发包情况:
服务器向客户端发出两个⼤包;客户端在接受到两个⼤包时,必须回复ack;
接着服务器向客户端发送⼀个中包或⼩包,但服务器由于Delayed Acknowledgment并没有马上ack;
由于发⽣队列中有未被ack的包,因此最后⼀个结束的⼩包被阻塞等待。
最后⼀个⼩包包含了整个响应数据的最后⼀些数据,所以它是结束⼩包,如果当前HTTP是⾮持久连接,那么在连接关闭时,最后这个⼩包会⽴即发送出去,这不会出现问题;
但是,如果当前HTTP是持久连接(⾮pipelining处理,pipelining仅HTTP 1.1⽀持,nginx⽬前对pipelining的⽀持很弱,它必须是前⼀个请求完全处理完后才能处理后⼀个请求),
即进⾏连续的Request/Respon、Request/Respon、…,处理,那么由于最后这个⼩包受到Nagle算法影响⽆法及时的发送出去
(具体是由于客户端在未结束上⼀个请求前不会发出新的request数据,导致⽆法携带ACK⽽延迟确认,进⽽导致服务器没收到客户端对上⼀个⼩包的的确认导致最后⼀个⼩包⽆法发送出来),
导致第n次请求/响应未能结束,从⽽客户端第n+1次的Request请求数据⽆法发出。
在http长连接中,服务器的发⽣类似于:Write-Write-Read,即返回respon header、返回html、读取下⼀个request
⽽在http短连接中,服务器的发⽣类似于:write-read-write-read,即返回处理结果后,就主动关闭连接,短连接中的clo之前的⼩包会⽴即发⽣,不会阻塞
我的理解是这样的:因为第⼀个 write 不会被缓冲,会⽴刻到达接收端,如果是 write-read-write-read 模式,此时接收端应该已经得到所有
需要的数据以进⾏下⼀步处理。
接收端此时处理完后发送结果,同时也就可以把上⼀个packet 的 Ack 可以和数据⼀起发送回去,不需要 delay,从⽽不会导致任何问题。
我做了⼀个简单的试验,注释掉了 HTTP Body 的发送,仅仅发送 Headers, Content-Length 指定为 0。
这样就不会有第⼆个 write,变成了 write-read-write-read 模式。此时再⽤ ab 测试,果然没有 40ms 的延迟了。
因此在短连接中并不存在⼩包阻塞的问题,⽽在长连接中需要做tcp_nodelay开启。
5、那tcp_nopush⼜是什么?
TCP_CORK选项的功能类似于在发送数据管道出⼝处插⼊⼀个“塞⼦”,使得发送数据全部被阻塞,直到取消TCP_CORK选项(即拔去塞⼦)或被阻塞数据长度已超过MSS才将其发送出去。
选项TCP_NODELAY是禁⽤Nagle算法,即数据包⽴即发送出去,⽽选项TCP_CORK与此相反,可以认为它是Nagle算法的进⼀步增强,即阻塞数据包发送,
具体点说就是:TCP_CORK选项的功能类似于在发送数据管道出⼝处插⼊⼀个“塞⼦”,使得发送数据全部被阻塞,
直到取消TCP_CORK选项(即拔去塞⼦)或被阻塞数据长度已超过MSS才将其发送出去。
举个对⽐⽰例,⽐如收到接收端的ACK确认后,Nagle算法可以让当前待发送数据包发送出去,即便它的当前长度仍然不够⼀个MSS,
但选项TCP_CORK则会要求继续等待,这在前⾯的tcp_nagle_check()函数分析时已提到这⼀点,即如果包数据长度⼩于当前MSS
&&((加塞 || …)|| …),那么缓存数据⽽不⽴即发送:
在TCP_NODELAY模式下,假设有3个⼩包要发送,第⼀个⼩包发出后,接下来的⼩包需要等待之前的⼩包被ack,在这期间⼩包会合并,直到接收到之前包的ack后才会发⽣;
⽽在TCP_CORK模式下,第⼀个⼩包都不会发⽣成功,因为包太⼩,发⽣管道被阻塞,同⼀⽬的地的⼩包彼此合并后组成⼀个⼤于mss的包后,才会被发⽣
TCP_CORK选项“堵塞”特性的最终⽬的⽆法是为了提⾼⽹络利⽤率,既然反正是要发⼀个数据包(零窗⼝探测包),
如果有实际数据等待发送,那么⼲脆就直接发送⼀个负载等待发送数据的数据包岂不是更好?
我们已经知道,TCP_CORK选项的作⽤主要是阻塞⼩数据发送,所以在nginx内的⽤处就在对响应头的发送处理上。
⼀般⽽⾔,处理⼀个客户端请求之后的响应数据包括有响应头和响应体两部分,那么利⽤TCP_CORK选项就能让这两部分数据⼀起发送:
假设我们需要等到数据量达到最⼤时才通过⽹络⼀次发送全部数据,这种数据传输⽅式有益于⼤量数据的通信性能,典型的应⽤就是⽂件服务器。
应⽤Nagle算法在这种情况下就会产⽣问题。因为TCP_NODELAY在发⽣⼩包时不再等待之前的包有没有ack,⽹络中会存在较多的⼩包,但这会影响⽹络的传输能⼒;
但是,如果你正在发送⼤量数据,你可以设置TCP_CORK选项禁⽤Nagle化,其⽅式正好同 TCP_NODELAY相反(TCP_CORK 和
TCP_NODELAY 是互相排斥的)。
下⾯就让我们仔细分析下其⼯作原理。
假设应⽤程序使⽤ndfile()函数来转移⼤量数据。应⽤协议通常要求发送某些信息来预先解释数据,这些信息其实就是报头内容。
典型情况下报头很⼩,⽽且套接字上设置了TCP_NODELAY。有报头的包将被⽴即传输,在某些情况下(取决于内部的包计数器),因为这个包成功地被对⽅收到后需要请求对⽅确认。
这样,⼤量数据的传输就会被推迟⽽且产⽣了不必要的⽹络流量交换。
但是,如果我们在套接字上设置了TCP_CORK(可以⽐喻为在管道上插⼊“塞⼦”)选项,具有报头的包就会填补⼤量的数据,所有的数据都根据⼤⼩⾃动地通过包传输出去。
当数据传输完成时,最好取消TCP_CORK 选项设置给连接“拔去塞⼦”以便任⼀部分的帧都能发送出去。这同“塞住”⽹络连接同等重要。
总⽽⾔之,如果你肯定能⼀起发送多个数据集合(例如HTTP响应的头和正⽂),那么我们建议你设置TCP_CORK选项,这样在这些数据之间不存在延迟。
能极⼤地有益于WWW、FTP以及⽂件服务器的性能,同时也简化了你的⼯作。
6、ndfile
从技术⾓度来看,ndfile()是磁盘和传输控制协议(TCP)之间的⼀种系统呼叫,但是ndfile()还能够⽤来在两个⽂件夹之间移动数据。
在各种不同的操作系统上实现ndfile()都会有所不同,当然这种不同只是极为细微的差别。通常来说,我们会假定所使⽤的操作系统是Linux核⼼2.4版本。
系统呼叫的原型有如下⼏种:
ssize_t ndfile(int out_fd, int in_fd, off_t *offt, size_t count)
in_fd 是⼀种⽤来读⽂件的⽂件描述符。
out_fd 是⼀种⽤来写⽂件的描述符。
Offt 是⼀种指向被输⼊⽂件变量位置的指针,ndfile()将会从它所指向的位置开始数据的读取。
Count 表⽰的是两个⽂件描述符之间数据拷贝的字节数。
ndfile()的威⼒在于,它为⼤家提供了⼀种访问当前不断膨胀的Linux⽹络堆栈的机制。
这种机制叫做“零拷贝(zero-copy)”,这种机制可以把“传输控制协议(TCP)”框架直接的从主机存储器中传送到⽹卡的缓存块(network card buffers)中去。
为了更好的理解“零拷贝(zero-copy)”以及ndfile(),让我们回忆⼀下以前我们在传送⽂件时所需要执⾏的那些步骤。
⾸先,⼀块在⽤户机器存储器内⽤于数据缓冲的位置先被确定了下来。
然后,我们必须使⽤read()这条系统呼叫来把数据从⽂件中拷贝到前边已经准备好的那个缓冲区中去。
(在通常的情况下,这个操做会把数据从磁盘上拷贝到操作系统的⾼速缓冲存储器中去,然后才会把数据从⾼速缓冲存储器中拷贝⾄⽤户空间中去,这种过程就是所谓的“上下⽂切换”。)
在完成了上述的那些步骤之后,我们得使⽤write()系统呼叫来将缓冲区中的内容发送到⽹络上去,程序段如下所⽰:
intout_fd, intin_fd;
char buffer[BUFLEN];
…
/* unsubstantial code skipped for clarity */
…
read(in_fd, buffer, BUFLEN); /* syscall, make context switch */
write(out_fd, buffer, BUFLEN); /* syscall, make context switch */
操作系统核⼼不得不把所有的数据⾄少都拷贝两次:先是从核⼼空间到⽤户空间的拷贝,然后还得再从⽤户空间拷贝回核⼼空间。