tcp nagle's algorithm and delayed ack

本文内容主要是为了帮助理解socket对象中的一个方法:socket.setTcpNoDelay(boolean)

其中相关的主要内容如下:

  1. 糊涂窗口综合症- silly window syndrome。
  2. nagle 算法 - nagle's algorithm。
  3. 延迟 ACK - tcp delayed acknowledgment。

糊涂窗口综合症

当 TCP 连接建立之后,在一些情况下,网络传输的 TCP 报文段中数据长度只有 1 个字节,而传输开销有 40 字节(20 字节的 IP 头 + 20 字节的 TCP 头),结果有很多 41 字节的 IP 数据报就在互连网中传来传去,造成网络拥塞,这种现象就叫糊涂窗口综合症

如果要避免糊涂窗口综合症,可以从发送端和接收端分别进行优化设置:

  1. 发送端使用Nagle 算法
  2. 接收端设置延迟 ACK

Nagle's algorithm

为了避免糊涂窗口综合症,纳格算法会尽可能发送大块数据,减少大量小数据包的发送,避免网络中充斥着许多小数据块,从而提高网络利用率。

纳格算法实现

参考 tcp_output.c 文件 1679 行tcp_nagle_check函数注释:

16791680168116821683168416851686168716881689169016911692
/* Return false, if packet can be sent now without violation Nagle's rules: * 1. It is full sized. (provided by caller in %partial bool) * 2. Or it contains FIN. (already checked by caller) * 3. Or TCP_CORK is not set, and TCP_NODELAY is set. * 4. Or TCP_CORK is not set, and all sent packets are ACKed. *    With Minshall's modification: all sent small packets are ACKed. */static bool tcp_nagle_check(bool partial, const struct tcp_sock *tp,                int nonagle){    return partial &&        ((nonagle & TCP_NAGLE_CORK) ||         (!nonagle && tp->packets_out && tcp_minshall_check(tp)));}

纳格算法伪代码

if there is new data to send  if the window size >= MSS and available data is >= MSS    send complete MSS segment now  else    if there is unconfirmed data still in the pipe      enqueue data in the buffer until an acknowledge is received    else      send data immediately    end if  end ifend if

纳格算法在发送端为了避免发送很小的 tcp segment,规定只有在下面情况下才会发送 tcp segment:

  1. 发送端的数据累计达到了 MSS(maximum segment size)。
  2. 如果该包含有 FIN,则允许发送。
  3. 设置了 TCP_NODELAY 选项,则允许发送。
  4. 未设置 TCP_CORK 选项时,若所有发出去的小数据包(包长度小于 MSS)均收到 ACK 确认,则允许发送。
  5. 上述条件都未满足,但发生了超时(一般为 200ms),则立即发送。

纳格算法维基百科原文有段说明如下:

A solution recommended by Nagle is to avoid the algorithm sending premature packets by buffering up application writes and then flushing the buffer:

The user-level solution is to avoid write-write-read sequences on sockets. write-read-write-read is fine. write-write-write is fine. But write-write-read is a killer. So, if you can, buffer up your little writes to TCP and send them all at once. Using the standard UNIX I/O package and flushing write before each read usually works.

也就是说纳格算法对于write-read-write-readwrite-write-write模式的应用能有效的优化网络,但对于使用write-write-read模式的应用,在启用纳格算法时,却反而可能会带来程序运行性能的问题,纳格算法的维基百科页面上提到了尽量编写好的代码而不要依赖 TCP 内置的所谓的算法来优化 TCP 的行为。

使用TCP_NODELAY选项可以禁止纳格算法。

延迟确认机制 - TCP delayed acknowledgment

RFC 1122定义,全名Delayed Acknowledgment,简称延迟 ACK,翻译为延迟确认

接收端不启用延迟 ACK时,在接收到每一个数据包后,都会发送一个 ACK 报文给发送方,这样就增加了网络中传输的小报文数。

与 Nagle 算法一样,延迟 ACK的目的也是为了减少网络中传输大量的小报文数,但是此设置是针对接收端的 ACK 报文的。

一个来自发送端的报文到达接收端,TCP 会延迟 ACK 的发送,根据实际情况来回复ACK 确认给发送端:

  1. 将 ACK 确认推迟到下一个 TCP segment 到来,即每收到两个 TCP segment,发送一个 ACK 确认。
  2. ACK 定时器超时,此时一个 TCP segment 对应一个 ACK 确认。
  3. 应用程序会对刚刚收到的数据进行应答,这样就可以用新数据将 ACK 捎带过去。

延迟 ACK最终目标是通过捎带技术或者多个 segment 共用一个 ACK 确认等技术来减少用于ACK 确认的 TCP segment 的数量,这样可以减少通信量,提高吞吐率。

关于 ACK 定时器超时说明

TCP 标准推荐最多延迟 500ms,微软指定的延迟为 200ms,Linux 上延迟 40ms。

Reducing the TCP delayed ack timeout中说明如下,其中tcp_delack_min在高版本的 Linux 服务器上可能已经不支持了:

Some applications that send small network packets can experience latencies due to the TCP delayed acknowledgement timeout. This value defaults to 40ms. To avoid this problem, try reducing the tcp_delack_min timeout value. This changes the minimum time to delay before sending an acknowledgement systemwide.

Write the desired minimum value, in microseconds, to /proc/sys/net/ipv4/tcp_delack_min

当纳格算法遇到延迟确认

write-write-read模式的应用程序中,发送端启用纳格算法,接收端启用延迟 ACK时,就会对程序产生性能影响,简单说明如下:

发送端伪代码示例

write(head); // writewrite(body); // writeread(response); // read

接收端伪代码示例

read(request);process(request);write(response);

假设这里 head 和 body 都比较小,并默认启用纳格算法,并且是第一次发送的时候:

  1. 根据 nagle 算法,第一个段 head 可以立即发送,因为没有等待确认的段;
  2. 接收端收到 head,但是包不完整,继续等待 body 达到并延迟 ACK
  3. 发送端继续写入 body,这时候 nagle 算法起作用了,因为 head 还没有被 ACK,所以 body 要延迟发送,这就造成了发送端和接收端都在等待对方发送数据的现象:
  4. 发送端等待接收端对 head 进行 ACK 确认,以便继续发送 body;
  5. 接收端在等待发送方发送 body 并延迟 ACK

这种时候只有等待一端超时并发送数据,应用程序才能继续往下执行,一般接收端的延迟 ACK40ms 超时先触发,此而在程序中就产生了 40ms 的响应延时。

java 代码示例

接收端 server

public class SocketTcpNoDelayServer {    private static final Logger LOGGER = LoggerFactory.getLogger(SocketTcpNoDelayServer.class);    private static final int PORT = 8888;    public static void main(String[] args) throws IOException {        ServerSocket serverSocket = new ServerSocket();        serverSocket.bind(new InetSocketAddress(PORT));        LOGGER.debug("Server startup at {}", PORT);        byte[] data = new byte[1460];        while (true) {            Socket socket = serverSocket.accept();            try {                InputStream socketInputStream = socket.getInputStream();                OutputStream socketOutputStream = socket.getOutputStream();                for (int i = 1; ; i++) {                    try {                        int read = socketInputStream.read(data);                        if (read == -1) {                            LOGGER.info("socket close for : {}", socket);                            socket.close();                            break;                        } else if (read > 0) {                            String line = new String(data, 0, read);                            LOGGER.info("{} : {}", i, line);                            socketOutputStream.write("test".getBytes());                        }                    } catch (IOException e) {                        e.printStackTrace();                        socket.close();                        break;                    }                }            } catch (IOException e) {                e.printStackTrace();                socket.close();            }        }    }}

运行 main 方法启动服务端应用程序。

发送端 client

public class SocketTcpNoDelayClient {    private static final Logger LOGGER = LoggerFactory.getLogger(SocketTcpNoDelayClient.class);    public static void main(String[] args) throws IOException {        Socket socket = new Socket();        // socket.setTcpNoDelay(true);        socket.setTcpNoDelay(false);        socket.connect(new InetSocketAddress("localhost", 8888));        byte[] data = new byte[1460];        try {            OutputStream socketOutputStream = socket.getOutputStream();            String head = "hello ";            String body = "world\r\n";            int i = 0;            for (; i < 5; i++) {                long start = System.currentTimeMillis();                // The user-level solution is to avoid write-write-read sequences on sockets.                // write-read-write-read is fine.                // write-write-write is fine.                // But write-write-read is a killer.                socketOutputStream.write(head.getBytes()); // write                socketOutputStream.write(body.getBytes()); // write                read(socket, data, i);                     // read                // 注意如果时间大于40ms,说明服务器接收端tcp有设置Delayed Ack,                // 是在等待后续数据包delayed超时之后,再向客户端发回ack消息,客户端再发第2个packet的                long end = System.currentTimeMillis();                LOGGER.debug("RTT: {}", end - start);            }            // 客户端发起 FIN 包            socket.shutdownOutput();            while (read(socket, data, i++) != -1) {                LOGGER.info("read server response ...");            }        } catch (IOException e) {            e.printStackTrace();            socket.close();        }    }    private static int read(Socket socket, byte[] data, int i) throws IOException {        int read = socket.getInputStream().read(data);   // read        if (read == -1) {            LOGGER.info("socket close for : {}", socket);            socket.close();        } else if (read > 0) {            String line = new String(data, 0, read);            LOGGER.info("{} : response : {}", i, line);        }        return read;    }}

运行测试用例,并使用 wireshark 抓包截图如下:

  1. 上图标黑的前 2 条是在禁用纳格算法时,客户端连续发送 head 和 body 给服务端,图中序号为 2057 和 2058 这 2 条。
  2. 启用纳格算法时(socket 默认就是启用的),在发送 head 和 body 之间,有收到接收端的一个ACK 确认消息,即图中 2105 条,如图显示第一次服务器并没有Delay ACK,至于服务器端是否有Delayed ACK则要看RTT时间,如果超过40ms则很可能服务端有启用了Delayed ACK

2019.3.28 更新 tshark 截屏

上面测试代码中Hello是 6 个字节,world\r\n是 7 个字节,如果是 13 个字节一起发送出去了,那就是这 2 个数据包被一起发送给服务端了。

本地开启 Nagle 算法时

可以看到下图中第 14,20 条记录的Len分别为2013,就是 Nagle 算法生效的表现,另外一些即时发送是因为客户端收到了服务端的ACK包,所以不管多小的数据都立即发送出去了。

局域网中的 Nagle 算法

在局域网上一个字节被发送、确认和回显的平均往返时间约为 16ms,在局域网环境下两个主机之间发送数据时很少使用 Nagle 算法。

本地关闭 Nagle 算法时

不管数据包大小都被即时发送给服务端了。

远程开启 Nagle 算法时

第一个数据包不管多小都是即时发送出去的,后面Len为 13 的数据包,都是因为 Nagle 算法生效产生的结果,对比关闭 Nagle 算法的数据可以看到,原来要 28 个数据包往来,开启 Nagle 之后只要 20 个数据包往来,如下图所示。

远程关闭 Nagle 算法时

不管数据包大小都被即时发送给服务端了,这里来回一共 28 个数据包。

References

  1. tcp_output.c
  2. Nagle's algorithm
  3. 当 Nagle 算法遇到 Delayed ACK
  4. java socket 参数详解:TcpNoDelay
  5. 神秘的 40 毫秒延迟与 TCP_NODELAY
  6. 再次谈谈 TCP 的 Nagle 算法与 TCP_CORK 选项
  7. 第19章 TCP的交互数据流