TCP拥塞控制ABC(Appropriate Byte Counting)的利弊说

TCP在慢启动阶段,每一个RTT拥塞窗口按指数级增长,TCP在拥塞避免阶段,每一个RTT拥塞窗口线性增加1。这些都是书上讲的,不必太认真,真实的情况要比这个复杂的多!
        首先我们看大部分的资料里讲的TCP是怎么实现每RTT增窗的,一切都是扯理论,没什么现实意义!
        在慢启动阶段,每收到一个ACK(数据包从发出到收到其ACK,就是一个RTT),窗口增加1,在拥塞避免阶段,每收到前一窗口整窗的ACK,窗口增加1,也就是说,每收到一个ACK,窗口增加1/cwnd!然而这都是理想情况,万一ACK丢了怎么办?万一接收端启用了delay ACK怎么办?
        根据标准规定,接收端最多只能延迟2*MSS这么多的ACK,如果再多了就是stretch ACK了!也就是说,如果接收端启用了delay ACK,每收到两个数据包的时候才发一个ACK的话,发送端在一个RTT时间段内可能增加的窗口只有预期的1/2,这符合大多数资料里讲的逻辑吗?

标准其实并没有规定在拥塞避免阶段窗口一定是一个RTT增加1,而只是用了一个近似的算法:
Traditionally, TCPs have approximated this increase by  increasing cwnd by 1/cwnd for each arriving ACK.  This algorithm opens cwnd by roughly 1 segment per RTT if the receiver ACKs each incoming segment and no ACK loss occurs.  However, if the receiver implements delayed ACKs [Bra89], the receiver returns roughly half as many ACKs, which causes the sender to open cwnd more conservatively(by approximately 1 segment every second RTT).

如果TCP真的表现为一包一ACK,那么大多数情况下确实可以做到慢启动指数增窗,拥塞避免线性增窗。但是正如上面所说,如果考虑到ACK丢失,或者接收端delay ACK等,理论逻辑就难免失真了。
        TCP的ACK机制本身就是一种反馈,理论上“被ACK的数据越多,意味着可以发送的越多”这种猜测总是合理的,于是RFC3465提出了一个ABC算法,即通过收到的ACK中被ACK的字节数来计算如何增加窗口。

1.safe area和dangerous area

TCP作为一种端到端的协议并没有带宽反馈的能力,因此其拥塞控制机制完全是基于探测的,即不断地试探带宽的极限,不管多么好的拥塞控制算法,其本质也不外乎以上这种探测。在拥塞控制看来,所谓的带宽探测最终落实到拥塞窗口探测上。
        如今的TCP实现基本延续了NewReno的内核,在拥塞窗口探测的过程中,它会经历两个区域,一个是safe区域,一个是dangerous区域,两个区域由ssthresh来分割。在safe区域中,执行指数增窗的慢启动,在dangerous区域执行线性增窗的拥塞避免。ssthresh事实上是安全增窗的上界,也可以理解为保守的满带宽窗口,既然是安全的区域,那么就可以尽可能快的增加窗口,于是理论上每来一个ACK(这说明网络是通的),窗口就可以增加1,而不必等待一窗数据均被ACK才增窗,在越过了ssthresh之后,TCP会认为随时都有可能超过网络的承载能力,于是只有在一窗数据都被ACK了之后,才可以增窗。

2.如何利用ACK的反馈信息

以上的关于safe/dangerous区域的描述中,所有的增窗行为均是通过ACK来反馈的,RFC2581建议使用ACK的数量来作为增窗的信号,然而面对ACK丢失或者delay ACK的时候,RFC3465给出了ABC算法,在ABC中,使用被ACK的字节数而不是ACK的数量来作为增窗的反馈信号。这样的话,以下过程将被执行:
1.慢启动阶段:只要被ACK的数据字节数达到了一个MSS大小,窗口增加一个MSS;
2.拥塞避免阶段:只要被ACK的数据字节数达到了一窗的大小,窗口增加一个MSS。

除此之外,ABC可以让TCP在慢启动阶段更加激进,它可以让TCP每收到ACK了一个MSS的确认包后,增加N个而不是1个MSS窗口的大小,因为这是在安全区域,激进不为过!

3.ABC与突发

ABC貌似解决了delay ACK以及ACK丢失带来的窗口增加缓慢的问题,然而却带来了突发问题,这个突发问题是利还是弊,并不绝对!本质上来讲,ABC算法会带来突发的原因是它会“记住”那些迟到的或者丢失的确认,并积累起来日后使用,这非常类似于网络流量控制的突发令牌桶的原理,令牌是可以积累使用的,在TCP中,每一个针对一个MSS大小数据段的确认,不管其是显式的还是隐式的,都相当于一个可以积累的令牌。

1.异常带来的突发

假设ACK大面积丢失,对于发送端而言,ACK到来的频率会降低,造成的效果就是窗口长时间由于收不到ACK而僵住,一旦收到一个ACK,发送端会发现它ACK了大量(甚至巨量)的数据,窗口一下子得到了补偿,可能会增加很多,这种突发对于慢启动阶段可能会更严重,因为慢启动阶段每确认MSS大小的数据,窗口就会增加N(取决于配置),而对于拥塞避免阶段,情况会缓和很多。
        这种异常带来的突发,在单向拥塞的情况下,问题不大,如果发生了双向拥塞,发送端的激进增窗带来的就是更多丢包了,发送端在无法区别对待ACK丢失和delay ACK的情况下,会造成很多的误判,幸好,TCP规范了stretch ACK的定义,这或许会给发送端的判断带来一些暗示,比如收到了连续ACK了超过2个MSS数据的确认包,就判断为是ACK丢失,保守增窗。
        为了保证发生上述问题的时候异常突发不会带来严重的后果,RFC3465规定慢启动阶段,N的最大值为2,即每收到一个MSS大小的确认,窗口最多增加2个MSS!

2.正常突发

相对于异常突发,正常突发就显得更加像是一个正反馈无级变速系统了,简单来讲,如果不考虑delay ACK和ACK丢失以及接收端的实现bug,ABC算法执行地会相当平滑,即ACK覆盖面越广,增窗就越快,不考虑拥塞的情况下,这意味着你发送的数据越多,窗口增加的越快!
        RFC3465有一个例子很好,比如你登录了ssh进行交互式作业,大多数情况下都是小数据的交互,此时窗口增加比较缓慢(ABC算法按照被ACK的字节数大小来决定是否增窗以及增加多少),此时如果你需要在终端显示一个大文件,那么交互的数据量几乎是瞬间变大,如果按照RFC2581的方式增窗,窗口增加完全按照ACK的数量来的话,就等于说是有了一个最高的限速,然而如果使用ABC的话,随着大块数据的发送和被确认,窗口的增速也随之增加。这种情况下,ACK到达的速率是不变的,然而ACK覆盖的字节数却变大了,窗口增速也就随之变大。

3.穿越ssthresh

在慢启动阶段,窗口按照指数增长,如果ssthresh的值比较大,在窗口穿越ssthresh的时候,其可能已经是十分大了,在最后一次慢启动增窗中,很大的可能性会使窗口一下子升到ssthresh以上很多,超过了网络的承载能力造成丢包。在这种情况下,ssthresh根本就没有起到阈值的作用,在ABC算法中N为2的情况下,问题更加严重。
        之所以会有这个问题,是因为TCP没有在窗口增加到ssthresh附近的时候没有更细化的控制。不过这不是什么问题,Linux 4.x+已经完美修正了这个问题(具体我不知道是哪个版本引入的,但是我确定3.10中没有,而4.3中有)。

4.ABC与Linux TCP实现

Linux关于ABC算法的实现经历了三个阶段。

阶段一:ABC作为一个sysctl选项

以2.6.32内核版本为例,Linux有一个sysctl_tcp_abc选项,它会选择是否使用ABC算法,如果启用ABC,一个ACK包确认的字节数会被保存在bytes_acked字段里,在拥塞避免阶段,TCP是这样使用bytes_acked的:

if (tp->bytes_acked >= tp->snd_cwnd*tp->mss_cache) {
    tp->bytes_acked -= tp->snd_cwnd*tp->mss_cache;
    if (tp->snd_cwnd < tp->snd_cwnd_clamp)
        tp->snd_cwnd++;
}

而bytes_acked会在收到ACK的时候被更新:

if (icsk->icsk_ca_state < TCP_CA_CWR)
    tp->bytes_acked += ack - prior_snd_una;
else if (icsk->icsk_ca_state == TCP_CA_Loss)
    /* we assume just one segment left network */
    tp->bytes_acked += min(ack - prior_snd_una,
                   tp->mss_cache);

然而,如果没有使用ABC的话,在每收到ACK的时候会执行下面的逻辑:

if (tp->snd_cwnd_cnt >= tp->snd_cwnd) {
    if (tp->snd_cwnd < tp->snd_cwnd_clamp)
        tp->snd_cwnd++;
    tp->snd_cwnd_cnt = 0;
} else {
    // 可见,不使用ABC是通过ACK的数量来计数的
    tp->snd_cwnd_cnt++;
}

在慢启动阶段:

if (sysctl_tcp_abc > 1 && tp->bytes_acked >= 2*tp->mss_cache)
    cnt <<= 1; // 可以增加2倍窗口
// 慢启动阶段完全按照一次ACK的MSS倍数来决定增窗大小,因此需要清除bytes_acked
tp->bytes_acked = 0;

tp->snd_cwnd_cnt += cnt;
// 注意,下面的算法可能会出现ssthresh的穿越问题!
while (tp->snd_cwnd_cnt >= tp->snd_cwnd) {
    tp->snd_cwnd_cnt -= tp->snd_cwnd;
    if (tp->snd_cwnd < tp->snd_cwnd_clamp)
        tp->snd_cwnd++;
}

阶段二:ABC可选实现

以3.10为例,你会发现没有了tcp_abc选项,在代码中,也再也没有了关于bytes_acked的计数,而且在拥塞避免阶段,也只剩下了tcp_cong_avoid_ai(tp, tp->snd_cwnd)的逻辑,而这个逻辑是按照ACK的数量来计数的。
        那么如果想实现ABC怎么办呢?只好在自己的拥塞模块里面自己写了。每个ACK所确认的数据量(即bytes_acked)可以通过pkts_acked回调函数获取(在清理传输队列的数据包后调用)。

阶段三:ABC内置并优化实现

到了Linux 4.4内核,依然还是没有tcp_abc选项,然而如果你看代码,发现基本已经完全实现了ABC算法:

void tcp_reno_cong_avoid(struct sock *sk, u32 ack, u32 acked)
{
    struct tcp_sock *tp = tcp_sk(sk);

    if (!tcp_is_cwnd_limited(sk))
        return;

    /* In "safe" area, increase. */
    if (tcp_in_slow_start(tp)) {
        // slow_start函数有了返回值,返回慢启动结束后,还剩下多少被确认的数据可以用来增加拥塞避免增窗计数器
        acked = tcp_slow_start(tp, acked);
        // 如果返回0,说明此次增窗还没有穿越ssthresh。可见新版本完美捕捉到了ssthresh穿越问题
        if (!acked)
            return;
    }
    /* In dangerous area, increase slowly. */
    // acked参数被传入,作为增窗条件的计数被累加。
    tcp_cong_avoid_ai(tp, tp->snd_cwnd, acked);
}

如果我们看下tcp_slow_start,会发现它异常简洁:

u32 tcp_slow_start(struct tcp_sock *tp, u32 acked)
{
    // 慢启动阶段,窗口最多增加到ssthresh。
    // acked累加到窗口,虽然不是ABC的标准算法(没有实现N增窗),但基本就是那个意思
    u32 cwnd = min(tp->snd_cwnd + acked, tp->snd_ssthresh);
    // 剩下的穿越ssthresh的部分交给拥塞避免来处理
    acked -= cwnd - tp->snd_cwnd;
    tp->snd_cwnd = min(cwnd, tp->snd_cwnd_clamp);

    return acked;
}

函数tcp_cong_avoid_ai的注释也多了一句话:

/* In theory this is tp->snd_cwnd += 1 / tp->snd_cwnd (or alternative w),
 * 下面这句话是新增的...
 * for every packet that was ACKed.
 */
void tcp_cong_avoid_ai(struct tcp_sock *tp, u32 w, u32 acked)
{
    /* If credits accumulated at a higher w, apply them gently now. */
    if (tp->snd_cwnd_cnt >= w) {
        tp->snd_cwnd_cnt = 0;
        tp->snd_cwnd++;
    }
    // 累加被确认的数据量而不是每收到ACK简单加1
    tp->snd_cwnd_cnt += acked;
    ...
}

TCP是一个复杂无比的协议,Linux的实现也历经着无比大的变化,从2.6.8到4.4,你会发现很多细节连基本思想都改变了,这背后,如果不了解RFC中阐释的思路,将很难理解其所以然,因此RFC才是王道,内功,心法!

时间: 2024-10-14 11:01:13

TCP拥塞控制ABC(Appropriate Byte Counting)的利弊说的相关文章

TCP拥塞控制

TCP必须使用端到端拥塞控制而不是使网络辅助的拥塞控制,因为IP层不向端系统提供显式的网络拥塞反馈.TCP采用的方法是让每一个发送方根据所感知到的网络拥塞程度来限制其能向连接发送流量的速率. TCP限制向其连接发送流量 TCP连接的每一端都是由一个接收缓存.一个发送缓存和几个变量组成.运行在发送方的TCP拥塞控制机制跟踪一个额外的变量,拥塞窗口(cwnd),它对一个TCP发送方能向网络中发送流量的速率进行了限制:在一个发送方中未被确认的数据量不会超过cwnd和rwnd中的最小值,即 LastBy

TCP拥塞控制 (1)

Basic: TCP,传输控制协议,是目前网络中应用最广泛的传输协议.SMTP.SSH.FTP.HTTP等因特网底层协议均是TCP. TCP面向连接,提供端到端的数据可靠传输.连接时三次握手,断开是四次挥手.具体表现为: 1.       TCP对传输的数据做了序号标记,其中序号标记安装字节数增长.TCP对端在接收到数据后发出一个ACK给对端(ACK中就包含序列号).TCP使用确认和超时重传机制保障了数据的可靠性传输. 2.       由于发送和接收端的数据处理能力不同,为了避免数据发送过快而

《网络协议》TCP 拥塞控制

TCP 拥塞控制主要有几种:慢启动.拥塞避免.快重传 以及 快恢复. 慢启动 为了防止网络拥塞,TCP 采用了一种慢启动算法,对发送数据量进行控制.为了调节发送端的数据发送量,引入了拥塞窗口,在慢启动时,将这个拥塞窗口设为 1 个报文段发送数据,之后每收到一次确认应答,拥塞窗口的值就加 1 个报文段.在发送数据包时,将拥塞窗口的大小与接收端主机通知的窗口大小进行比较,然后选择较小的值来控制数据量的发送.拥塞窗口是发送端使用的流量控制,而通告窗口则是接收端使用的流量控制. 慢启动算法步骤如下(cw

linux内核工程导论-网络:tcp拥塞控制

这篇文章本来是在tcp那篇里面的,但是那篇太长了,不专一.就完善了一下提取出来了. TCP拥塞控制 拥塞控制讨论的是很多个同时存在的tcp连接应该怎么规划自己的数据包发送和接收速度,以在彼此之间共享带宽,同时与其他实体的机器公平的竞争带宽,而不是自己全占. 拥塞控制的核心是AIMD(additive-increase/multiplicative-decrease ),线性增加乘性减少.为啥不用线性增加线性减少,或者是乘性增加乘性减少呢?这个有人专门研究过,只有AIMD可以收敛聚合使得链路公平.

TCP拥塞控制 2

解决传统TCP缺陷: 1.窗口太小,最大65535. TCP利用了选项功能,其头部存在预留项,用于扩展等用途.窗口扩大选项增加了额外的16位来表示窗口大小,窗口的值由首部的16位大小和选项的16位值共同组成. 不过不是用加法组成的,而是利用移位窗口值的幂来表示的,也就是说如果移位窗口值为 10,那么窗口的最大值就是65535*210,这个值就比较大了,足够表示窗口的大小了. 2.数据包丢失,即认定为网络出现了拥塞 在高速网络中,这种假设是不成立的.如果笼统地认为分组丢失就是拥塞所引起的,从而降低

TCP 拥塞控制

TCP 拥塞控制 相关名词 滑动窗口 tcp通过滑动窗口进行流量控制,所谓的窗口可以理解为接收端所能提供的缓冲区大小. TCP是一个滑动窗口协议,即一个TCP连接的发送端在某个时刻能发多少数据是由滑动窗口控制的 RTT(Round trip time) 表示从发送端到接收端的一去一回需要的时间. TCP在数据传输过程中会对RTT进行采样(即对发送的数据包及其ACK的时间差进行测量,并根据测量值更新RTT值) RTO (Retransmission TimeOut) 发送数据包,启动重传定时器,重

计算机网络概述 传输层 TCP拥塞控制

TCP拥塞控制 计算机网络中的带宽.交换结点中的缓存和处理机等,都是网络的资源.在某段时间,若对网络中某一资源的需求超过了该资源所能提供的可用部分,网络的性能就会变坏.这种情况就叫做拥塞. 拥塞控制就是防止过多的数据注入网络中,这样可以使网络中的路由器或链路不致过载.拥塞控制是一个全局性的过程,和流量控制不同,流量控制指点对点通信量的控制. 拥塞控制 和 流量控制 的区别? 1. 拥塞控制:拥塞控制是作用于网络的,它是防止过多的数据注入到网络中,避免出现网络负载过大的情况: 2. 流量控制:流量

TCP拥塞控制及连接管理

在阅读此篇之前,博主强烈建议先看看TCP可靠传输及流量控制. 一.TCP拥塞控制 在某段时间,若对网络中某资源的需求超过了该资源所能提供的可用部分,网络的性能就要变坏——产生拥塞(congestion).出现资源拥塞的条件:对资源需求的总和 > 可用资源:拥塞带来的问题:若网络中有许多资源同时产生拥塞,网络的性能就要明显变坏,整个网络的吞吐量将随输入负荷的增大而下降. 1. 拥塞的控制方法一(慢开始和拥塞避免) 发送方维持一个叫做拥塞窗口 cwnd (congestion window)的状态变

TCP拥塞控制机制

我们知道TCP是拥有拥塞控制机制的,而UDP是没有的,为什么需要拥塞控制机制呢,就是防止丢包过多导致传输效率低下.网络中传输的包太多,路由器的缓存又不够,每一个发送端都以自己想要的发送速率发送包,自然会导致网络拥塞.所以我TCP就包括了拥塞控制机制. 有几种拥塞控制方法? 2种 1.端到端拥塞控制.网络层没有显示的告知传输层此时网络出现拥塞了,传输层通过报文段的丢失(超时或3次冗余确认得知)认为网络出现拥塞了,TCP会缩减其拥塞窗口,减小发送速率. 2.网络辅助的拥塞控制.网络层显示的告知发送端