Nagle 算法
Nagle算法由John Nagle在1984年提出,这个算法可以减少网络中小的packet的数量,从而降低网络的拥塞程度。一个常见的例子就是Telnet程序,用户在控制台的每次击键都会发送一个packet,这个packet通常包含41个字节,然而只有一个字节是payload,其余40个字节都是header,如果每次击键都发送一个packet,那就会造成了巨大的开销。
为了减小这种开销,Nagle算法指出,当TCP发送了一个小的segment(小于MSS),它必须等到接收了对方的ACK之后,才能继续发送另一个小的segment。那么在等待的过程中(一个RTT时间),TCP就能尽量多地将要发送的数据收集在一起,从而减少要发送的segment的数量。
默认情况下,TCP开启了Nagle算法,然而Nagle算法并不是灵丹妙药,它会增加TCP发送数据的延迟。在一些要求低延迟的应用程序中(例如即时通讯应用),则需要禁用Nagle算法:
Delayed ACK
TCP的 Delayed ACK 与Nagle算法有异曲同工之妙,Delayed ACK很好理解,当TCP接收到数据时,并不会立即发送ACK给对方,相反,它会等待应用层产生数据,以便将ACK和数据一起发送(在Linux最多等待40ms)。
我们知道,Nagle算法会增加TCP发送数据的延迟,然而,在某些情况下,Delayed ACK会放大这种延迟。一个常见的例子客户端HTTP POST协议,首先看看客户端的代码:
客户端调用两次write()
来发送HTTP POST请求,服务端则需要调用两次read()
读取客户端的HTTP请求:
通常来说,HTTP请求的header和body都是小的segment,但由于TCP默认开启Nagle算法,因此客户端在发送请求的header之后,如果还未收到ACK,则不能发送body。
Server在收到请求的header之后,由于还没有收到请求的body,因此无法立即产生HTTP响应给客户端,这就导致了http_request_header的ACK大概会延迟40ms才发送。
为避免这种延迟的出现,需要做两件事:
- 设置
TCP_NODELAY
选项。 - 将客户端的两次
write()
合并成一个,避免服务端的Delayed ACK。
Scatter-Gather I/O
write()
函数负责将应用程序缓冲区的数据写入内核缓冲区中,那么合并两次write()
操作,一种常见的做法是先分配一块比较大的缓冲区,接着将两块小的缓冲区的数据依次拷贝到这块大的缓冲区中,最后调用write()
一次性写入这块大的缓冲区的数据。然而这种做法带来不小的开销,一次内存分配操作和两次memcpy()
操作。幸运的是,Linux提供了writev()
函数,它可以将几块不连续的缓冲区的数据写入内核中。
TCP_CORK
大多数Web Server为了提高性能,在发送数据是并不会直接使用write()
,一个典型的例子就是,Web Server响应客户端请求的时候,它需要先发送HTTP响应header,接着发送网页的内容,而网页的内容存在于磁盘中,为了减少数据的拷贝开销,通常是使用sendfile()
去发送页面内容的,这种情况下,应用程序就不需要在用户态分配内存来存储页面内容了。
为了发送HTTP响应,Server调用了一次write()
和一次sendfile()
,在开启TCP_NODELAY
的情况下,这会导致至少两个TCP segment发送出去。但更多时候页面的数据是很少的,在这种情况下,write()
会发送一个segment,sendfile()
也会发送一个segment,那么有没有办法让这两个segment合并在一起再发送出去呢?
为解决这个问题,Linux提供了TCP_CORK
选项,如果在某个TCP socket上开启了这个选项,那就相当于在这个socket的出口堵上了塞子,往这个socket写入的数据都会聚集起来。虽然堵上了塞子,但是segment总得发送,不然数据会塞满整个TCP发送缓冲区的,那么什么时候塞子会打开呢?下面几种情况都会导致这个塞子打开,这样TCP就能继续发送segment出来了。
- 程序取消设置TCP_CORK这个选项。
- socket聚集的数据大于一个MSS的大小。
- 自从堵上塞子写入第一个字节开始,已经经过200ms。
- socket被关闭了。
一旦满足上面的任何一个条件,TCP就会将数据发送出去。对于Server来说,发送HTTP响应既要发送尽量少的segment,同时又要保证低延迟,那么需要在写完数据后显式取消设置TCP_CORK
选项,让数据立即发送出去:
参考资料
- TCP_NODELAY and Small Buffer Writes
- TCP_CORK: More than you ever wanted to know
- TCP/IP options for high-performance data transmission
- Nginx Optimization: understanding sendfile, tcp_nodelay and tcp_nopush
- The Linux Programming Interface Page 1262
- Linux: When to use scatter/gather IO (readv, writev) vs a large buffer with fread