网络性能优化参数关系解读 | TCP Nagle / TCP_NODELAY / TCP_QUICKACK / TCP_CORK

注:本文为 “网路性能优化” 相关文章合辑

未整理去重。

如有内容异常,请看原文。


TCP_NODELAY 详解

lenky0401 发表于 2012-08-25 16:40

在网络拥塞控制领域,Nagle 算法(Nagle algorithm)是一个非常著名的算法,其名称来源于发明者 John Nagle。1984 年,John Nagle 首次提出该算法,旨在解决福特汽车公司网络中的拥塞问题(RFC 896)。具体问题描述如下:如果应用程序每次仅产生 1 个字节的数据,并以网络数据包的形式发送到远端服务器,那么很容易导致网络因过多的数据包而过载。例如,在使用 Telnet 连接到远程服务器时,每次击键操作会生成 1 个字节的数据并发送一个数据包。在这种情况下,一个仅包含 1 个字节有效数据的数据包,却需要携带 40 个字节的包头(即 IP 头 20 字节 + TCP 头 20 字节),有效载荷(payload)利用率极低。这种情况被称为“愚蠢窗口症候群”(Silly Window Syndrome)。对于轻负载网络,这种状况或许可以接受,但对于重负载网络,极易导致拥塞甚至瘫痪。

为解决上述问题,Nagle 算法的核心改进如下:如果发送端多次发送包含少量字符的数据包(长度小于 MSS 的数据包称为小包,长度等于 MSS 的数据包称为大包),则发送端会先发送第一个小包,而将后续到达的少量字符数据缓存起来,直到收到接收端对前一个数据包的 ACK 确认,或者当前字符属于紧急数据,或者积攒的数据量达到一定数量(例如缓存的数据已达到数据包的最大长度),才会将这些数据组成一个较大的数据包发送出去。

以下是 Linux 内核中相关的代码实现:

/* Filename : \linux-3.4.4\net\ipv4\tcp_output.c */
/* Return 0, if packet can be sent now without violation Nagle's rules:* 1. It is full sized.* 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 inline int tcp_nagle_check (const struct tcp_sock *tp,const struct sk_buff *skb,unsigned mss_now, int nonagle)
{return skb->len < mss_now &&((nonagle & TCP_NAGLE_CORK) ||(!nonagle && tp->packets_out && tcp_minshall_check (tp)));
}
/* Return non-zero if the Nagle test allows this packet to be sent now. */
static inline int tcp_nagle_test (const struct tcp_sock *tp, const struct sk_buff *skb,unsigned int cur_mss, int nonagle)
{/* Nagle rule does not apply to frames, which sit in the middle of the write_queue (they have no chances to get new data).* This is implemented in the callers, where they modify the 'nonagle' argument based upon the location of SKB in the send queue.*/if (nonagle & TCP_NAGLE_PUSH)return 1;/* Don't use the nagle rule for urgent data (or for the final FIN).* Nagle can be ignored during F-RTO too (see RFC413).*/if (tcp_urg_mode (tp) || (tp->frto_counter == 2) ||(TCP_SKB_CB (skb)->tcp_flags & TCPHDR_FIN))return 1;if (!tcp_nagle_check (tp, skb, cur_mss, nonagle))return 1;return 0;
}

从代码中可以看出,tcp_nagle_test() 函数首先检查是否设置了 TCP_NAGLE_PUSH 标志。如果设置了该标志(例如主动禁止 Nagle 算法,或者明确是连接的最后一个数据包),则立即返回 1,表示可以发送数据包。接下来,代码处理特殊数据包,如紧急数据包、带 FIN 标志的结束包以及带 F-RTO 标志的包。最后,调用 tcp_nagle_check() 函数进行判断。如果该函数返回 1,则表示数据包不立即发送。具体逻辑为:如果数据包长度小于当前 MSS,并且满足以下条件之一,则缓存数据而不立即发送:

  1. 已主动加塞或明确标识后续还有数据(内核表示为 MSG_MORE)。
  2. 启用了 Nagle 算法,并且存在未被 ACK 确认的已发送数据包。

img

上图左侧展示了未开启 Nagle 算法的情况,客户端应用层下传的数据包被立即发送到网络中,而不管数据包的大小如何。右侧图展示了开启 Nagle 算法后的情况,在未收到服务器对第一个数据包的 ACK 确认之前,客户端应用层下传的数据包被缓存起来,直到收到 ACK 确认后才发送。这样,总包数由原来的 3 个减少为 2 个,网络负载降低,同时客户端和服务器需要处理的数据包数量也减少,从而降低了 CPU 等资源的消耗。

虽然 Nagle 算法在某些场景下能够提高网络利用率、降低包处理主机资源消耗,但在某些场景下却弊大于利。这需要引入另一个概念:延迟确认(Delayed ACK)。延迟确认是提高网络利用率的另一种优化机制,它针对的是 ACK 确认包。在 TCP 协议中,正常情况下,接收端会对收到的每一个数据包向发送端发送一个 ACK 确认包。而延迟确认机制则是将 ACK 延后发送,使其与数据包或窗口更新通知包一起发送。

img

上图左侧展示了一般情况,右侧图展示了延迟确认机制中的两种情况:通过反向数据携带 ACK 和超时发送 ACK。根据 RFC 1122,ACK 的最大超时时间为 500 毫秒,但在实际实现中,最大超时时间通常为 200 毫秒。例如,在 Linux 3.4.4 中,TCP_DELACK_MAX 宏定义了该超时最大值:

/* Filename : \linux-3.4.4\include\net\tcp.h */
#define TCP_DELACK_MAX ((unsigned)(HZ/5)) /* maximal time to delay before sending an ACK */

当 Nagle 算法与延迟确认机制相互作用时,可能会导致问题。例如,发送端有一段数据要发送给接收端,数据长度不足以组成一个大包。根据 Nagle 算法,发送端会等待接收端对第一个数据包的 ACK 确认,或者等待应用层传下更多数据。然而,接收端由于延迟确认机制的作用,不会立即发送 ACK,而是等待。如果接收端在等待超时后才发送 ACK,那么发送端的第二个数据包就需要等待 200 毫秒才能发送。在 HTTP 等单向数据传输的应用中,这种情况尤为常见,可能导致时延显著增加。

为解决上述问题,Minshall 对 Nagle 算法进行了改进,相关描述可在文档中找到,Linux 内核中也已应用了这种改进。以下是改进后的代码:

/* Filename : \linux-3.4.4\net\ipv4\tcp_output.c */
/* Minshall's variant of the Nagle send check. */
static inline int tcp_minshall_check (const struct tcp_sock *tp)
{return after (tp->snd_sml, tp->snd_una) &&!after (tp->snd_sml, tp->snd_nxt);
}

该函数的实现基于以下字段的含义(RFC 793、RFC 1122):

  • tp->snd_nxt:下一个待发送的字节序号。
  • tp->snd_una:下一个待确认的字节序号。如果其值等于 tp->snd_nxt,则表示所有已发送数据均已得到确认。
  • tp->snd_sml:最近一个已发送的小包的最后一个字节序号(不一定是已确认的)。

改进的核心思想是:在判断当前包是否可发送时,仅检查最近一个小包是否已确认。如果已确认(即 tcp_minshall_check(tp) 返回假),则表示可以发送;否则延迟等待。这种改进缩短了延迟,提高了带宽利用率。

例如,对于前面提到的场景,第一个数据包是大包,因此无论其对应的 ACK 是否收到,都不会影响对第二个数据包的发送判断。由于所有小包均已确认(实际上是因为没有发送过小包),第二个数据包可以直接发送而无需等待。

传统 Nagle 算法是一种“包 - 停 - 等”协议,在未收到前一个包的确认前不会发送第二个包。而改进的 Nagle 算法是一种折中处理:如果未确认的不是小包,则可以发送第二个包。这种改进保证了在同一个往返时间(RTT)内,网络上只有一个当前连接的小包。然而,在某些特殊情况下,改进的 Nagle 算法可能会产生不利影响。例如,当 3 个数据块相继到达且后续没有其他数据时,传统 Nagle 算法仅产生一个小包,而改进的 Nagle 算法可能会产生 2 个小包(第二个小包是由于延迟等待超时产生的)。尽管如此,这种影响并不大,因此可以认为这是一种折中处理。

img

TCP 中的 Nagle 算法默认是启用的,但并不适用于所有场景。对于 telnet 或 rlogin 等远程登录应用,Nagle 算法较为适用(原本就是为此设计的),但在某些应用场景下,需要关闭该算法。例如,在处理 HTTP 持久连接(Keep-Alive)时,可能会出现奇数包和结束小包问题。具体来说,当已有奇数个包发出,并且还有一个结束小包(不是带 FIN 标志的包,而是 HTTP 请求或响应的结束包)等待发送时,就会出现该问题。以下是一个包含 3 个包和 1 个结束小包的发包示例:

img

最后一个结束小包包含了整个响应数据的最后一些数据。如果当前 HTTP 是非持久连接,则在连接关闭时,最后一个小包会立即发送,不会出现问题。然而,如果当前 HTTP 是持久连接(非 pipelining 处理,仅 HTTP 1.1 支持,并且目前部分旧版浏览器尚不支持,nginx 对 pipelining 的支持较弱),那么由于最后一个小包受到 Nagle 算法的影响,无法及时发送。具体原因是:客户端在未结束上一个请求前不会发送新的请求数据,导致无法携带 ACK 而延迟确认,进而服务器未收到客户端对上一个小包的确认,导致最后一个小包无法发送。这会导致第 n 次请求/响应未能结束,从而客户端第 n+1 次的请求数据无法发出。

img

为解决该问题,nginx 会主动关闭 Nagle 算法。以下是相关代码:

/* Filename : \linux-3.4.4\net\ipv4\tcp_output.c */
static void
ngx_http_set_keepalive (ngx_http_request_t *r)
{...if (tcp_nodelay&& clcf->tcp_nodelay&& c->tcp_nodelay == NGX_TCP_NODELAY_UNSET){ngx_log_debug0 (NGX_LOG_DEBUG_HTTP, c->log, 0, "tcp_nodelay");if (setsockopt (c->fd, IPPROTO_TCP, TCP_NODELAY,(const void *) &tcp_nodelay, sizeof (int))== -1){...}c->tcp_nodelay = NGX_TCP_NODELAY_SET;}
}

当 nginx 执行到该函数时,表示当前连接是持久连接。如果满足以下条件,则对套接字设置 TCP_NODELAY,禁止 Nagle 算法:

  1. 局部变量 tcp_nodelay 用于标记 TCP_CORK 选项,由配置指令 tcp_nopush 指定,默认为 off。
  2. clcf->tcp_nodelay 对应 TCP_NODELAY 选项的配置指令 tcp_nodelay 的配置值,默认为 1。
  3. c->tcp_nodelay 用于标记当前是否已对该套接字设置 TCP_NODELAY 选项,第一次执行时通常为 NGX_TCP_NODELAY_UNSET

因此,如果条件满足,则设置 TCP_NODELAY,使最后的响应数据能够立即发送,从而解决上述问题。


TCP/IP 详解 – Nagle 算法和 TCP_NODELAY

鱼思故渊 于 2014-04-19 14:38:38 发布

在客户端持续向服务器发送小数据时,接收到响应的时间可能会很长。这可能是由于 TCP_NODELAY 的原因。现在基本可以确定,问题出在 Nagle 算法上。

在 TCP/IP 协议中,无论发送多少数据,都需要在数据前加上协议头,同时接收方需要发送 ACK 以确认收到数据。为了尽可能利用网络带宽,TCP 希望每次都能发送足够大的数据块。Nagle 算法正是为了实现这一点,避免网络中充斥着许多小数据块。

Nagle 算法的基本定义是:任意时刻,最多只能有一个未被确认的小段。所谓“小段”,指的是小于 MSS 尺寸的数据块;所谓“未被确认”,是指一个数据块发送出去后,没有收到对方发送的 ACK 确认该数据已收到。

例如,客户端调用 socket 的 write 操作将一个 int 型数据(称为 A 块)写入网络中。由于此时连接是空闲的(即没有未被确认的小段),因此该 int 型数据会立即发送到服务器端。接着,客户端又调用 write 操作写入“/r/n”(简称 B 块),此时 A 块的 ACK 尚未返回,因此可以认为已经存在一个未被确认的小段。于是,B 块不会立即发送,而是等待 A 块的 ACK 收到(大约 40 毫秒后),B 块才会被发送。整个过程如下图所示:

这里还隐藏了一个问题:为什么 A 块数据的 ACK 需要 40 毫秒才收到?这是因为 TCP/IP 中不仅有 Nagle 算法,还有一个 ACK 延迟机制。当服务器端收到数据后,不会立即向客户端发送 ACK,而是将 ACK 的发送延迟一段时间(假设为 t),希望在 t 时间内服务器端会向客户端发送应答数据,这样 ACK 就可以和应答数据一起发送。这解释了为什么“/r/n”(B 块)总是在 A 块之后 40 毫秒才发出。

如果觉得 Nagle 算法影响了性能,可以通过 设置 TCP_NODELAY 禁用它。当然,更合理的方案是 使用一次大数据的写操作,而不是多次小数据的写操作


Linux 下 TCP 延迟确认(Delayed Ack)机制导致的时延问题分析

3035597182 2015-04-26 23:03:33

案例

在进行 Server 压力测试时发现,客户端不断向服务器发送请求,并接收服务器的响应。然而,在接收服务器响应的过程中,会出现 recv 阻塞 40 毫秒的情况。查看服务器端日志,服务器处理每个请求的时间均在 2 毫秒以内,并已将响应发送给客户端。

产生原因

产生该问题的原因是 TCP 的延迟确认(Delayed Ack)机制。服务器端调用 send 向客户端发送响应时,send 只是将数据存入 TCP 发送缓冲区。TCP 协议栈是否会发送该数据包,还需考虑 Nagle 算法。

解决办法

在 TCP 中,recv 到数据后,调用一次 setsockopt 函数,设置 TCP_QUICKACK。例如:

setsockopt (fd, IPPROTO_TCP, TCP_QUICKACK, (int*){1}, sizeof (int));

产生问题原因的详细分析

1. 延迟确认机制及作用

在《TCP/IP 详解卷一:协议》第 19 章中,详细描述了 TCP 在处理交互数据流(Interactive Data Flow)时,采用了延迟确认机制和 Nagle 算法来减少小分组数目。

2. TCP 的延迟确认机制为什么会导致 recv 延时?

仅 TCP 的延迟确认机制本身,并不会导致请求延时(因为 recv 系统调用并不需要等待 ACK 包发出去才能返回)。一般来说,只有当该机制与 Nagle 算法或拥塞控制(如慢启动或拥塞避免)混合作用时,才可能会导致时延增加。

3. 延迟确认机制与 Nagle 算法

Nagle 算法的规则如下(可参考 tcp_output.c 文件中 tcp_nagle_check 函数的注释):

1). 如果包长度达到 MSS,则允许发送。
2). 如果该包含有 FIN,则允许发送。
3). 如果设置了 TCP_NODELAY 选项,则允许发送。
4). 如果未设置 TCP_CORK 选项,并且所有已发送的包均已被确认,或者所有已发送的小数据包(包长度小于 MSS)均已被确认,则允许发送。

对于规则 4),一个 TCP 连接上最多只能有一个未被确认的小数据包。如果某个小分组的确认被延迟(如案例中的 40 毫秒),那么后续小分组的发送也会相应延迟。也就是说,延迟确认不仅影响被延迟确认的那个数据包,还会影响后续的所有应答包。

4. 关于 TCP_NODELAYTCP_CORK 选项

TCP_CORK 选项与 TCP_NODELAY 一样,用于控制 Nagle 化。

  1. 打开 TCP_NODELAY 选项,则无论数据包多么小,都会立即发送(不考虑拥塞窗口)。

  2. 如果将 TCP 连接比喻为一个管道,那么 TCP_CORK 选项的作用就像一个塞子。设置 TCP_CORK 选项,就是用塞子塞住管道;取消 TCP_CORK 选项,就是将塞子拔掉。当 TCP_CORK 选项被设置时,TCP 连接不会发送任何小包,只有当数据量达到 MSS 时,才会发送。通常在数据传输完成后,需要取消该选项,以便让不够 MSS 大小的包能够及时发送。

5. 为什么 TCP_QUICKACK 需要在每次 recv 后重新设置?

因为 TCP_QUICKACK 不是永久的,所以在每次 recv 数据后,应该重新设置。


神秘的 40 毫秒延迟与 TCP_NODELAY

posted on 2017-03-18 12:31 wajika

在编写 HTTP Server 并使用 ab 进行性能测试时,出现了一个困扰了几天的问题:神秘的 40 毫秒延迟。

1. 现象

首先,使用 ab 不加 -k 选项进行测试:

[~/dev/personal/breeze]$ /usr/sbin/ab -c 1 -n 10 http://127.0.0.1:8000/styles/shThemeRDark.css

测试结果显示,响应时间不超过 1 毫秒。然而,一旦加上 -k 选项启用 HTTP Keep-Alive,结果就变成了这样:

[~/dev/personal/breeze]$ /usr/sbin/ab -k -c 1 -n 10 http://127.0.0.1:8000/styles/shThemeRDark.css

响应时间变成了 36 毫秒。使用 strace 工具跟踪,发现是读取下一个请求之前的 epoll_wait 花了 40 毫秒才返回。这意味着要么是客户端等待了 40 毫秒才发送请求,要么是之前写入的响应数据过了 40 毫秒才到达客户端。由于 ab 作为压力测试工具不可能故意延迟发送请求,因此问题出在响应数据的延迟上。

2. 背后的原因

40 毫秒的延迟是由于 TCP 协议中的 Nagle 算法和延迟确认机制共同作用的结果。Nagle 算法旨在提高带宽利用率,通过合并小的 TCP 包来避免过多的小报文浪费带宽。如果开启了该算法(默认情况下),协议栈会累积数据,直到满足以下条件之一才发送:

  1. 积累的数据量达到最大的 TCP Segment Size。
  2. 收到一个 ACK。

TCP 延迟确认机制也是为了类似的目的而设计的,它会延迟 ACK 包的发送,以便协议栈有机会合并多个 ACK,从而提高网络性能。如果一个 TCP 连接的一端启用了 Nagle 算法,而另一端启用了延迟确认机制,且发送的数据包较小,则可能会出现以下情况:发送端等待接收端对上一个数据包的 ACK 才发送当前数据包,而接收端则延迟了 ACK 的发送。因此,当前数据包也会被延迟。延迟确认机制有一个超时机制,默认超时时间为 40 毫秒。

3. 为什么只有 Write-Write-Read 时才会出问题

维基百科上有一段伪代码介绍了 Nagle 算法:

if there is new data to sendif the window size >= MSS and available data is >= MSSsend complete MSS segment nowelseif there is unconfirmed data still in the pipeenqueue data in the buffer until an acknowledge is receivedelsesend data immediatelyend ifend if
end if

从伪代码可以看出,当待发送的数据小于 MSS 时(外层的 else 分支),还需判断是否有未确认的数据。只有当管道中存在未确认的数据时,才会将数据缓存起来等待 ACK。因此,发送端发送的第一个 write 不会被缓存,而是立即发送(进入内层的 else 分支)。此时,接收端收到数据后,由于期待更多数据,不会立即发送 ACK。根据延迟确认机制,ACK 会被延迟。当发送端发送第二个数据包时,由于队列中存在未确认的数据包,会进入内层 if 的 then 分支,该数据包会被缓存起来。此时,发送端等待接收端的 ACK,而接收端则延迟发送 ACK,直到超时(40 毫秒),ACK 才被发送回去,发送端缓存的数据包才会被发送。

如果采用 write-read-write-read 模式,则不会出现问题。因为第一个 write 不会被缓存,会立即到达接收端。接收端处理完后发送结果,并将 ACK 与数据一起发送回去,无需延迟确认,因此不会导致任何问题。

4. 解决方案

4.1 优化协议

连续 write 小数据包然后 read 是一种不好的网络编程模式。这种连续的 write 应该在应用层合并成一次 write。然而,如果程序难以进行这样的优化,可以采用以下方法。

4.2 开启 TCP_NODELAY

简单来说,该选项的作用是禁用 Nagle 算法。禁用后,就不会出现由该算法引起的问题。在 UNIX C 中,可以使用 setsockopt 来实现:

static void _set_tcp_nodelay (int fd)
{int enable = 1;setsockopt (fd, IPPROTO_TCP, TCP_NODELAY, (void*)&enable, sizeof (enable));
}

在 Java 中,Socket 对象有一个 setTcpNoDelay 方法,直接设置为 true 即可。据我所知,Nginx 默认开启了这个选项,这为我提供了一些安慰:既然 Nginx 都这么做了,我也可以先默认开启 TCP_NODELAY


Linux 下 TCP 延迟确认(Delayed Ack)机制导致的时延问题分析

潘安群 修改于 2017-06-19 19:29:42

案例一

同事随手写了一个压力测试程序,其逻辑为:每秒钟先连续发送 N 个 132 字节的包,然后连续接收 N 个由后台服务回显回来的 132 字节包。代码简化如下:

char sndBuf [132];
char rcvBuf [132];
while (1) {for (int i = 0; i < N; i++) {send (fd, sndBuf, sizeof (sndBuf), 0);...}for (int i = 0; i < N; i++) {recv (fd, rcvBuf, sizeof (rcvBuf), 0);...}sleep (1);
}

在实际测试中发现,当 N 大于等于 3 时,从第 2 秒开始,每次第三个 recv 调用总会阻塞 40 毫秒左右。然而,在分析服务器端日志时,发现所有请求在服务器端的处理时间均在 2 毫秒以下。

定位过程如下:首先尝试使用 strace 跟踪客户端进程,但奇怪的是,一旦 strace attach 上进程,所有收发都正常,不会出现阻塞现象。退出 strace 后,问题重现。经同事提醒,可能是 strace 改变了程序或系统的某些设置。于是使用 tcpdump 抓包分析,发现服务器端在回显应答包后,客户端并没有立即对该数据进行 ACK 确认,而是等待了近 40 毫秒后才确认。经过 Google 并查阅《TCP/IP 详解卷一:协议》,得知这是 TCP 的延迟确认(Delayed Ack)机制。

解决办法如下:在 recv 系统调用后,调用一次 setsockopt 函数,设置 TCP_QUICKACK。最终代码如下:

char sndBuf [132];
char rcvBuf [132];
while (1) {for (int i = 0; i < N; i++) {send (fd, sndBuf, 132, 0);...}for (int i = 0; i < N; i++) {recv (fd, rcvBuf, 132, 0);setsockopt (fd, IPPROTO_TCP, TCP_QUICKACK, (int []){1}, sizeof (int));}sleep (1);
}

案例二

在营销平台内存化 CDKEY 版本进行性能测试时,发现请求时延分布异常:90% 的请求时延在 2 毫秒以内,而 10% 的请求时延始终在 38-42 毫秒之间。这是一个非常有规律的数字:40 毫秒。由于之前经历过案例一,因此猜测这也是由延迟确认机制引起的时延问题。经过简单的抓包验证后,通过设置 TCP_QUICKACK 选项,成功解决了时延问题。

延迟确认机制

在《TCP/IP 详解卷一:协议》第 19 章中,详细描述了 TCP 在处理交互数据流(Interactive Data Flow)时,采用了延迟确认机制和 Nagle 算法来减少小分组数目。

1. 为什么 TCP 延迟确认会导致延迟?

实际上,仅延迟确认机制本身并不会导致请求延迟(最初以为必须等到 ACK 包发出去,recv 系统调用才会返回)。一般来说,只有当该机制与 Nagle 算法或拥塞控制(如慢启动或拥塞避免)混合作用时,才可能会导致时延增加。接下来详细分析它们是如何相互作用的。

延迟确认与 Nagle 算法

Nagle 算法的规则如下(可参考 tcp_output.c 文件中 tcp_nagle_check 函数的注释):

  1. 如果包长度达到 MSS,则允许发送。
  2. 如果该包含有 FIN,则允许发送。
  3. 如果设置了 TCP_NODELAY 选项,则允许发送。
  4. 如果未设置 TCP_CORK 选项,并且所有已发送的包均已被确认,或者所有已发送的小数据包(包长度小于 MSS)均已被确认,则允许发送。

对于规则 4),一个 TCP 连接上最多只能有一个未被确认的小数据包。如果某个小分组的确认被延迟(如案例中的 40 毫秒),那么后续小分组的发送也会相应延迟。也就是说,延迟确认影响的并不是被延迟确认的那个数据包,而是后续的应答包。

延迟确认与拥塞控制

我们先利用 TCP_NODELAY 选项关闭 Nagle 算法,再来分析延迟确认与 TCP 拥塞控制是如何相互作用的。

慢启动

TCP 的发送方维护一个拥塞窗口(cwnd)。TCP 连接建立时,该值初始化为 1 个报文段。每收到一个 ACK,该值就增加 1 个报文段。发送方取拥塞窗口与通告窗口(与滑动窗口机制对应)中的最小值作为发送上限。发送方开始发送 1 个报文段,收到 ACK 后,cwnd 从 1 增加到 2,即可以发送 2 个报文段。当收到这两个报文段的 ACK 后,cwnd 就增加为 4,即指数增长。例如,在第一个 RTT 内,发送一个包,并收到其 ACK,cwnd 增加 1。在第二个 RTT 内,可以发送两个包,并收到对应的两个 ACK,则 cwnd 每收到一个 ACK 就增加 1,最终变为 4,实现了指数增长。

在 Linux 实现中,并不是每收到一个 ACK 包,cwnd 就增加 1。如果在收到 ACK 时,并没有其他数据包在等待被 ACK,则不增加。

2. 为什么是 40 毫秒?这个时间能否调整?

在 RedHat 的官方文档中,有如下说明:某些应用在发送小报文时,可能会因为 TCP 的延迟确认机制而产生延迟。其默认值为 40 毫秒。可以通过修改 tcp_delack_min,调整系统级别的最小延迟确认时间。例如:

# echo 1 > /proc/sys/net/ipv4/tcp_delack_min

这表示期望设置最小的延迟确认超时时间为 1 毫秒。然而,在 Slackware 和 SUSE 系统下,均未找到该选项,这意味着在这些系统中,40 毫秒的最小值无法通过配置调整。

linux-2.6.39.1/net/tcp.h 中,有如下宏定义:

#define TCP_DELACK_MIN ((unsigned)(HZ/25)) /* minimal time to delay before sending an ACK */

Linux 内核每隔固定周期会发出 timer interrupt (IRQ 0),HZ 用于定义每秒的 timer interrupts 次数。例如,HZ 为 1000,表示每秒有 1000 次 timer interrupts。HZ 可在编译内核时设置。在现有服务器上运行的系统中,HZ 值均为 250。

因此,最小的延迟确认时间为 40 毫秒。TCP 连接的延迟确认时间通常初始化为最小值 40 毫秒,随后根据连接的重传超时时间(RTO)、上次收到数据包与本次接收数据包的时间间隔等参数进行调整。具体的调整算法可以参考 linux-2.6.39.1/net/ipv4/tcp_input.c 中的 tcp_event_data_recv 函数。

3. 为什么 TCP_QUICKACK 需要在每次调用 recv 后重新设置?

在 man 7 tcp 中,有如下说明:

TCP_QUICKACK
Enable quickack mode if set or disable quickack mode if cleared. In quickack mode, acks are sent immediately, rather than delayed if needed in accordance to normal TCP operation. This flag is not permanent, it only enables a switch to or from quickack mode. Subsequent operation of the TCP protocol will once again enter/leave quickack mode depending on internal protocol processing and factors such as delayed ack timeouts occurring and data transfer. This option should not be used in code intended to be portable.

手册中明确指出 TCP_QUICKACK 不是永久的。其具体实现如下:

case TCP_QUICKACK:if (!val) {icsk->icsk_ack.pingpong = 1;} else {icsk->icsk_ack.pingpong = 0;if ((1 << sk->sk_state) &(TCPF_ESTABLISHED | TCPF_CLOSE_WAIT) &&inet_csk_ack_scheduled (sk)) {icsk->icsk_ack.pending |= ICSK_ACK_PUSHED;tcp_cleanup_rbuf (sk, 1);if (!(val & 1))icsk->icsk_ack.pingpong = 1;}}break;

Linux 下的 socket 有一个 pingpong 属性,用于表明当前连接是否为交互数据流。如果其值为 1,则表示为交互数据流,会使用延迟确认机制。然而,pingpong 的值是动态变化的。例如,当 TCP 连接要发送一个数据包时,会执行如下函数(linux-2.6.39.1/net/ipv4/tcp_output.c,Line 156):

/* Congestion state accounting after a packet has been sent. */
static void tcp_event_data_sent (struct tcp_sock *tp, struct sk_buff *skb, struct sock *sk)
{...tp->lsndtime = now;/* If it is a reply for ato after last received* packet, enter pingpong mode.*/if ((u32)(now - icsk->icsk_ack.lrcvtime) < icsk->icsk_ack.ato)icsk->icsk_ack.pingpong = 1;
}

最后两行代码说明:如果当前时间与最近一次接收数据包的时间间隔小于计算的延迟确认超时时间,则重新进入交互数据流模式。也可以这样理解:延迟确认机制被确认有效时,会自动进入交互式。

通过以上分析可知,TCP_QUICKACK 选项需要在每次调用 recv 后重新设置

4. 为什么不是所有包都延迟确认?

在 TCP 实现中,使用 tcp_in_quickack_mode 函数(linux-2.6.39.1/net/ipv4/tcp_input.c,Line 197)来判断是否需要立即发送 ACK。其函数实现如下:

/* Send ACKs quickly, if "quick" count is not exhausted* and the session is not interactive.*/
static inline int tcp_in_quickack_mode (const struct sock *sk)
{const struct inet_connection_sock *icsk = inet_csk (sk);return icsk->icsk_ack.quick && !icsk->icsk_ack.pingpong;
}

要满足以下两个条件才能进入 quickack 模式:

  1. pingpong 被设置为 0。
  2. 快速确认数(quick)必须为非 0。

关于 pingpong 的值,前面已有描述。而 quick 属性的代码注释为:scheduled number of quick acks,即快速确认的包数量。每次进入 quickack 模式时,quick 被初始化为接收窗口除以 2 倍 MSS 值(linux-2.6.39.1/net/ipv4/tcp_input.c,Line 174)。每次发送一个 ACK 包时,quick 就减 1。

5. 关于 TCP_CORK 选项

TCP_CORK 选项与 TCP_NODELAY 一样,用于控制 Nagle 化。

  1. 打开 TCP_NODELAY 选项,则意味着无论数据包多么小,都会立即发送(不考虑拥塞窗口)。
  2. 如果将 TCP 连接比喻为一个管道,那么 TCP_CORK 选项的作用就像一个塞子。设置 TCP_CORK 选项,就是用塞子塞住管道;取消 TCP_CORK 选项,就是将塞子拔掉。例如,以下代码:
int on = 1;
setsockopt (sockfd, SOL_TCP, TCP_CORK, &on, sizeof (on)); // 设置 TCP_CORK
write (sockfd, ...);    // 例如,HTTP 头
sendfile (sockfd, ...); // 例如,HTTP 正文
on = 0;
setsockopt (sockfd, SOL_TCP, TCP_CORK, &on, sizeof (on)); // 取消 TCP_CORK

TCP_CORK 选项被设置时,TCP 连接不会发送任何小包,只有当数据量达到 MSS 时,才会发送。当数据传输完成时,通常需要取消该选项,以便让不够 MSS 大小的包能够及时发送。如果应用程序确定可以一起发送多个数据集合(例如 HTTP 响应的头和正文),建议设置 TCP_CORK 选项,这样在这些数据之间不存在延迟。为了提升性能和吞吐量,Web Server 和文件服务器通常会使用该选项。

著名的高性能 Web 服务器 Nginx,在使用 sendfile 模式时,可以通过将 nginx.conf 配置文件中的 tcp_nopush 配置为 on 来打开 TCP_CORK 选项。TCP_NOPUSHTCP_CORK 功能类似,只不过 NOPUSH 是 BSD 下的实现,而 CORK 是 Linux 下的实现。此外,Nginx 为了减少系统调用,追求性能极致,针对短连接(一般传送完数据后,立即主动关闭连接,对于 Keep-Alive 的 HTTP 持久连接除外),程序并不通过 setsockopt 调用取消 TCP_CORK 选项,因为关闭连接会自动取消 TCP_CORK 选项,并将剩余数据发送出去。


套接字 socket 选项 TCP_NODELAYTCP_CORKTCP_QUICKACK

冬生0 已于 2022-03-02 14:15:28 修改

一、简介

  • TCP_NODELAY:关闭 Nagle 算法,控制数据的发送。Nagle 算法规定,如果包大于 MSS(Max Segment Size)或含有 FIN,则立即发送;否则放入缓冲区,等待已发送的包被确认后再发送。该算法可以减少网络中的小包数量,降低 IP 头部在网络中的比重,从而提升网络性能。
  • TCP_CORK:设置后不会发送任何小包(小于 MSS),除非超时 200 毫秒。
  • TCP_QUICKACK(自 Linux 2.4.4 起可用):设置后会立即发送确认 ACK,而不是延迟发送 ACK。如果未开启,则延迟确认,使得协议有机会合并 ACK,提高网络利用率。默认超时确认时间为 40 毫秒,该系统值可配置。

二、测试

测试方法:服务器 IP 为 192.168.x.5,客户端 IP 为 192.168.x.7。服务器端接受客户端连接,但不做任何处理;客户端与服务器建立连接后,连续发送 10 个字符。

  1. 服务器端关闭 QUICKACK,客户端关闭 NODELAY(即会延迟发送):服务器关闭了 QUICKACK,因此会等待超时后再确认,或等到有数据发送时顺带发送确认 ACK。但由于服务器没有数据发送,因此会等待 40 毫秒超时。由于客户端没有收到服务器端的确认 ACK,且 nodelay 关闭(即开启了延迟发送),因此会等待收到服务器的 ACK 后才继续发送剩余数据。

  2. 服务器端开启 QUICKACK,客户端开启 NODELAY(立即发送):服务器端收到一个数据后立即发送确认给客户端。

  3. 服务器端关闭 QUICKACK,客户端开启 NODELAY(立即发送):客户端不等待确认,连续发送 10 个字符。服务器端在 10 毫秒后发送 ACK。疑问:为什么是 10 毫秒后发送 ACK?

  4. 服务器端开启 QUICKACK,客户端关闭 NODELAY(即会延迟发送):服务器收到数据后立即发送确认给客户端,客户端将剩余的 9 个字符合并发送。

  5. 服务器端关闭 QUICKACK,客户端开启 TCP_CORK:客户端发送 10 个字符,远小于 MSS,因此会等待 200 毫秒超时后发送。

三、总结

延迟与带宽利用率不可兼得。如果需要降低延迟,则应开启 QUICKACKNODELAY;否则关闭两者,以提高带宽利用率。

四、附 demo

Server 代码

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <netdb.h>
#include <errno.h>
#include <syslog.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <signal.h>
#include <sys/wait.h>#define BUFLEN 1024int main (int argc, char** argv)
{int listenfd, connfd;socklen_t clilen;struct sockaddr_in cliaddr, servaddr;listenfd = socket (AF_INET, SOCK_STREAM, 0);memset (&cliaddr, 0, sizeof (cliaddr));memset (&servaddr, 0, sizeof (servaddr));servaddr.sin_family = AF_INET;servaddr.sin_addr.s_addr = htonl (INADDR_ANY);servaddr.sin_port = htons (9000);bind (listenfd, (struct sockaddr*)&servaddr, sizeof (servaddr));listen (listenfd, 128);printf ("Listening on port 9000...\n");clilen = sizeof (cliaddr);connfd = accept (listenfd, (struct sockaddr*)&cliaddr, &clilen);if (connfd <= 0) {perror ("accept error\n");exit (1);}int quick = 1;setsockopt (connfd, IPPROTO_TCP, TCP_QUICKACK, &quick, sizeof (int));sleep (10);close (connfd);return 0;
}

Client 代码

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <errno.h>
#include <syslog.h>
#include <netinet/tcp.h>
#include <sys/socket.h>#define BUFLEN 1024int main (int argc, char** argv)
{int sockfd;struct sockaddr_in servaddr;if (argc != 2) {printf ("Usage: %s <server_addr>\n", argv[0]);exit (1);}sockfd = socket (AF_INET, SOCK_STREAM, 0);memset (&servaddr, 0, sizeof (servaddr));servaddr.sin_family = AF_INET;servaddr.sin_port = htons (9000);if (inet_pton (AF_INET, argv[1], &servaddr.sin_addr) <= 0) {printf ("Invalid address: %s\n", argv[1]);return 1;}if (connect (sockfd, (struct sockaddr*)&servaddr, sizeof (servaddr)) != 0) {printf ("Failed to connect to server %s, errno: %d\n", argv[1], errno);return 1;}int rc = 0;int nodelay = 1;rc = setsockopt (sockfd, IPPROTO_TCP, TCP_NODELAY, &nodelay, sizeof (int));int cork = 1;rc = setsockopt (sockfd, IPPROTO_TCP, TCP_CORK, &cork, sizeof (int));printf ("Connected and options set.\n");char sendline [2] = {0};sendline [0] = 'a';int n = 0;for (int i = 0; i < 10; i++)n += write (sockfd, sendline, strlen (sendline));printf ("%d bytes sent\n", n);if (read (sockfd, sendline, 2) <= 0) {printf ("Server terminated\n");exit (1);}return 0;
}

【计算机网络】Socket 的 TCP_NODELAY 选项与 Nagle 算法

morris131 于 2024-01-19 09:44:36 发布

TCP_NODELAY 是一个套接字选项,用于控制 TCP 套接字的延迟行为。当 TCP_NODELAY 选项被启用(即设置为 true)时,会禁用 Nagle 算法,从而实现 TCP 套接字的无延迟传输。这意味着每次发送数据时都会立即发送,不会等待缓冲区的填充或等待确认。

TCP_NODELAY 选项的演示

Socket 服务端代码

package com.morris.socket;import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;/*** Socket 服务端,演示 TCP_NODELAY** @see java.net.SocketOptions*/
public class TcpNoDelayServerDemo {public static void main (String [] args) throws IOException {ServerSocket serverSocket = new ServerSocket (8090);while (true) {Socket client = serverSocket.accept ();System.out.println ("Accepted connection from: " + client.getRemoteSocketAddress ());new Thread (() -> {try {InputStream inputStream = client.getInputStream ();byte [] buffer = new byte [1024];while (true) {int len = inputStream.read (buffer);if (len == -1) {System.out.println ("Connection closed by: " + client.getRemoteSocketAddress ());inputStream.close ();client.close ();break;} else {System.out.print ("Received: " + new String (buffer, 0, len));}}} catch (IOException e) {e.printStackTrace ();}}).start ();}}
}

Socket 客户端代码

package com.morris.socket;import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
import java.nio.charset.StandardCharsets;/*** Socket 客户端,演示 TCP_NODELAY** @see java.net.SocketOptions*/
public class TcpNoDelayClientDemo {public static void main (String [] args) throws IOException {Socket socket = new Socket ("192.168.1.11", 8099);socket.setTcpNoDelay (true);OutputStream outputStream = socket.getOutputStream ();for (int i = 0; i < 10; i++) {outputStream.write ((i + "\n").getBytes (StandardCharsets.UTF_8));outputStream.flush ();}outputStream.close ();socket.close ();}
}

默认情况下(即 TcpNoDelayfalse,开启 Nagle 算法)的运行结果:

Accepted connection from: /192.168.1.10:4760
Received: 0
Received: 1
2
3
4
5
6
7
8
9

客户端发送了 10 个报文,而服务端只收到了 2 个。客户端在发送报文的过程中进行了合并。

以下是 tcpdump 抓到的服务端的报文,从中也可以清晰地看到,客户端往服务端发送了 2 个数据报文(Flags 为 PSH):

$ tcpdump tcp port 8099
19:37:53.914138 IP 192.168.1.10.4760 > 192.168.1.11.8099: Flags [S], seq 3193331081, win 64240, options [mss 1452,nop,wscale 8,nop,nop,sackOK], length 0
19:37:53.914182 IP 192.168.1.11.8099 > 192.168.1.10.4760: Flags [S.], seq 2693275116, ack 3193331082, win 29200, options [mss 1460,nop,nop,sackOK,nop,wscale 7], length 0
19:37:53.925761 IP 192.168.1.10.4760 > 192.168.1.11.8099: Flags [.], ack 1, win 516, length 0
19:37:53.927131 IP 192.168.1.10.4760 > 192.168.1.11.8099: Flags [P.], seq 1:3, ack 1, win 516, length 2
19:37:53.927172 IP 192.168.1.11.8099 > 192.168.1.10.4760: Flags [.], ack 3, win 229, length 0
19:37:53.927977 IP 192.168.1.10.4760 > 192.168.1.11.8099: Flags [FP.], seq 3:21, ack 1, win 516, length 18
19:37:53.928363 IP 192.168.1.11.8099 > 192.168.1.10.4760: Flags [F.], seq 1, ack 22, win 229, length 0
19:37:53.940733 IP 192.168.1.10.4760 > 192.168.1.11.8099: Flags [.], ack 2, win 516, length 0

TcpNoDelay 设置为 true 后(即禁用 Nagle 算法)的运行结果:

Accepted connection from: /192.168.1.10:3307
Received: 0
Received: 1
Received: 2
Received: 3
4
Received: 5
6
Received: 7
Received: 8
9

客户端发送了 10 个报文,而服务端收到了 7 个。客户端在发送报文的过程中允许小的数据包立即发送,尽量不合并。

以下是 tcpdump 抓到的服务端的报文,从中也可以清晰地看到,客户端往服务端发送了 10 个数据报文(Flags 为 PSH):

$ tcpdump tcp port 8099
19:39:07.953002 IP 192.168.1.10.3307 > 192.168.1.11.8099: Flags [S], seq 2087425409, win 64240, options [mss 1452,nop,wscale 8,nop,nop,sackOK], length 0
19:39:07.953071 IP 192.168.1.11.8099 > 192.168.1.10.3307: Flags [S.], seq 1383068300, ack 2087425410, win 29200, options [mss 1460,nop,nop,sackOK,nop,wscale 7], length 0
19:39:07.961935 IP 192.168.1.10.3307 > 192.168.1.11.8099: Flags [.], ack 1, win 516, length 0
19:39:07.963806 IP 192.168.1.10.3307 > 192.168.1.11.8099: Flags [P.], seq 1:3, ack 1, win 516, length 2
19:39:07.963808 IP 192.168.1.10.3307 > 192.168.1.11.8099: Flags [P.], seq 3:5, ack 1, win 516, length 2
19:39:07.963858 IP 192.168.1.11.8099 > 192.168.1.10.3307: Flags [.], ack 3, win 229, length 0
19:39:07.963867 IP 192.168.1.11.8099 > 192.168.1.10.3307: Flags [.], ack 5, win 229, length 0
19:39:07.963903 IP 192.168.1.10.3307 > 192.168.1.11.8099: Flags [P.], seq 5:7, ack 1, win 516, length 2
19:39:07.963925 IP 192.168.1.11.8099 > 192.168.1.10.3307: Flags [.], ack 7, win 229, length 0
19:39:07.963904 IP 192.168.1.10.3307 > 192.168.1.11.8099: Flags [P.], seq 7:9, ack 1, win 516, length 2
19:39:07.963933 IP 192.168.1.11.8099 > 192.168.1.10.3307: Flags [.], ack 9, win 229, length 0
19:39:07.963904 IP 192.168.1.10.3307 > 192.168.1.11.8099: Flags [P.], seq 9:11, ack 1, win 516, length 2
19:39:07.963939 IP 192.168.1.11.8099 > 192.168.1.10.3307: Flags [.], ack 11, win 229, length 0
19:39:07.964809 IP 192.168.1.10.3307 > 192.168.1.11.8099: Flags [P.], seq 11:13, ack 1, win 516, length 2
19:39:07.964810 IP 192.168.1.10.3307 > 192.168.1.11.8099: Flags [P.], seq 13:15, ack 1, win 516, length 2
19:39:07.964811 IP 192.168.1.10.3307 > 192.168.1.11.8099: Flags [P.], seq 15:17, ack 1, win 516, length 2
19:39:07.964812 IP 192.168.1.10.3307 > 192.168.1.11.8099: Flags [P.], seq 17:19, ack 1, win 516, length 2
19:39:07.964813 IP 192.168.1.10.3307 > 192.168.1.11.8099: Flags [P.], seq 19:21, ack 1, win 516, length 2
19:39:07.964861 IP 192.168.1.11.8099 > 192.168.1.10.3307: Flags [.], ack 13, win 229, length 0
19:39:07.964868 IP 192.168.1.11.8099 > 192.168.1.10.3307: Flags [.], ack 15, win 229, length 0
19:39:07.964871 IP 192.168.1.11.8099 > 192.168.1.10.3307: Flags [.], ack 17, win 229, length 0
19:39:07.964874 IP 192.168.1.11.8099 > 192.168.1.10.3307: Flags [.], ack 19, win 229, length 0
19:39:07.964878 IP 192.168.1.11.8099 > 192.168.1.10.3307: Flags [.], ack 21, win 229, length 0
19:39:07.964923 IP 192.168.1.10.3307 > 192.168.1.11.8099: Flags [F.], seq 21, ack 1, win 516, length 0
19:39:07.965196 IP 192.168.1.11.8099 > 192.168.1.10.3307: Flags [F.], seq 1, ack 22, win 229, length 0
19:39:07.974934 IP 192.168.1.10.3307 > 192.168.1.11.8099: Flags [.], ack 2, win 516, length 0

Nagle 算法

TCP 协议是网络编程中最重要的协议之一。TCP 协议将上层的数据附上 TCP 报头等信息,封装成一个个报文段(segment),然后交由下层网络层去处理。TCP 协议定义了 TCP 报文段的结构,如下图所示:

img

可以看出,TCP 每个报文段的首部大小至少是 20 字节。因此,如果用户数据仅为 1 字节,再加上网络层 IP 包头 20 字节,则整个 IP 数据包的大小为 41 字节。那么整个 IP 数据包的负荷率为 1/41。这显然是不划算的,会降低网络的传输效率。当网络中充斥着这种 IP 数据包时,整个网络几乎都在传输一些无用的包头信息,这种问题被称为小包问题。特别是在 Telnet 协议中,当用户远程登录到一个主机时,他的每一次键盘敲击实际上都会产生一个携带用户数据量小的数据包,这是典型的小包问题。

为了解决这种问题,出现了 Nagle 算法。该算法由 John Nagle 为解决实际过程中的小包问题而发明。其思想是将多个即将发送的小段用户数据缓存并合并成一个大段数据,一次性发送出去。特别地,只要发送者还没有收到前一次发送的 TCP 报文段的 ACK(即连接中还存在未回执 ACK 的 TCP 报文段),发送方就应该一直缓存数据,直到数据达到可以发送的大小,然后再统一合并到一起发送出去。如果收到上一次发送的 TCP 报文段的 ACK,则立即发送缓存的数据。

与之相呼应的还有一个网络优化机制,称为 TCP 延迟确认。这是针对接收方的机制。由于 ACK 包属于有效数据较少的小包,因此延迟确认机制会导致接收方将多个收到数据包的 ACK 打包成一个回复包返回给发送方。这样可以避免过多的只包含 ACK 的 TCP 报文段导致网络额外开销(前面提到的小包问题)。延迟确认机制有一个超时机制,即当收到每一个 TCP 报文段后,如果该 TCP 报文段的 ACK 超过一定时间还未发送,就会启动超时机制,立刻将该 ACK 发送出去。因此,延迟确认机制可能会带来 500 毫秒的 ACK 延迟确认时间。

延迟确认机制和 Nagle 算法几乎是在同一时期提出的,但由不同的团队提出。这两种机制在某种程度上确实对网络传输进行了优化,在通常的协议栈实现中,这两种机制是默认开启的。

然而,这两种机制结合起来时,可能会产生一些负面的影响,导致应用程序的性能下降。

TCP_NODELAY 使用注意事项

Nagle 算法应用于发送端。简而言之,对于发送端而言:

  • 当第一次发送数据时,不需要等待,即使是 1 字节的小包也会立即发送。
  • 后续发送数据时,需要累积数据包,直到满足以下条件之一才会继续发送数据:
    • 数据包达到最大段大小 MSS。
    • 接收端收到之前数据包的确认 ACK。

MTU(Maximum Transmission Unit)是指在网络通信中能够传输的最大数据包大小。MSS(Maximum Segment Size)通常小于等于 MTU,因为数据包中还包含 TCP/IP 协议头部的额外开销。一般情况下,IPv4 网络中的 MTU 大小为 1500 字节,而 IPv6 网络中的 MTU 大小为 1280 字节。

可以通过网卡信息查看 MTU 的大小:

$ ifconfig
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
inet 172.24.104.61  netmask 255.255.240.0  broadcast 172.24.111.255
inet6 fe80::215:5dff:fe6f:5634  prefixlen 64  scopeid 0x20<link>
ether 00:15:5d:6f:56:34  txqueuelen 1000  (Ethernet)
RX packets 213794  bytes 293979318 (293.9 MB)
RX errors 0  dropped 0  overruns 0  frame 0
TX packets 106646  bytes 8514354 (8.5 MB)
TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

禁用 Nagle 算法可以降低延迟,适用于实时性要求较高的应用,如实时音视频传输或实时游戏。然而,禁用 Nagle 算法可能会导致更频繁的网络传输,增加网络开销。因此,在传输大量小数据包的情况下,禁用 Nagle 算法可能会降低网络效率。

TCP_NODELAY 选项通常在创建套接字后设置。对于已经建立的连接,可能需要重新创建套接字并设置选项。

需要根据具体的需求和场景来决定是否使用 TCP_NODELAY 选项,权衡延迟和网络效率之间的关系。


TCP Nagle 算法:网络优化的双刃剑

Linux编程用C 发布于 2024-02-23 21:13・IP 属地四川

在网络通信的世界里,TCP(传输控制协议)是确保数据可靠传输的基石。然而,为了进一步提高网络效率,TCP 引入了一系列优化算法,其中 Nagle 算法就是其中之一。今天,我们将探讨 Nagle 算法的原理、作用 以及它在现代网络中的应用。

Nagle 算法的起源

Nagle 算法由 John Nagle 在 1984 年提出,旨在解决 TCP/IP 网络中的小分组问题。在广域网(WAN)中,频繁发送小数据包可能会导致网络拥塞,因为每个小数据包都需要单独的确认(ACK),这增加了网络的负载。Nagle 算法通过合并小数据包来减少这种拥塞。

Nagle 算法的工作原理

Nagle 算法的核心思想是:在一个 TCP 连接上,最多只能有一个未被确认的小数据包(小于 MSS,即最大报文段大小)在网络中。算法的逻辑如下:

  • 如果有数据要发送,且可用窗口大小大于等于 MSS,或者数据包含 FIN 标志(表示连接即将关闭),则立即发送数据。
  • 如果有未确认的数据,且新数据可以与未确认的数据合并(即新数据的大小加上未确认数据的大小小于 MSS),则将新数据添加到未确认数据的末尾。
  • 如果新数据不能与未确认数据合并,且未确认数据已收到 ACK,那么发送未确认数据和新数据。
  • 如果新数据不能与未确认数据合并,且未确认数据未收到 ACK,那么等待 ACK 到达后再发送新数据。

Nagle 算法的优势与劣势

优势

  • 减少网络拥塞:通过合并小数据包,减少了网络中的数据包数量,降低了拥塞的可能性。
  • 提高网络效率:在低速网络中,Nagle 算法可以显著提高传输效率。

劣势

  • 增加延迟:在交互式应用中,Nagle 算法可能导致显著的延迟,因为它等待 ACK 或合并数据包。
  • 不适用于实时应用:对于需要快速响应的应用(如在线游戏、实时视频流),Nagle 算法可能会影响用户体验。

如何调整 Nagle 算法

在某些情况下,我们可能需要关闭 Nagle 算法以减少延迟。这可以通过设置 TCP_NODELAY 套接字选项来实现。在编程时,可以通过以下代码片段来禁用 Nagle 算法:

int noDelay = 1;
setsockopt (socket, IPPROTO_TCP, TCP_NODELAY, (char *)&noDelay, sizeof (noDelay));

结语

Nagle 算法是 TCP 协议中的一个重要优化。它在提高网络效率的同时,也可能带来延迟问题。在设计网络应用时,开发者需要根据应用的特性和网络环境来决定是否使用 Nagle 算法。通过合理配置,我们可以在保证数据传输可靠性的同时,优化用户体验。


via:

  • TCP_NODELAY 详解 - C/C+±Chinaunix
    http://bbs.chinaunix.net/thread-3767363-1-1.html

  • TCP/IP 详解 --nagle 算法和 TCP_NODELAY_nagle 和 nodelay-CSDN 博客
    https://blog.csdn.net/yusiguyuan/article/details/24109845

  • Linux 下 TCP 延迟确认(Delayed Ack)机制导致的时延问题分析_产生原因 (延迟确认机制和 Nagle 算法及_行者无疆_新浪博客
    https://blog.sina.com.cn/s/blog_b4ef897e0102vgch.html

  • 神秘的 40 毫秒延迟与 TCP_NODELAY - wajika - 博客园
    https://www.cnblogs.com/wajika/p/6573028.html

  • Linux 下 TCP 延迟确认 (Delayed Ack) 机制导致的时延问题分析 - 腾讯云开发者社区 - 腾讯云
    https://cloud.tencent.com/developer/article/1004356

  • 套接字 socket 选项 TCP_NODELAY、TCP_CORK 与 TCP_QUICKACK_socket nodelay-CSDN 博客
    https://blog.csdn.net/WEI_GW2012/article/details/123213084

  • 【计算机网络】Socket 的 TCP_NODELAY 选项与 Nagle 算法_tcp nodelay-CSDN 博客
    https://blog.csdn.net/u022812849/article/details/135689740

  • TCP Nagle 算法:网络优化的双刃剑 - 知乎
    https://zhuanlan.zhihu.com/p/683650018

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/900449.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

玄机-应急响应-webshell查杀

题目要求&#xff1a; 要求获取四个flag webshell查杀&#xff1a; 常见的webshell&#xff1a; PHP: eval(), system(), exec(), shell_exec(), passthru(), assert(), base64_decode() ASP: Execute(), Eval(), CreateObject() JSP: Runtime.getRuntime().exec() websh…

docker存储卷及dockers容器源码部署httpd

1. COW机制 Docker镜像由多个只读层叠加而成,启动容器时,Docker会加载只读镜像层并在镜像栈顶部添加一个读写层。 如果运行中的容器修改了现有的一个已经存在的文件,那么该文件将会从读写层下面的只读层复制到读写层,该文件的只读版本依然存在,只是已经被读写层中该文件…

PyTorch中卷积层torch.nn.Conv2d

在 PyTorch 中&#xff0c;卷积层主要由 torch.nn.Conv1d、torch.nn.Conv2d 和 torch.nn.Conv3d 实现&#xff0c;分别对应一维、二维和三维卷积操作。以下是详细说明&#xff1a; 1. 二维卷积 (Conv2d) - 最常用 import torch.nn as nn# 基本参数 conv nn.Conv2d(in_channe…

从 ZStack 获取物理机与云主机信息并导出 Excel 文件

文章目录 从 ZStack 获取物理机与云主机信息并导出 Excel 文件环境zstack 官网客户端封装讲解 获取物理机信息讲解 获取云主机信息并关联物理机讲解 导出数据到 Excel 文件讲解 运行主程序讲解 总结最终文档效果完整代码 从 ZStack 获取物理机与云主机信息并导出 Excel 文件 在…

5.好事多磨 -- TCP网络连接Ⅱ

前言 第4章节通过回声服务示例讲解了TCP服务器端/客户端的实现方法。但这仅是从编程角度的学习&#xff0c;我们尚未详细讨论TCP的工作原理。因此&#xff0c;将详细讲解TCP中必要的理论知识&#xff0c;还将给出第4章节客户端问题的解决方案。 一、回声客户端完美实现 第4章…

sql server数据库可疑修复

sql server数据库可疑修复 从上图可以看到数据库nchrdb显示可疑&#xff0c;导致原因为NC系统在增加公共薪资项目的时候&#xff0c;扩展字段报错了&#xff0c;第一次遇到这种情况&#xff0c;折腾了很久终于解决&#xff0c;记下解决方案&#xff1a; 1&#xff0c;将SQL数据…

Flutter之页面布局二

目录&#xff1a; 1、列表布局1.1、基础列表1.2、水平滑动的列表1.3、网格列表1.3、不同列表项的列表1.4、包含间隔的列表1.6、长列表 2、滚动2.1、浮动的顶栏2.2、平衡错位滚动 1、列表布局 1.1、基础列表 import package:flutter/material.dart;void main() > runApp(con…

ARM------硬件程序开发

硬件程序开发流程 相关硬件的工作原理 理解硬件的工作原理&#xff0c;明确硬件的功能和用途。 硬件连接 将硬件设备正确连接到开发板上。 编写程序 根据硬件功能编写相应的程序代码。 调试验证 通过调试工具验证程序的正确性&#xff0c;确保硬件功能正常。 控制LED的…

《QT从基础到进阶·七十四》Qt+C++开发一个python编译器,能够编写,运行python程序改进版

1、概述 源码放在文章末尾 根据上一篇文章回顾下利用QtC实现了一个简易的python编译器&#xff0c;类似pycharm或vsCode这样的编译器&#xff0c;该python编译器目前实现了如下功能&#xff1a; &#xff08;1&#xff09;支持编写python程序 &#xff08;2&#xff09;编写代…

Winform MQTT客户端连接方式

项目中使用到Winform的数据转发服务&#xff0c;所以记录下使用到的方法。 一.创建单例模板 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks;namespace ConsoleApp.Scripts {public class SingleTon&…

Windows强制删除任何你想删除的文件和文件夹

Windows强制删除任何你想删除的文件和文件夹 本教程适用于 Windows 10/11 系统&#xff0c;工具和命令均经过验证。 为什么删除会失败&#xff1f; 权限不足&#xff1a;文件或文件夹可能需要管理员权限才能删除。文件被占用&#xff1a;某个程序正在使用目标文件&#xff0c…

Jmeter如何使用MD5进行加密?

在软件测试中&#xff0c;使用 JMeter 对数据进行 MD5 加密是一项常见需求&#xff0c;尤其是在模拟用户登录等涉及密码加密的场景时。下面详细介绍在 JMeter 里运用 MD5 加密的具体步骤。 1. 添加 BeanShell 预处理器 JMeter 本身没有直接的 MD5 加密功能&#xff0c;但可以…

4-c语言中的数据类型

一.C 语⾔中的常量 1.生活中的数据 整数&#xff1a; 100,200,300,400,500 小数: 11.11 22.22 33.33 字母&#xff1a; a&#xff0c;b&#xff0c;c&#xff0c;d A&#xff0c;B&#xff0c;C&#xff0c;D 在 C 语⾔中我们把字⺟叫做字符. 字符⽤单引号引⽤。例如A’ 单词…

中钧科技通过数字赋能,编织“数字互联网”助力数字化进程!

时间飞逝转眼间2025年已过去四分之一&#xff0c;作为一名95后回顾当下的生活&#xff0c;忍不住感慨10年和现在的对比。发现现在的手机支付、网上挂号、APP打车、在线学习、网络订餐、线上协同办公都以逐渐成为人们生活、工作的常态。也正是在这样的常态背景下&#xff0c;加快…

AI重塑云基础设施,亚马逊云科技打造AI定制版IaaS“样板房”

AI正在彻底重塑云基础设施。 IDC最新《2025年IDC MarketScape&#xff1a;全球公有云基础设施即服务&#xff08;IaaS&#xff09;报告》显示&#xff0c;AI正在通过多种方式重塑云基础设施&#xff0c;公有云IaaS有望继续保持快速增长&#xff0c;预计2025年全球IaaS的整体规…

高效深度学习lecture01

lecture01 零样本学习(Zero-Shot Learning, ZSL)&#xff1a; 模型可以在没有见过某种特定任务或类别的训练样本的情况下&#xff0c;直接完成对应的任务 利用知识迁移 模型在一个任务上训练时学到的知识&#xff0c;能够迁移到其他任务上比如&#xff0c;模型知道“狗”和“…

使用 iPerf 测试内网两台机器之间的传输速度

在现代网络管理中&#xff0c;确保内部网络&#xff08;内网&#xff09;的高效运行是至关重要的。为了评估和优化网络性能&#xff0c;我们需要一种可靠的方法来测试内网中不同设备间的传输速率。iPerf 作为一款广泛使用的工具&#xff0c;能够帮助我们准确测量两个节点之间的…

视频设备轨迹回放平台EasyCVR如何搭建公共娱乐场所远程视频监控系统

一、背景介绍 由于KTV、酒吧、足疗店等服务场所人员流动频繁、环境复杂&#xff0c;一直是治安管理的重点区域。为有效打击 “黄赌毒”、打架斗殴、寻衅滋事等违法犯罪的活动&#xff0c;打造安全有序的娱乐消费环境&#xff0c;我国相关部门将加大对这类场所的清查与管控力度…

vue进度条组件

<div class"global-mask" v-if"isProgress"><div class"contentBox"><div class"progresstitie">数据加载中请稍后</div><el-progress class"progressStyle" :color"customColor" tex…

Css:如何解决绝对定位子元素内容被父级元素overflow:hidden属性剪裁

一、问题描述 今天小伙伴提了一个bug&#xff0c;在点击列表项的“…”按钮应该出现的悬浮菜单显示不完整&#xff1a; 二、问题排查 一般这种问题&#xff0c;是由于悬浮菜单采用的是绝对定位&#xff0c;而父级采用了overflow:hidden属性。但需要注意的是&#xff0c;这里的…