用户层可通过setsockopt系统调用设置TCP套接口的TCP_CORK选项。开启时,内核将阻塞不完整的报文,当关闭此选项时,发送阻塞的报文。此处的不完整指的是应用层发送的数据长度不足一个MSS长度。使用场景是在调用sendfile发送文件内容之前,提前发送一个描述文件信息的头部数据段,并且阻塞住此头部数据,与之后的sendfile数据一同发送。或者用于优化吞吐性能。但是,TCP_CORK最多只能将数据阻塞200毫秒,如果超过此时间值,内核将自动发送阻塞的数据。
用户层设置
内核函数do_tcp_setsockopt处理用户层套接口的TCP_CORK选项设置。开启时,TCP套接口的nonagle变量增加TCP_NAGLE_CORK标志,关闭选项时,清除TCP_NAGLE_CORK标志,调用tcp_push_pending_frames函数发送阻塞的数据。
TCP_CORK的判断位于NAGLE算法判断函数tcp_nagle_check中。如下的第二个判断条件,如果nonagle变量设置了TCP_NAGLE_CORK标志,阻塞数据包的发送。另外两个条件判断为NAGLE算法服务,第一个条件是判断数据包是否达到MSS的长度;第三个判断是在TCP_CORK未开启,并且nonagle为空的情况下(NAGLE算法开启),如果套接口有未确认的数据报文,而且minshell检查为真,意味着上一个发送的小报文还未得到确认,阻塞当前要发送的小报文。
RFC1122定义的延迟确认(delayed ack)功能会导致NAGLE算法在有些情况的低效运行,例如在内核要发送一个大包和紧随的一个小包时,对端的延迟确认功能可能导致第一个大包的确认ACK报文不能及时发送(最长200毫秒),进而本端的最后一个小包由于NAGLE算法也不能发出。为此minshell功能做了一些改进,即在检测到本端并没有未确认的小包时,立即发送此小包。
tcp_push
tcp_sendmsg()中,在sock发送缓存不足、系统内存不足或应用层的数据都拷贝完毕等情况下,
都会调用tcp_push()来把已经拷贝到发送队列中的数据给发送出去。
tcp_push()主要做了以下事情:
tcp_autocorking
当应用程序连续地发送小包时,如果能够把这些小包合成一个全尺寸的包再发送,无疑可以减少
总的发包个数。tcp_autocorking的思路是当规则队列Qdisc、或网卡的发送队列中有尚未发出的
数据包时,那么就延迟小包的发送,等待应用层的后续数据,直到Qdisc或网卡发送队列的数据
包成功发送出去为止。
同时满足以下条件时,tcp_push()才会自动阻塞:
PUSH标志位所表达的是发送方通知接收方传输层应该尽快的将这个报文段交给应用层。传输层及以下的数据往往是由系统所带的协议栈进行处理的,客户端在收到一个个报文之后,经由协议栈解封装之后会立马把数据交给应用层去处理吗?如果说在收到报文之后立马就交给上层,这时候应用层由于数据不全,可能也不会进行处理。而且每来一个报文就交一次,效率很低。因此传输层一般会是隔几个报文,统一上交数据。什么时候上交数据呢,就是在发送方将PUSH标志位置1的时候。那么什么时候标志位会置1呢,通常是发送端觉得传输的数据应用层可以进行处理了的时候。
举个例子来说,TLS 协议中的的证书交换部分,通常证书链的大小在3K-4K左右,一般分三个报文来进行传输。只有当这3K-4K的报文传输完毕之后,那么数据形成完整的证书链,这个时候对于接收方才是有意义的(可以进行证书链的验证),单纯的一个报文无异于乱码。因此在TLS连接中,通常会发现证书的第三个报文同上设置了push位,是发送方来告知接收方,可以把数据送往tcp的上层了,因为这些报文已经组成了有意义的内容了。同样接收方在解析了TCP的PUSH字段后,也会清空自己的缓冲区,向上层交数据。
Nginx 有两个配置项: TCP_NODELAY 和 TCP_NOPUSH
1、从nginx模块中来查看:
语法: tcp_nodelay on | off; |
默认值: tcp_nodelay on;
上下文: http, server, location
开启或关闭nginx使用TCP_NODELAY选项的功能。 这个选项仅在将连接转变为长连接的时候才被启用。(译者注,在upstream发送响应到客户端时也会启用)。
语法: tcp_nopush on | off; |
默认值: tcp_nopush off;
上下文: http, server, location
开启或者关闭nginx在FreeBSD上使用TCP_NOPUSH套接字选项, 在Linux上使用TCP_CORK套接字选项。 选项仅在使用sendfile的时候才开启。 开启此选项允许
在Linux和FreeBSD 4.*上将响应头和正文的开始部分一起发送;
一次性发送整个文件。
tcp_nodelay的功能是什么
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)。可以看到,这种情况对于轻负载的网络来说,可能还可以接受,但是对于重负载的网络而言,就极有可能承载不了而轻易的发生拥塞瘫痪。
假如需要频繁的发送一些小包数据,比如说1个字节,以IPv4为例的话,则每个包都要附带40字节的头,也就是说,总计41个字节的数据里,其中只有1个字节是我们需要的数据。
为了解决这个问题,出现了Nagle算法。它规定:如果包的大小满足MSS,那么可以立即发送,否则数据会被放到缓冲区,等到已经发送的包被确认了之后才能继续发送。
通过这样的规定,可以降低网络里小包的数量,从而提升网络性能。
DelayedAcknowledgment:
假如需要单独确认每一个包的话,那么网络中将会充斥着无数的ACK,从而降低了网络性能。
为了解决这个问题,DelayedAcknowledgment规定:不再针对单个包发送ACK,而是一次确认两个包,或者在发送响应数据的同时捎带着发送ACK,又或者触发超时时间后再发送ACK。
通过这样的规定,可以降低网络里ACK的数量,从而提升网络性能。
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_CORK选项禁用Nagle化,其方式正好同 TCP_NODELAY相反(TCP_CORK 和 TCP_NODELAY 是互相排斥的)。
、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/Response、Request/Response、…,处理,那么由于最后这个小包受到Nagle算法影响无法及时的发送出去
(具体是由于客户端在未结束上一个请求前不会发出新的request数据,导致无法携带ACK而延迟确认,进而导致服务器没收到客户端对上一个小包的的确认导致最后一个小包无法发送出来),
导致第n次请求/响应未能结束,从而客户端第n+1次的Request请求数据无法发出。
在http长连接中,服务器的发生类似于:Write-Write-Read,即返回response header、返回html、读取下一个request
而在http短连接中,服务器的发生类似于:write-read-write-read,即返回处理结果后,就主动关闭连接,短连接中的close之前的小包会立即发生,不会阻塞
因为第一个 write 不会被缓冲,会立刻到达接收端,如果是 write-read-write-read 模式,此时接收端应该已经得到所有需要的数据以进行下一步处理。
接收端此时处理完后发送结果,同时也就可以把上一个packet 的 Ack 可以和数据一起发送回去,不需要 delay,从而不会导致任何问题。
我做了一个简单的试验,注释掉了 HTTP Body 的发送,仅仅发送 Headers, Content-Length 指定为 0。
这样就不会有第二个 write,变成了 write-read-write-read 模式。此时再用 ab 测试,果然没有 40ms 的延迟了。
因此在短连接中并不存在小包阻塞的问题,而在长连接中需要做tcp_nodelay开启。
那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 是互相排斥的)。
下面就让我们仔细分析下其工作原理。
假设应用程序使用sendfile()函数来转移大量数据。应用协议通常要求发送某些信息来预先解释数据,这些信息其实就是报头内容。
典型情况下报头很小,而且套接字上设置了TCP_NODELAY。有报头的包将被立即传输,在某些情况下(取决于内部的包计数器),因为这个包成功地被对方收到后需要请求对方确认。
这样,大量数据的传输就会被推迟而且产生了不必要的网络流量交换。
但是,如果我们在套接字上设置了TCP_CORK(可以比喻为在管道上插入“塞子”)选项,具有报头的包就会填补大量的数据,所有的数据都根据大小自动地通过包传输出去。
当数据传输完成时,最好取消TCP_CORK 选项设置给连接“拔去塞子”以便任一部分的帧都能发送出去。这同“塞住”网络连接同等重要。
总而言之,如果你肯定能一起发送多个数据集合(例如HTTP响应的头和正文),那么我们建议你设置TCP_CORK选项,这样在这些数据之间不存在延迟。
能极大地有益于WWW、FTP以及文件服务器的性能,同时也简化了你的工作。
6、sendfile
从技术角度来看,sendfile()是磁盘和传输控制协议(TCP)之间的一种系统呼叫,但是sendfile()还能够用来在两个文件夹之间移动数据。
在各种不同的操作系统上实现sendfile()都会有所不同,当然这种不同只是极为细微的差别。通常来说,我们会假定所使用的操作系统是Linux核心2.4版本。
系统呼叫的原型有如下几种:
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count)
in_fd 是一种用来读文件的文件描述符。
out_fd 是一种用来写文件的描述符。
Offset 是一种指向被输入文件变量位置的指针,sendfile()将会从它所指向的位置开始数据的读取。
Count 表示的是两个文件描述符之间数据拷贝的字节数。
sendfile()的威力在于,它为大家提供了一种访问当前不断膨胀的Linux网络堆栈的机制。
这种机制叫做“零拷贝(zero-copy)”,这种机制可以把“传输控制协议(TCP)”框架直接的从主机存储器中传送到网卡的缓存块(network card buffers)中去。
为了更好的理解“零拷贝(zero-copy)”以及sendfile(),让我们回忆一下以前我们在传送文件时所需要执行的那些步骤。
首先,一块在用户机器存储器内用于数据缓冲的位置先被确定了下来。
然后,我们必须使用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 */
操作系统核心不得不把所有的数据至少都拷贝两次:先是从核心空间到用户空间的拷贝,然后还得再从用户空间拷贝回核心空间。
每一次操做都需要上下文切换(context-switch)的这个步骤,其中包含了许多复杂的高度占用CPU的操作。
系统自带的工具vmstat能够用来在绝大多数UNIX以及与其类似的操作系统上显示当前的“上下文切换(context-switch)”速率。
请看叫做“CS”的那一栏,有相当一部分的上下文切换是发生在取样期间的。用不同类型的方式进行装载可以让使用者清楚的看到使用这些参数进行装载时的不同效果。
在有了sendfile()零拷贝(zero-copy)之后,如果可能的话,通过使用直接存储器访问(Direct Memory Access)的硬件设备,数据从磁盘读取到操作系统高速缓冲存储器中会变得非常之迅速。
而TLB高速缓冲存储器则被完整无缺的放在那里,没有充斥任何有关数据传输的文件。
应用软件在使用sendfile() primitive的时候会有很高的性能表现,这是因为系统呼叫没有直接的指向存储器,因此,就提高了传输数据的性能。
通常来说,要被传输的数据都是从系统缓冲存储器中直接读取的,其间并没有进行上下文切换的操作,也没有垃圾数据占据高速缓冲存储器。
因此,在服务器应用程序中使用sendfile()能够显著的减少对CPU的占用。
TCP/IP网络的数据传输通常建立在数据块的基础之上。从程序员的观点来看,发送数据意味着发出(或者提交)一系列“发送数据块”的请求。
在系统级,发送单个数据块可以通过调用系统函数write() 或者sendfile() 来完成。
因为在网络连接中是由程序员来选择最适当的应用协议,所以网络包的长度和顺序都在程序员的控制之下。同样的,程序员还必须选择这个协议在软件中得以实现的方式。
TCP/IP协议自身已经有了多种可互操作的实现,所以在双方通信时,每一方都有它自身的低级行为,这也是程序员所应该知道的情况。
尽管有许多TCP选项可供程序员操作,而我们却最关注如何处置其中的两个选项,它们是TCP_NODELAY 和 TCP_CORK,这两个选项都对网络连接的行为具有重要的作用。
许多UNIX系统都实现了TCP_NODELAY选项,但是,TCP_CORK则是Linux系统所独有的而且相对较新;它首先在内核版本2.4上得以实现。
此外,其他UNIX系统版本也有功能类似的选项,值得注意的是,在某种由BSD派生的系统上的TCP_NOPUSH选项其实就是TCP_CORK的一部分具体实现。
三、总结
你的数据传输并不需要总是准确地遵守某一选项或者其它选择。在那种情况下,你可能想要采取更为灵活的措施来控制网络连接:
在发送一系列当作单一消息的数据之前设置TCP_CORK,而且在发送应立即发出的短消息之前设置TCP_NODELAY。
如果需要提供网络的传输效率,应该减少小包的传输,使用TCP_CORK来做汇总传输,在利用sendfile来提高效率;
但如果是交互性的业务,那应该让任意小包可以快速传输,关闭Nagle算法,提高包的传输效率。
TCP_CORK优化了传输的bits效率,tcp_nodelay优化了传输的packet效率。
语法: tcp_nodelay on | off; |
默认值:
tcp_nodelay on;
上下文: http, server, location
开启或关闭nginx使用TCP_NODELAY选项的功能。 这个选项仅在将连接转变为长连接的时候才被启用。(译者注,在upstream发送响应到客户端时也会启用)。
语法: tcp_nopush on | off; |
默认值:
tcp_nopush off;
上下文: http, server, location
开启或者关闭nginx在FreeBSD上使用TCP_NOPUSH套接字选项, 在Linux上使用TCP_CORK套接字选项。 选项仅在使用sendfile的时候才开启。