TCP接收到重叠数据(overlap)后的行为解析-附带一个有关Delay ACK和超时重传的优化_网卡overlap 是什么

本文写于国庆长假第一天早晨,正好碰到今天热线值班,终于不用假期出去添堵折腾了(14年来[自离开高中],从来没有过过一个完整的可以休息的假期!预定了N次在家的假期,失败了N次,谎称过几次加班,但也不是长计,因为必须要离开家,实在也没地方去,我觉得此生假期难自由了,然而如果公司硬性规定必须在家值班,那也是不啻一种上好的方法啊!哈哈),作文一篇,以表达对假期自由的感慨!

最开始的事实

首先,我们先明确Linux TCP实现中的3个事实,这些叫做“事实”的论述是颇为主观的,它们只是我积累下来的所谓“事实”,这些事实即:

1.Linux作为数据接收端的时候,默认不会Delay ACK,直到...

直到Linux接收端发现了处在交互模式,即pingpong设置为1的时候,才会开启Delay ACK。你试下在Linux上用wget去一个web服务器下载一个文件并抓包,你会发现对接收到的数据的ACK并没有Delay。这也是与Windows的Delay ACK不同的地方,详情参见《
TCP之Delay ACK在Linux和Windows上实现的异同-Linux的自适应ACK

2.按序接收的快速路径中,TCP接收端如果收到已经收到的数据,会直接丢弃

这个事实可以引申到部分重叠模式的数据接收。举个例子,如果收到一个按序的长度为10的skb,携带序列号10-20,那么当再次收到长度为15,携带序列号15-25的skb时,会将已经收到的15-20丢弃,仅仅保存21-25的新数据。不光如此,当收到这样的部分重叠数据时,会立即回复一个ACK,不管当前是不是处在交互行为的Delay ACK模式下。

3.乱序接收的慢速SACK路径中,TCP接收端如果收到已经收到的数据,会用新数据覆盖老数据

这显然是合理的,因为在乱序模式下,意味着可能出现了丢包或者多径延时,这将使TCP离开正常的状态,此时不管是发送端还是接收端所有的努力都是趋向于将TCP带回到按序处理的快速路径,接收端会尽可能快速回复ACK以反馈信息,发送端会重传可能丢失的数据,由于接收端并不对乱序数据做任何存储上的保证,因此其假设直到最终填洞完毕,之前的数据都可能无效!但是确实一定是这样吗?请跟着在下面实验中找答案。

        以上3个事实看起来并不是显然的,因此需要涉及一些用例来验证。能够探测TCP行为的轻量级工具,我选packetdrill,因此我们就用这个工具来验证吧。

packetdrill验证事实

现在我们来用packetdrill验证一下上述的事实。由于需要验证事实2和事实3,因此需要打印出接收端收到的数据,当前的packetdrill并不支持这个功能,所以需要修改它的代码。

1.为packetdrill构造的数据段增加可以用来区别的payload信息并打印

为此,我们需要修改tcp_packet.c的new_tcp_packet函数,增加简单的payload:

struct packet *new_tcp_packet(int address_family,
                               enum direction_t direction,
                               enum ip_ecn_t ecn,
                               const char *flags,
                               u32 start_sequence,
                               u16 tcp_payload_bytes,
                               u32 ack_sequence,
                               s32 window,
                               const struct tcp_options *tcp_options,
                               char **error)
{
        ...
        const int ip_bytes =
                 ip_header_bytes + tcp_header_bytes + tcp_payload_bytes;
        // 增加一个static变量,每次递增,以可打印的ASCII码为起始。
        static int pad = 70;
        ...
        packet = packet_new(ip_bytes);
        memset(packet->buffer, 0, ip_bytes);
        if (tcp_payload_bytes) {
                memset(packet->buffer + ip_header_bytes + tcp_header_bytes, pad++, tcp_payload_bytes);
                memset(packet->buffer + ip_header_bytes + tcp_header_bytes + tcp_payload_bytes -1, 0, 0);
                printf("send:n%sn-end send-n", packet->buffer + ip_header_bytes + tcp_header_bytes);
        }
        ...
}

2.在系统调用read之后,打印事实上收到的payload信息

为此,我们修改run_system_call.c的syscall_read函数,打印输出:

static int syscall_read(struct state *state, struct syscall_spec *syscall,
                        struct expression_list *args, char **error)
{
        ...
        result = read(live_fd, buf, count);
        printf("payload:n%sn", buf);
        ...
}

修改后直接make之,很容易便生成了新的packetdrill。接下来我们将用这个新编译的packetdrill进行一切实验。首先我为事实1和事实2设计了以下的脚本,执行之并抓包:

0.000 socket(..., SOCK_STREAM, IPPROTO_TCP) = 3
0.000 setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0
0.000 bind(3, ..., ...) = 0
0.000 listen(3, 1) = 0

0.000 < S 0:0(0) win 32792 <mss 1000, sackOK, nop, nop, nop, wscale 7>
0.000 > S. 0:0(0) ack 1 <...>  

0.000 < . 1:1(0) ack 1 win 257

0.000 accept(3, ..., ...) = 4   

// 以下开启Delay ACK
0.100 < . 1:11(10) ack 1 win 257
0.100 write(4, ..., 20) = 20
0.100 < . 11:11(0) ack 21 win 257

0.100 < . 11:21(10) ack 21 win 257
1.000 read(4, ..., 20) = 20

// 由于已经Delay ACK了一次,其Oneshot特性关闭了自身,以下再次触发开启Delay ACK的序列
1.100 < . 21:31(10) ack 21 win 257
1.100 write(4, ..., 20) = 20
1.100 < . 31:31(0) ack 41 win 257

1.100 < . 26:41(15) ack 41 win 257  //这一次发送一个overlap的数据段,部分重叠旧数据,部分包含新数据
2.000 read(4, ..., 20) = 20

// Receiver ACKs all data.
10.000 < . 1:1(0) ack 6001 win 257 

在分析抓包之前,我们先看一下打印输出:

TCP接收到重叠数据(overlap)后的行为解析-附带一个有关Delay ACK和超时重传的优化_网卡overlap 是什么

这个打印输出基本上与事实2所描述的是一致的,数据段26:31之前已经被收到,已经被ACK过了,因此接收了其包含的部分新数据31:41,旧数据26:31直接丢弃!然后再看下抓包:

TCP接收到重叠数据(overlap)后的行为解析-附带一个有关Delay ACK和超时重传的优化_网卡overlap 是什么

我们可以看到,当webcache端口也就是8080接收到第一个段,即1:11的时候,立即回复了ACK,这印证了事实1,之后在收到11:21的时候,却Delay了ACK,至于说这个序列是怎么开启Delay ACK的,不属于本文范畴,我在《
TCP之Delay ACK在Linux和Windows上实现的异同-Linux的自适应ACK》中已经说的比较明确了,现在的重点是,请注意在第一批数据,即1:21被read之后,我触发了一个与1:21数据同样的序列,唯一不同的是最后的26:41属于重叠模式,这一次的ACK是立即回复的,关于深层次的原因,本文不讨论,可以去看RFC或者Linux内核源码关于overlap和乱序的部分,本文仅仅给出一些感官上的体验。

        最后,我们构造一个场景来验证一下事实3。我构造了以下的脚本:

0.000 socket(..., SOCK_STREAM, IPPROTO_TCP) = 3
0.000 setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0
0.000 bind(3, ..., ...) = 0
0.000 listen(3, 1) = 0

0.000 < S 0:0(0) win 32792 <mss 1000, sackOK, nop, nop, nop, wscale 7>
0.000 > S. 0:0(0) ack 1 <...>  

0.000 < . 1:1(0) ack 1 win 257

0.000 accept(3, ..., ...) = 4   

0.100 < . 21:31(10) ack 1 win 257  // 模拟1:21丢失
0.100 < . 6:21(15) ack 1 win 257  // 模拟重传6:21,这些数据从未被接收过
0.100 < . 16:31(15) ack 1 win 257 // 模拟重传16:31,这些数据部分(21:31)被接收过
0.100 < . 1:11(10) ack 1 win 257  // 模拟重传1:11,这些数据部分(6:11)被接收过,部分(1:6)是新数据
1.000 read(4, ..., 30) = 30

// Receiver ACKs all data.
10.000 < . 1:1(0) ack 6001 win 257 

这是一个多么乱的场景啊,按照事实3,还是给个图示比较方便看:

TCP接收到重叠数据(overlap)后的行为解析-附带一个有关Delay ACK和超时重传的优化_网卡overlap 是什么

脚本的运行输出结果是:

TCP接收到重叠数据(overlap)后的行为解析-附带一个有关Delay ACK和超时重传的优化_网卡overlap 是什么

看似与事实3的论述是一致的。

        猛一看,与按序接收的事实2相反,这里在乱序接收时,确实是以最后收到的数据为准的。但是注意接收16:31时,16:21这段数据并没有被覆盖,事实也确实如此,这是为什么呢?除此之外,还有一个疑问可能待解析,那就是,是不是只要收到被ACK的数据,就会丢弃新数据(显然这是毫无疑问的),只要收到没有被ACK的数据,就会新数据覆盖老数据呢?在数据没有被ACK的情况下,丝毫不理会是按序接收还是乱序接收,统统覆盖老数据呢??

        为了答疑这个疑问,我又构造了一个脚本,这次我用Delay ACK来模拟积压未被ACK的顺序接收的数据,脚本如下:

0.000 socket(..., SOCK_STREAM, IPPROTO_TCP) = 3
0.000 setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0
0.000 bind(3, ..., ...) = 0
0.000 listen(3, 1) = 0

0.000 < S 0:0(0) win 32792 <mss 1000, sackOK, nop, nop, nop, wscale 7>
0.000 > S. 0:0(0) ack 1 <...>  

0.000 < . 1:1(0) ack 1 win 257

0.000 accept(3, ..., ...) = 4   

// 以下两句开启交互pingpong行为,进入Delay ACK模式
0.100 < . 1:11(10) ack 1 win 257
0.100 write(4, ..., 20) = 20
0.100 < . 11:11(0) ack 21 win 257

// 首先传输一个10字节的段
0.100 < . 11:21(10) ack 21 win 257
// 然后部分重叠地推进数据接收
0.100 < . 16:26(10) ack 21 win 257
1.000 read(4, ..., 25) = 25

// Receiver ACKs all data.
10.000 < . 1:1(0) ack 6001 win 257

以下是抓包以及输出结果的分析,首先看抓包:

TCP接收到重叠数据(overlap)后的行为解析-附带一个有关Delay ACK和超时重传的优化_网卡overlap 是什么

输出结果如下:

TCP接收到重叠数据(overlap)后的行为解析-附带一个有关Delay ACK和超时重传的优化_网卡overlap 是什么

关于这个,在RFC 793里面有一段论述:

  When a segment overlaps other already received segments we reconstruct
  the segment to contain just the new data, and adjust the header fields
  to be consistent.


通过以上的实验,我们能不能把事实2和事实3总结成更加通用的说法呢?诚然,按照顺序接收路径和乱序填洞路径来分类重叠数据接收行为是不错的,但是我们发现了一个“黑天鹅”,即在非顺序接收16:31数据时,并没有覆盖掉16:21段的数据,这意味着事实3是有问题的,通过以上的实验,我们将结论总结成以下的说法:

1.如果新接收到的数据在后面与之前接收到的数据前面重叠,则新数据据覆盖旧数据;
2.如果新接收到的数据在前面与之前接收到的数据后面重叠,则丢弃新数据重叠的部分。

我构造了本节最后一个脚本来印证结论:

0.000 socket(..., SOCK_STREAM, IPPROTO_TCP) = 3
0.000 setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0
0.000 bind(3, ..., ...) = 0
0.000 listen(3, 1) = 0

0.000 < S 0:0(0) win 32792 <mss 1000, sackOK, nop, nop, nop, wscale 7>
0.000 > S. 0:0(0) ack 1 <...>  

0.000 < . 1:1(0) ack 1 win 257

0.000 accept(3, ..., ...) = 4   

0.100 < . 21:31(10) ack 1 win 257
0.100 < . 26:31(5) ack 1 win 257
0.100 < . 21:26(5) ack 1 win 257
0.100 < . 1:16(15) ack 1 win 257  
0.100 < . 11:26(15) ack 1 win 257   
//0.100 < . 11:21(10) ack 1 win 257  
1.000 read(4, ..., 30) = 30

// Receiver ACKs all data.
10.000 < . 1:1(0) ack 6001 win 257 

无注释,无输出,请自行运行。

        好了,认识完了这些事实,我们接下来考虑一个小小的优化,这个涉及到了超时重传。

超时重传的Delay ACK

现在我们注意一下上述packetdrill脚本中的第一个Delay ACK,即收到数据11:21时候的ACK,请问接收端怎么知道这个数据段是原始数据段还是超时重发的数据段呢?答案是没有办法知道!我们稍微改一下这个脚本,让11:21段的发送“显得像是在超时重传”,即将其时间往后移一秒!

0.000 socket(..., SOCK_STREAM, IPPROTO_TCP) = 3
0.000 setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0
0.000 bind(3, ..., ...) = 0
0.000 listen(3, 1) = 0

0.000 < S 0:0(0) win 32792 <mss 1000, sackOK, nop, nop, nop, wscale 7>
0.000 > S. 0:0(0) ack 1 <...>

0.000 < . 1:1(0) ack 1 win 257

0.000 accept(3, ..., ...) = 4

// 以下开启Delay ACK
0.100 < . 1:11(10) ack 1 win 257
0.100 write(4, ..., 20) = 20
0.100 < . 11:11(0) ack 21 win 257

//原始数据段丢失 0.100 < . 11:21(10) ack 21 win 257 // 注意时间戳
//第一次重传数据段丢失 0.080 < . 11:21(10) ack 21 win 257 // 注意时间戳,此例中RTO约80ms
// ...时间戳指数退避
1.100 < . 11:21(10) ack 21 win 257  // 延迟1秒发送,看起来像是原始数据段丢了,这里是指数退避好几次后成功重发的

10.000 < . 1:1(0) ack 6001 win 257

结果我就不贴图了,请自行验证!

        事实上,这个“看起来像是”确实很像!因为数据段的中途丢失不会给任何信息给接收端(这里不谈路由器的ECN标志),所以即便是已经指数退避很久的重传段被接收端收到,如果此时依然在交互模式中,那么还是会Delay ACK的!这就会带来至少40ms的延时!

优化超时重传时40ms的Delay ACK延迟

虽然接收端并不知道一个数据段是原始的还是超时重传的,但是发送端知道!因此在发生超时重传的时候,顺带发送一个字节已经被ACK的数据是有益的。

        与上述简单的packetdrill脚本所造场景不同的是,真实场景中发送端往往可一次性发送的数据很多,也就是说窗口比较大,该优化的作用比较有限。由于一个TCP段所携带的数据必须是序列号连续的,因此仅仅针对UNA(尚未确认的第一个字节)后第一个TCP数据段的超时重传可以使用“多发一个字节以促使接收端收到重叠乱序数据而立即回复ACK”这个优化,修改的代码非常简单,仅仅将UNA后重传的第一个数据段的TCP头部的seq域的值减去1即可!因为这1个字节与已经接收的最后1个字节重叠,按照上一小节最后的结论“
2.如果新接收到的数据在前面与之前接收到的数据后面重叠,则丢弃新数据重叠的部分。

        必须要注意的是,针对尚未确认的空洞数据段的重传,没有必要采用重叠模式重传,在此,我们必须明确为什么要重叠模式重传,为的是消除接收端Delay ACK的影响!在由于SACK存在空洞的情况下,接收端会视收到的数据段为乱序数据,这种情况自然就不会Delay ACK了!

原文链接: https://blog.csdn.net/dog250/article/details/52716578

欢迎关注

微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍;

也有高质量的技术群,里面有嵌入式、搜广推等BAT大佬

    TCP接收到重叠数据(overlap)后的行为解析-附带一个有关Delay ACK和超时重传的优化_网卡overlap 是什么

原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/407840

非原创文章文中已经注明原地址,如有侵权,联系删除

关注公众号【高性能架构探索】,第一时间获取最新文章

转载文章受原作者版权保护。转载请注明原作者出处!

(0)
上一篇 2023年4月26日 上午10:40
下一篇 2023年4月26日 上午10:41

相关推荐