TCP 中的 Delay ACK 和 Nagle 算法

简介: TCP 中的 Delay ACK 和 Nagle 算法

哈喽大家好,我是咸鱼。

今天分享一篇大佬的文章,作者:卡瓦邦噶!

文章链接:https://www.kawabangga.com/posts/5845

教科书介绍的 TCP 内容通常比较基础:包括三次握手,四次挥手,数据发送通过收到 ACK 来保证可靠传输等等。当时我以为已经学会了 TCP,但是后来在工作中,随着接触 TCP 越来越多,我发现很多内容和书上的不一样——现实世界的 TCP 要复杂一些。

我们从一个简单的 HTTP 请求开始。发送一个简单的 HTTP 请求,tcpdump 抓包如下:

04:13:49.438293 IP foobarhost.53422 > 104.244.42.65.http: Flags [S], seq 637381086, win 64240, options [mss 1460,sackOK,TS val 2468293419 ecr 0,nop,wscale 7], length 0
04:13:49.577825 IP 104.244.42.65.http > foobarhost.53422: Flags [S.], seq 1622592001, ack 637381087, win 65535, options [mss 1460], length 0
04:13:49.578004 IP foobarhost.53422 > 104.244.42.65.http: Flags [.], ack 1, win 64240, length 0
04:13:49.578644 IP foobarhost.53422 > 104.244.42.65.http: Flags [P.], seq 1:38, ack 1, win 64240, length 37: HTTP: POST /apikey=1&command=2 HTTP/1.0
04:13:49.579110 IP 104.244.42.65.http > foobarhost.53422: Flags [.], ack 38, win 65535, length 0
04:13:49.702633 IP 104.244.42.65.http > foobarhost.53422: Flags [P.], seq 1:204, ack 38, win 65535, length 203: HTTP: HTTP/1.0 400 Bad Request
04:13:49.702662 IP foobarhost.53422 > 104.244.42.65.http: Flags [.], ack 204, win 64037, length 0
04:13:50.702170 IP 104.244.42.65.http > foobarhost.53422: Flags [F.], seq 204, ack 38, win 65535, length 0
04:13:50.702783 IP foobarhost.53422 > 104.244.42.65.http: Flags [F.], seq 38, ack 205, win 64037, length 0
04:13:50.703525 IP 104.244.42.65.http > foobarhost.53422: Flags [.], ack 39, win 65535, length 0

第一个和书上不一样的地方是,TCP 结束连接不是要 4 次挥手吗?为什么这里只出现了 3 次?

04:13:50.702170 IP 104.244.42.65.http > foobarhost.53422: Flags [F.], seq 204, ack 38, win 65535, length 0
04:13:50.702783 IP foobarhost.53422 > 104.244.42.65.http: Flags [F.], seq 38, ack 205, win 64037, length 0
04:13:50.703525 IP 104.244.42.65.http > foobarhost.53422: Flags [.], ack 39, win 65535, length 0

回顾 TCP 的包结构,FIN 和 ACK 其实是不同的 flags,也就是说,理论上我可以在同一个 Segment 中,即可以设置 FIN 也可以同时设置 ACK。

TCP segment

所以如果在结束连接的时候,客户端发送 FIN,这时候服务端一看:“正好我也没有东西要发送了。”于是,除了要 ACK 自己收到的 FIN 之外,也要发送一个 FIN 回去。那不如我一石二鸟,直接用一个包好了。

TCP FIN 教科书的图,和实际的图

既然 FIN 可以附带去 ACK 自己收到的 FIN,那么数据是否也可以附带 ACK?也是可以的。

Delay ACK

TCP 是全双工的,意味着两端都可以同时向对方发送数据,而两端又需要分别去 ACK 自己收到的数据。

TCP 的一端在收到数据之后,反正马上也要发送数据回去,与其发送两个包:一个 ACK 和一个数据包,不如不立即发送 ACK 回去,而是等待一段时间——我反正一会要发送数据给你,等到那时候,我再带上 ACK 就好啦。这就是 Delay ACK。

数据 + ACK
Delay ACK 可以显著降低网络中纯 ACK 包的数量,大概 1/3. 纯 ACK 包(即 payload length 是 0 ),有 20 bytes IP header 和 20 bytes TCP header。

Delay ACK 的假设是:如果我收到一个包,那么应用层会需要对这个包做出回应,所以我等到应用的回应之后再发出去 ACK。这个假设是有问题的。而且现实是,Delay ACK 所造成的问题比它要解决的问题要多。(下文详解)

Nagle 算法

现在再考虑这样一个问题:像 nc 和 ssh 这样的交互式程序,你按下一个字符,就发出去一个字符给 Server 端。每通过 TCP 发送一个字符都需要额外包装 20 bytes IP header 和 20 bytes TCP header,发送 1 bytes 带来额外的 40 bytes 的流量,不是很不划算吗?

除了像这种程序,还有一种情况是应用代码写的不好。TCP 实际上是由 Kernel 封装然后通过网卡发送出去的,用户程序通过调用 write syscall 将要发送的内容送给 Kernel。有些程序的代码写的不好,每次调用 write 都只写一个字符(发送端糊涂窗口综合症)。如果 Kernel 每收到一个字符就发送出去,那么有用数据和 overhead 占比就是 1/41.

为了解决这个问题,Nagle 设计了一个巧妙的算法 (Nagle’s Algorithm),其本质就是:发送端不要立即发送数据,攒多了再发。但是也不能一直攒,否则就会造成程序的延迟上升。

算法的伪代码如下:

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

简单来说,就是如果要发送的内容足够一个 MSS 了,就立即发送。否则,每次收到对方的 ACK 才发送下一次数据。

Delay ACK 和 Nagle 算法

这两个方法看似都能解决一些问题。但是如果一起用就很糟糕了。

假设客户端打开了 Nagle’s Algorithm,服务端打开了 Delay ACK。这时候客户端要发送一个 HTTP 请求给服务端,这个 HTTP 请求大于 1 MSS,要用 2 个 IP 包发送。于是情况就变成了:

  • Client: 这是第一个包
  • Server:… (不会发送 ACK,直到 Server 想发送数据给 Client,但是这里因为 Server 没有收到整个 HTTP 请求内容,所以 Server 不会发送数据给 Client)
  • Client: … (因为 Nagle 算法,Client 在等待对方的 ACK,然后再发送第二个包的数据)
  • Server: 好吧,我等够了,这是 ACK
  • Client: 这是第二个包

Nagle’s Algorithm 和 Delay ACK 在一起使用的时候的问题

这里有一个类似死锁的情况发生。会导致某些情况下,HTTP 请求有不合理的延迟。

再多说一点有关的历史,我曾经多次在 hackernews 上看到 Nagle 的评论(Nagle 亲自解释 Nagle 算法!1,2)。大约 1980s,Nagle 和 Berkeley 为了解决几乎相同的问题,发明了二者。Berkeley 的问题是,很多用户通过终端共享主机,网络会被 ssh 或者 telnet 这样的字符拥塞。于是用 Delay ACK,确实可以解决 Berkeley 的问题。但是 Nagle 觉得,他们根本不懂问题的根源。如果他当时还在网络领域的话,就不会让这种情况发生。可惜,他当时改行去了一家创业公司,叫 Autodesk。

解决方法是关闭 Delay ACK 或者 Nagle’s Algorithm。

配置方法

关闭 Nagle’s Algorithm 的方法:可以给 socket 设置 TCP_NODELAY. 这样程序在 write 的时候就可以 bypass Nagle’s Algorithm,直接发送。

关闭 Delay ACK 的方法:可以给 socket 设置 TCP_QUICKACK,这样自己(作为 server 端)在收到 TCP segment 的时候会立即 ACK。实际上,在现在的 Linux 系统默认就是关闭的。

前面文章提到过:如果在收到对方的第二次包SYN+ACK之后很快要发送数据,那么第三次包ACK可以带着数据一起发回去。这在Windows和Linux中都是比较流行的一种实现。但是数据的大小在不同实现中有区别。

如果我们关闭 TCP_QUICKACK ,就可以看到几乎每一次 TCP 握手,第三个 ACK 都是携带了数据的。

int off = 0;
setsockopt(sockfd, IPPROTO_TCP, TCP_QUICKACK, &off, sizeof(off));
04:13:32.240213 IP foobarhost.57010 > 104.244.42.65.http: Flags [S], seq 1515096107, win 64240, options [mss 1460,sackOK,TS val 2468276221 ecr 0,nop,wscale 7], length 0
04:13:32.383742 IP 104.244.42.65.http > foobarhost.57010: Flags [S.], seq 1620480001, ack 1515096108, win 65535, options [mss 1460], length 0
04:13:32.384536 IP foobarhost.57010 > 104.244.42.65.http: Flags [P.], seq 1:38, ack 1, win 64240, length 37: HTTP: POST /apikey=1&command=2 HTTP/1.0
相关实践学习
容器服务Serverless版ACK Serverless 快速入门:在线魔方应用部署和监控
通过本实验,您将了解到容器服务Serverless版ACK Serverless 的基本产品能力,即可以实现快速部署一个在线魔方应用,并借助阿里云容器服务成熟的产品生态,实现在线应用的企业级监控,提升应用稳定性。
云原生实践公开课
课程大纲 开篇:如何学习并实践云原生技术 基础篇: 5 步上手 Kubernetes 进阶篇:生产环境下的 K8s 实践 相关的阿里云产品:容器服务 ACK 容器服务 Kubernetes 版(简称 ACK)提供高性能可伸缩的容器应用管理能力,支持企业级容器化应用的全生命周期管理。整合阿里云虚拟化、存储、网络和安全能力,打造云端最佳容器化应用运行环境。 了解产品详情: https://www.aliyun.com/product/kubernetes
相关文章
|
网络协议 应用服务中间件 nginx
玩转Kubernetes TCP Ingress
如何使用Kubernetes的TCP Ingress
19288 0
|
运维 Kubernetes 网络协议
kubernetes 的TCP 数据包可视化
kubernetes 的TCP 数据包可视化
293 0
|
算法 网络协议 中间件
TCP 拥塞控制算法
最近花了些时间在学习TCP/IP协议上,首要原因是由于本人长期以来对TCP/IP的认识就只限于三次握手四次分手上,所以希望深入了解一下。再者,TCP/IP和Linux系统层级的很多设计都可以用于中间件系统架构上,比如说TCP 拥塞控制算法也可以用于以响应时间来限流的中间件。更深一层,像TCP/IP协议这种基础知识和原理性的技术,都是经过长时间的考验的,都是前人智慧的结晶,可以给大家很多启示和帮助。
TCP 拥塞控制算法
|
网络协议
ACK的累加规则-wireshark抓包分析-不包含tcp头部、ip头部、数据链路层头部等。
ACK的累加规则-wireshark抓包分析-不包含tcp头部、ip头部、数据链路层头部等。
ACK的累加规则-wireshark抓包分析-不包含tcp头部、ip头部、数据链路层头部等。
|
网络协议 算法 测试技术
TCP 拥塞控制详解 | 5. 回避算法
TCP 拥塞控制详解 | 5. 回避算法
198 1
TCP 拥塞控制详解 | 5. 回避算法
|
存储 网络协议 算法
TCP 拥塞控制详解 | 4. 控制算法(下)
TCP 拥塞控制详解 | 4. 控制算法(下)
189 0
TCP 拥塞控制详解 | 4. 控制算法(下)
|
存储 网络协议 算法
TCP 拥塞控制详解 | 4. 控制算法(上)
TCP 拥塞控制详解 | 4. 控制算法(上)
193 0
TCP 拥塞控制详解 | 4. 控制算法(上)
|
网络协议 算法 安全
深入浅出 BPF TCP 拥塞算法实现原理
深入浅出 BPF TCP 拥塞算法实现原理
437 0
|
缓存 算法 网络协议
【Java 网络编程】客户端 Socket 配置 ( 超时时间 | 端口复用 | Nagle 算法 | 心跳包机制 | 连接关闭机制 | 缓冲区大小 | 性能权重设置 | 紧急数据设置 )
【Java 网络编程】客户端 Socket 配置 ( 超时时间 | 端口复用 | Nagle 算法 | 心跳包机制 | 连接关闭机制 | 缓冲区大小 | 性能权重设置 | 紧急数据设置 )
902 0