各位老铁们好,相信很多人对深入解析Linux环境中TCP延迟确认机制都不是特别的了解,因此呢,今天就来为大家分享下关于深入解析Linux环境中TCP延迟确认机制以及的问题知识,还望可以帮助大家,解决大家的一些困惑,下面一起来看看吧!
第2:章rcvBuf[132];
3: 同时(1) {
4: for (int i=0; i N; i++){
5: 发送(fd, sndBuf, sizeof(sndBuf), 0);
6:…
7:}
8: for (int i=0; i N; i++) {
9: 接收(fd, rcvBuf, sizeof(rcvBuf), 0);
10:…
11:}
12: 睡眠(1);
13:}在实际测试中发现,当N大于等于3时,第二秒之后,每第三次recv调用总会阻塞40毫秒左右。然而,在分析服务器端日志时,发现所有请求都被服务器端拦截。处理时间小于2ms。
当时的具体定位过程是这样的:我首先尝试使用strace来跟踪客户端进程,但奇怪的是:一旦strace附加了进程,所有发送和接收都正常,不会出现阻塞。一旦退出strace,问题又出现了。一位同事提醒我,strace可能改变了程序或系统中的某些内容(这个问题目前还不清楚),所以我使用tcpdump抓包并进行分析。我发现服务器后端返回响应包后,客户端没有响应。数据没有立即ACK,而是等待了近40毫秒才被确认。经过谷歌并查了《TCP/IP详解卷一:协议》,才知道这是TCP的Delayed Ack机制。
解决办法如下:在recv系统调用后,调用一次setsockopt函数设置TCP_QUICKACK。最终代码如下:
1: 字符sndBuf[132];
第2:章rcvBuf[132];
3: 同时(1) {
4: for (int i=0; i N; i++) {
5: 发送(fd, sdBuf, 132, 0);
6:…
7:}
8: for (int i=0; i N; i++) {
9: 接收(fd, rcvBuf, 132, 0);
10: setsockopt(fd, IPPROTO_TCP, TCP_QUICKACK, (int[]){1}, sizeof(int));
11:}
12: 睡眠(1);
13: }案例二:在营销平台基于内存的CDKEY版本做性能测试时,发现请求耗时分布异常:90%的请求在2ms以内,10%左右的耗时始终在38-42 毫秒之间。这是一个非常规律的数字:40ms。因为之前经历过案例1,所以猜测延迟问题也是延迟确认机制造成的。经过简单的抓包验证,通过设置TCP_QUICKACK选项解决了延迟问题。
延迟确认机制
其原理在《TCP/IP详解卷一:协议》的第19章有详细描述:TCP是处理交互式数据流(即Interactive Data Flow,与Bulk Data Flow,即块数据流不同)。典型的交互数据流如telnet、rlogin等),采用Delayed Ack机制和Nagle算法来减少小团体的数量。
这两种机制的原理书中已经讲得很清楚了,这里不再赘述。本文后续部分将通过分析Linux下TCP/IP的实现来讲解TCP的延迟确认机制。
1. TCP延迟确认为什么会造成延迟?
事实上,只有延迟确认机制才不会造成请求延迟(一开始我以为recv系统调用要等到ACK包发送完毕后才会返回)。一般来说,只有当该机制与Nagle算法或拥塞控制(慢启动或拥塞避免)混合时,时间消耗可能会增加。让我们仔细看看他们是如何互动的:
延迟确认与Nagle算法我们先看一下Nagle算法的规则(参考tcp_output.c文件中的tcp_nagle_check函数注释):
1)如果数据包长度达到MSS,则允许发送;
2)如果包中包含FIN,则允许发送;
3)如果设置了TCP_NODELAY选项,则允许发送;
4)当TCP_CORK选项没有设置时,如果所有传出的数据包都被确认,或者所有传出的小数据包(数据包长度小于MSS)都被确认,则允许发送。
对于规则4),要求一条TCP连接上最多只能有一个未确认的小数据包,并且在该数据包的确认到达之前不能发送其他小数据包。如果某个小包的确认延迟(本例为40ms),那么后续小包的发送也会相应延迟。换句话说,延迟确认不会影响被延迟确认的数据包,而是影响后续的响应包。
1 00:44:37.878027 IP 172.25.38.135.44792 172.25.81.16.9877: S 3512052379:3512052379(0) 赢58402 00:44:3 7.878045 IP 172.25.81.16.9877 172.25.38.135.44792: S 3581620571:3581620571(0) ack 3512052380 赢57923 00:44:37.8790 80 IP 172.25.38.135.44792 172.25。 81.16.9877:ack 1 胜46
.
4 00:44:38.885325 IP 172.25.38.135.44792 172.25.81.16.9877: P 1321:1453(132) ack 1321 赢86
5 00:44:38.886037 IP 172.25.81.16.9877 172.25.38.135.44792: P 1321:1453(132) ack 1453 赢2310
6 00:44:38.887174 IP 172.25.38.135.44792 172.25.81.16.9877: P 1453:2641(1188) ack 1453 赢102
7 00:44:38.887888 IP 172.25.81.16.9877 172.25.38.135.44792: P 1453:2476(1023) ack 2641 赢2904
8 00:44:38.925270 IP 172.25.38.135.44792 172.25.81.16.9877:ack 2476 胜118
9 00:44:38.925276 IP 172.25.81.16.9877 172.25.38.135.44792: P 2476:2641(165) ack 2641 赢2904
10 00:44:38.926328 IP 172.25.38.135.44792 172.25.81.16.9877: ack 2641 win 134 从上面的tcpdump 抓包分析来看,第8 个包有延迟,第9 个包的数据在服务器端(172 .25. 81.16)虽然已经放到了TCP 发送缓冲区中(应用层调用的send已经返回),根据Nagle算法,第9个包需要等到第7个包(小于MSS)的ACK到达才可以可以发出去。
延迟确认与拥塞控制我们先使用TCP_NODELAY选项关闭Nagle算法,然后分析延迟确认和TCP拥塞控制是如何交互的。
慢启动:TCP的发送方维护一个拥塞窗口,记为cwnd。当TCP连接建立时,该值被初始化为1段,每收到一个ACK,该值就增加1段。发送方以拥塞窗口和通知窗口(对应滑动窗口机制)之间的最小值作为发送上限(拥塞窗口是发送方使用的流控,通知窗口是发送方使用的流控)接收器)。发送方开始发送一个报文段。收到ACK后,cwnd从1增加到2,即可以发送2个报文段。收到这两个报文段的ACK后,cwnd增加到4。即指数增长:例如,在第一个RTT中,如果发送一个数据包并收到其ACK,则cwnd增加1,而在第二个RTT中,cwnd增加1 RTT,可以发送两个数据包,收到对应的两个ACK,那么cwnd就会增加,每收到一个ACK,就加1,最后变成4,实现指数增长。
在Linux实现中,cwnd并不是每收到一个ACK包就加1。如果收到ACK时没有其他数据包等待ACK,则不会增加。
我使用案例1的测试代码,在实际测试中,cwnd从初始值2开始,最终维持3个消息段的值。 tcpdump结果如下:
1 16:46:14.288604 IP 172.16.1.3.1913 172.16.1.2.20001: S 1324697951:1324697951(0) 赢58402 16:46:14.289 172.16 .1.3.1913 172.16。 1.2.20001:ack 1胜1460
.
4 16:46:15.327493 IP 172.16.1.3.1913 172.16.1.2.20001: P 1321:1453(132) ack 1321 赢4140
5 16:46:15.329749 IP 172.16.1.2.20001 172.16.1.3.1913: P 1321:1453(132) ack 1453 赢2904
6 16:46:15.330001 IP 172.16.1.3.1913 172.16.1.2.20001: P 1453:2641(1188) ack 1453 赢4140
7 16:46:15.333629 IP 172.16.1.2.20001 172.16.1.3.1913: P 1453:1585(132) ack 2641 赢3498
8 16:46:15.337629 IP 172.16.1.2.20001 172.16.1.3.1913: P 1585:1717(132) ack 2641 赢3498
9 16:46:15.340035 IP 172.16.1.2.20001 172.16.1.3.1913: P 1717:1849(132) ack 2641 赢3498
10 16:46:15.371416 IP 172.16.1.3.1913 172.16.1.2.20001:ack 1849 胜4140
11 16:46:15.371461 IP 172.16.1.2.20001 172.16.1.3.1913: P 1849:2641(792) ack 2641 赢3498
12 16:46:15.371581 IP 172.16.1.3.1913 172.16.1.2.20001: ack 2641 win 4536 上表中的数据包是当TCP_NODELAY 设置且cwnd 增长到3 时的数据包。第7、8、9 个发送后,它们被限制在拥塞窗口大小,即使TCP 缓冲区中有数据此时可以发送的数据包,不能继续发送,即第11个数据包必须等到第10个数据包到达后才能发送,而第10个数据包显然有40ms的延迟。
注:TCP连接的详细信息,如当前拥塞窗口大小、MSS等,可以通过getsockopt(man 7 tcp)的TCP_INFO选项查看。
2、为什么是40ms?这个时间能不能调整呢?首先,在redhat的官方文档中,有如下描述:
当一些应用程序发送小数据包时,由于TCP的Delayed Ack机制,可能会出现一定的延迟。其值默认为40ms。可以通过修改tcp_delack_min来调整系统层面的最小延迟确认时间。例如:
# echo 1 /proc/sys/net/ipv4/tcp_delack_min
即期望将最小延迟确认超时设置为1ms。
但在slackware和suse系统下并没有找到这个选项,也就是说这两个系统下无法通过配置来调整40ms的最小值。
linux-2.6.39.1/net/tcp.h下有如下宏定义:
1: /* 发送ACK 之前的最小延迟时间*/
2: #define TCP_DELACK_MIN ((无符号)(HZ/25))
注意:Linux 内核会每隔固定周期发出一次定时器中断(IRQ 0)。 HZ 用于定义每秒有多少个定时器中断。例如HZ为1000,表示每秒有1000个定时器中断。 HZ可以在编译内核时设置。对于在我们现有服务器上运行的系统,HZ 值为250。
可以看出,最小延迟确认时间为40ms。
TCP连接的延迟确认时间一般初始化为最小值40ms,然后根据连接的重传超时(RTO)以及上次接收到的数据包与本次的时间间隔等参数不断调整。具体调整算法请参考linux-2.6.39.1/net/ipv4/tcp_input.c第564行的tcp_event_data_recv函数。
3、为什么TCP_QUICKACK需要在每次调用recv后重新设置?在man 7 tcp中,有如下描述:
TCP_QUICKACK 如果设置则启用快速确认模式,如果清除则禁用快速确认模式。在快速确认模式下,确认会立即发送,而不是根据正常TCP 操作在需要时延迟发送。该标志不是永久的,它仅允许切换到快速确认模式或从快速确认模式切换。 TCP 协议的后续操作将根据内部协议处理以及延迟确认超时发生和数据传输等因素再次进入/离开快速确认模式。此选项不应在可移植的代码中使用。
手册明确指出TCP_QUICKACK不是永久的。那么它的具体实现是怎样的呢? TCP_QUICKACK选项的实现请参考setsockopt函数:
1: 案例TCP_QUICKACK:
2: 如果(!val){
3: icsk-icsk_ack.pingpong=1;
4: } 其他{
5: icsk-icsk_ack.pingpong=0;
6: if ((1 sk-sk_state)
7:(TCPF_ESTABLISHED | TCPF_CLOSE_WAIT)
8: inet_csk_ack_scheduled(sk)){
9: icsk-icsk_ack.pending |=ICSK_ACK_PUSHED;
10: tcp_cleanup_rbuf(sk, 1);
11: 如果(!(val 1))
12: icsk-icsk_ack.pingpong=1;
13:}
14:}
15: 断;事实上,Linux下的socket有一个pingpong属性来指示当前链接是否是交互式数据流。如果其值为1,则表示是交互式数据流,将采用延迟确认机制。但乒乓球的价值会动态变化。例如,当一个TCP链路想要发送一个数据包时,它将执行以下函数(linux-2.6.39.1/net/ipv4/tcp_output.c,第156行):
1: /* 数据包发送后的拥塞状态统计。 */
2:静态无效tcp_event_data_sent(结构tcp_sock * tp,
3: 结构sk_buff *skb, 结构sock *sk)
4: {
5:
6: tp-lsndtime=现在;
7: /* 如果是上次收到后的回复
8: * 数据包,进入乒乓模式。
9:*/
10: if ((u32)(现在- icsk-icsk_ack.lrcvtime) icsk-icsk_ack.ato)
11: icsk-icsk_ack.pingpong=1;
12: }最后两行代码说明:如果当前时间与最后收到数据包的时间间隔小于计算出的延迟确认超时时间,则重新进入交互数据流模式。也可以这样理解:当延迟确认机制被确认有效后,会自动进入交互模式。
从上面的分析可以看出,TCP_QUICKACK选项是需要在每次调用recv后重新设置的。4、为什么不是所有包都延迟确认?TCP实现中,通过函数tcp_in_quickack_mode(linux-2.6.39.1/net/ipv4/tcp_input.c,第197行)来判断是否需要ACK立即发送。其功能实现如下:
1: /* 如果“快速”计数未耗尽,则快速发送ACK
2: * 并且会话不是交互式的。
3:*/
4: 静态内联int tcp_in_quickack_mode(const struct sock *sk)
5: {
6: const struct inet_connection_sock *icsk=inet_csk(sk);
7:返回icsk-icsk_ack.quick!icsk-icsk_ack.pingpong;
8: } 必须满足两个条件才能被视为快速模式:
1.乒乓球设置为0。
2. 快速确认(quick)的数量必须非零。
乒乓球的价值前面已描述过。 Quick属性代码中的注释为:scheduled number of fast acks,即快速确认的数据包数量。每次进入quickack模式,quick初始化为接收窗口除以2倍MSS值(linux-2.6.39.1/net/ipv4/tcp_input.c,第174行),每次发送ACK包,quick为减1。
5、关于TCP_CORK选项TCP_CORK 选项与TCP_NODELAY 相同,控制Nagleization。
1.打开TCP_NODELAY选项,这意味着无论数据包有多小,都会立即发送(不管拥塞窗口如何)。
2. 如果将TCP 连接比作管道,则TCP_CORK 选项的作用就像插头。设置TCP_CORK选项意味着用塞子堵塞管道,取消TCP_CORK选项意味着移除塞子。例如,以下代码:
1: int=1;
2: setsockopt(sockfd, SOL_TCP, TCP_CORK, on, sizeof(on)); //设置TCP_CORK
3: 写(sockfd,); //例如,http标头
4: sendfile(sockfd,); //例如,http正文
5: 开=0;
【深入解析Linux环境中TCP延迟确认机制】相关文章:
2.米颠拜石
3.王羲之临池学书
8.郑板桥轶事十则
用户评论
我一直对Linux系统是怎么工作的挺好奇的,这个机制讲清楚了?
有12位网友表示赞同!
看了一下标题,感觉可以深入了解一下Linux网络优化方面的知识
有14位网友表示赞同!
我记得以前学过延迟确认,这个标题引发了我之前学习的相关记忆
有16位网友表示赞同!
想了解更多关于TCP延迟确认机制的内容,这篇文章应该能提供一些答案
有9位网友表示赞同!
Linux系统真是太强大了吧,它是怎么实现这种智能的网络通信机制的?
有12位网友表示赞同!
最近在学习网络编程,这个机制看起来很有用处,可以增加代码效率吗?
有11位网友表示赞同!
我一直觉得TCP/IP协议很复杂,期待这篇文章能解释得简单易懂
有14位网友表示赞同!
学习Linux系统总感觉是趟一条未知的河流,希望能从这篇文章中获取一些指引
有20位网友表示赞同!
对于网络延迟问题一直比较关注,希望这篇分析能够有所帮助
有20位网友表示赞同!
看文章标题感觉很专业,对Linux内核机制有深入了解的人应该能受益匪浅
有16位网友表示赞同!
这方面知识对我来说很有挑战,想尝试一下深入阅读学习
有16位网友表示赞同!
我平时主要使用Windows系统,看看 Linux 的网络优化方式会不会给我启发
有6位网友表示赞同!
希望文章能讲解清楚各种延迟确认机制的优缺点比较
有14位网友表示赞同!
如果能够结合一些案例分析,相信会更易于理解和消化
有12位网友表示赞同!
学习Linux的过程中,网络协议知识是不可或缺的一部分,期待这篇文章能给我带来帮助
有20位网友表示赞同!
文章是否会涉及到不同的操作系统对延迟确认机制的实现区别?
有18位网友表示赞同!
对于网络优化问题一直有很强的兴趣,希望从这篇文章中获取一些新的见解
有14位网友表示赞同!
感觉Linux系统的底层机制确实非常巧妙,期待进一步探索
有16位网友表示赞同!
想了解一下延迟确认机制在实际应用中的效果和意义
有7位网友表示赞同!
学习 Linux 系统是一个长期目标,希望能够不断积累知识和经验
有6位网友表示赞同!