QVegas-一个升级版的TCP Vegas拥塞算法

拥塞避免带来了非常多疑惑,本文解开这个疑惑并给出一个实实在在但却非常简陋的算法。
        事实上在基于丢包的拥塞算法中,拥塞避免的过程总是伴随着AI和MD的,不能光说AI而忽略MD。
        假设考虑的是基于时延的拥塞算法,AI和MD事实上是不须要的,由于算法会依据时延的变化来调整窗体。
        所以说,AIMD是基于丢包的算法中採取的窗体调整方法,当中AI保证了带宽利用率。而MD则保证了公平性。

对于基于时延的算法而言,带宽的利用率和公平性保证均分散性体如今算法的运行过程中了。当中:
时延添加:说明路由器交换机開始排队了,此时降窗。保证了公平性
时延降低:说明路由器交换机的排队正在缓解。能够适当增窗,保证带宽满负荷
出现丢包:重传丢包,不调整窗体,由于算法过程全然接管窗体调整,丢包与窗体无关

看起来,这绝对是一个非常完美的“拥塞避免”方案。可是却有个前提,那就是路由器交换机排队是由自己或者相同运行基于时延算法的流导致的。由于每个流都会预期。仅仅要某一个连接造成了出现排队。那么大家都会排队,排队时延会添加RTT。依照算法,大家就会降窗缓解。假设说排队是一个CUBIC流造成的,那么CUBIC在全然占满缓存之前是不会降窗的(忽略AQM,假设是尾部丢包),而在队列越来越长的过程中,基于时延的算法会持续降窗退让,这里的问题和BBR遇到深队列的问题一样。
        TCP Vegas就是这种一种基于时延的算法,它的思想是非常正确的,有人说假设玩得好。Vegas的效率要高于BBR的,由于BBR是主动使用最小RTT,旨在不排队,而Vegas则不关注详细的RTT,仅仅是关注RTT的变化率,结果就是Vegas不能避免排队,可是它会把队列深度限制在一个范围内。我确信这一点,所以我对其做了改动。


先说一个我对Vegas的疑惑吧。


        为什么在Vegas的cong_avoid回调函数tcp_vegas_cong_avoid中会有reno的AI调用:

if (!vegas->doing_vegas_now) {
    tcp_reno_cong_avoid(sk, ack, acked);
    return;
}
if (after(ack, vegas->beg_snd_nxt)) {
    ...
    if (vegas->cntRTT <= 2) {
        tcp_reno_cong_avoid(sk, ack, acked);
    } else {
        ...
    }
}
就是说在一定程度上不满足Vegas的调用条件时,仍然要回退到Reno的AI过程。而不是仅仅的返回处理。

我起初觉得这是实现的问题,这种实现并不纯粹,更相似于是一种混合的Hybrid算法,而不是全然的Vegas算法,然而事情并不是如此简单。
        大家都知道Vegas面对Reno,CUBIC时会吃亏。原因上面已经说过了,可是有没有办法缓解一下呢?简单的解决之道就是将Reno算法插入Vegas当中。也就是当前Linux对Vegas的实现。可是问题在于。当我实測Vegas的时候,发现其对带宽的利用率极低。我想知道为什么。所以我打出了一幅时间-窗体图,发现了令人遗憾的锯齿!为什么会有锯齿呢?基于时延的算法难道不该是波浪形曲线吗?
        问题出在,在Vegas面对丢包时,窗体会下降1或者2个MSS。这意味着当丢包恢复后,窗体会从一个低值又一次开涨,这在浅队列情况下尤其严重。

正确的做法应该是。从丢包时Vegas计算出来的窗体值開始涨。

不幸的是,Linux实现的Vegas并没有区分对待一个窗体的哪一部分是Vegas算法过程增长的。哪部分是间隙中Reno算法增长的。我要改的非常easy,就是区分两者。然后在丢包时区分对待两者。
        主要改动了三部分:
1.添加了reno_inc计数器,表明在连续两次异常状态间一共靠Reno算法涨了多少窗体;
2.添加last_cwnd,记录“上一次”Vegas结束时cwnd的大小,在当次进入Vegas逻辑的时候,恢复之。注意,可能包括了Reno的增窗量;
3.在ssthresh回调中,将last_cwnd减去响应的Reno的MD降窗量作为新的last_cwnd。这意味着出现丢包时把Reno给予的还了回去。

我来解释一下为什么Vegas不必Care丢包的原因,由于Vegas的算法是正确的,它真正做到了拥塞避免,所以不会由于拥塞而丢包,真的由于别的流填满了缓存导致了丢包,也不是Vegas的全责。所以丢包肯定不是由于Vegas自己造成的拥塞导致的。在TCP拥塞控制算法中,谁的责任谁弥补一定要落到实处。

注意,这不是我不减窗的理由,这是我的权利。
        我老婆去年11月初回上海。我家的车在地库停了10来天。她回来时发现车子右前方被撞了,调取监控发现了那个肇事逃逸的傻逼并找到了他,对方无奈赔偿了损失,请注意,这个损失不光包括车辆损坏的损失。还包括这段修车的时间不能开车的时间损失。责任方一定要理清,我无辜受害者躺枪了干嘛还要降窗?!

经过改动之后。时间-窗体曲线变成了下面的豁口形状:




我如今的想法是,事实上,在Linux的Vegas实现中,Vegas在ssthresh中为Reno背了黑锅,Vegas的拥塞避免根本就不须要罪与罚的循环,即不须要AI(罪)和MD(罚),由于Vegas的算法根本就不会犯罪,它的收敛分布在整个算法运行的过程中了。
        Vegas的排队问题介于理想的不排队和排队排到满之间。
        Vegas性能差并不仅仅是由于其受到了CUBIC之流的欺负,很多其他的是由于它採取了基于丢包算法的那一套罪与罚的框架,对于Vegas而言。假设发生丢包,窗体也是要减的,然而这是没有必要的。Vegas自己已经在探測到RTT变大时主动减法降窗了。而且不止一次...在这里贴代码是不合时宜的,详细的代码请參见github:https://github.com/marywangran/qvegas 我就在这里贴一个代码细节。那就是Vegas借了别人的优点,记得还:
1.avoid过程:
if (!vegas->doing_vegas_now) {
    u32 cwnd = tp->snd_cwnd;
    tcp_reno_cong_avoid(sk, ack, acked);
    qvegas->reno_inc += tp->snd_cwnd - cwnd;
    return;
}
if (after(ack, vegas->beg_snd_nxt)) {
    ...
    if (vegas->cntRTT <= 2) {
        u32 cwnd = tp->snd_cwnd;
        tcp_reno_cong_avoid(sk, ack, acked);
        qvegas->reno_inc += tp->snd_cwnd - cwnd;
    } else {
        tp->snd_cwnd = qvegas->lost_cwnd;
        ...
        qvegas->lost_cwnd = tp->snd_cwnd;
    }
} 
2.ssthresh过程:
static inline u32 tcp_qvegas_ssthresh(struct tcp_sock *tp)
{
    struct sock *sk = (struct sock *)tp;
    struct qvegas *qvegas = inet_csk_ca(sk);

    if (tp->lost_out) // 仅仅有在丢包的时候,返还Reno的增量,且依照AIMD的方式返还
        qvegas->lost_cwnd = max(tp->snd_cwnd - qvegas->reno_inc>>1U, 2U);
    return  max(min(tp->snd_ssthresh, tp->snd_cwnd-1), 2U);
}
改动之后,通过你对网络的认识,调整alpha,beta。gamma到一个相对大的区间,在不丢包的前提下从容应对深队列RTT的添加。可是前提是你对网络有一个比較深刻的认识,但我觉得大部分人是没有这个能力的。

你想啊,一帮大学毕业就到格子间编程的。怎么可能知道网络是怎么运作的。

我是一个实实在在的project派,假设有机会的话,看核心网路由交换的统计数据以及日志是不错的选择,不然无论如何讽刺TCP相关的一切都不为过。


        我不得不恶心一把马可.波罗这个傻逼,他把道听途说的东西非常不真实的传递到了西方,说什么杭州的房子都是金子造的...结果人家西方人到了中国。除了发现一群群贫困但勤勤恳恳的农民自建的茅草屋之外。别无太大的惊喜,说什么白银外流。贸易逆差,那是西方工业革命以后的事了...说白了。TCP的ACK就是马可.波罗,就是一个又瞎又傻还喜欢造谣的傻逼,敢问假设你被它蛊惑了的话,你的一辈子能幸福吗?你怀着一种憧憬扑向一堆根本就不存在的东西,你看到了海市蜃楼但你觉得它就是终点,一次又一次被骗仍然不改。
....
连续看了《大秦帝国》的裂变,纵横,崛起,我觉得,我能赞赏的人物中,包括全部的秦王。以及商鞅和李斯,其他的国君将相都仅仅只是在完毕KPI而已!总结一句的话,那就是苏秦之流完毕KPI,而赳赳老秦奋六世之余烈憋大招。至于是什么大招,就不用我说了。
....

从源头抑制Bufferbloat

关于Bufferbloat,将问题扼杀在源头是一个比错的想法。让本地的Buffer限制数据包的一次性突发。总比数据包突发到中间路由器交换机缓存要好得多。那么怎么办呢?非常easy。把自己网卡的发送队列设置得小一点哦。这个能够通过下面的命令简单设置:
ifconfig eth2 txqueuelen 16
当然,你也能够用tc设置更加复杂的策略。

再谈Hybrid算法

在此,我得说一下微软的Compound TCP拥塞控制算法,简称CTCP。


        微软的CTCP算法是与我们常见的那些诸如Reno。CUBIC。Vegas等截然不同的算法。由于你非常难说出“它是基于什么”来控制窗体的,所以说它是一种启示式的混合算法更加合适。正如其名字所表达的那样。


        事实上微软的这个算法跟我的这个算法思路非常像。不同点在于:

1.CTCP:以基于丢包的(比方Reno)窗体为主。辅助以时延(比方Vegas)窗体发现带宽被用尽

CTCP的目标是在长肥管道的时候。以公平性以及不主动添堵为前提高速逼近可用的带宽,一旦发现有排队,将会退回到基于丢包的AIMD过程。在发现排队之前。则能够使用基于时延的算法计算的窗体叠加到基于丢包的窗体之上。高速涨窗。
        这个算法的目标和Scalable TCP以及High speed TCP算法的目标是非常一致的,可是在效果上据说会更好,我并没有实际进行过測试。所以不敢有更进一步的结论。

仅仅从算法的逻辑上看,CTCP是比較合理的,它降低了带宽的浪费,反正仅仅要还有带宽是空暇的,CTCP就会高速去占用,一旦发现没有带宽空暇了。就会退到AIMD来和其他的连接公平共享全部的带宽。
        TCP最关键的一点,收敛。CTCP做到了。CTCP的Paper在:https://www.microsoft.com/en-us/research/wp-content/uploads/2016/02/tr-2005-86.pdf

2.QVegas:以时延窗体为主,辅助以基于丢包的窗体添加抢占性

QVegas的目标在于能够公平地和其他的算法共存,既然其他算法欺负Vegas,那么Vegas何不自己来呢。所以就融合了Reno。

可是公平性并没有被破坏。QVegas终于在丢包的时候,不是把Reno的增量通过乘性减窗的方式还回去了么?
        无论是CTCP还是我的QVegas,都属于Hybrid算法,它们的总的窗体值都有两个分量组成。一个为主一个为辅,二者的不同点在于其出发点全然相反。所以说,看到CTCP的Paper中下面一段话。就不足为奇了:
... it should also react to packet losses. It is because packet losses may still be an indicator of heavy congestion,  
and hence reducing sending rate upon packet loss is a necessary conservative behavior to avoid congestion collapse.

我一開始是奇怪的,我在想既然使用了时延计算了窗体,为什么还要激烈地乘性减窗呢?只是我瞬间就想到了,我是站在自己的QVegas的立场上来评估CTCP算法的,对于CTCP而言。情况跟我的QVegas全然相反。CTCP就是一个基于丢包的算法,所以它发现了丢包。还是要MD的。而我的QVegas算法则是一个基于时延的算法。所以在发现丢包时,仅仅要把Reno分量减下去就可以。


从BBR窗体控制的一个细节再谈TCP收敛

參见前文《让人非常easy误解的TCP拥塞控制算法》,我一直在强调公平性。公平性是TCP收敛的保证,假设TCP不收敛,那么这个协议就是全然不可用的。
港真,所谓的TCP收敛,要求TCP连接做到的仅仅有一点,那就是悬崖勒马。


        这里我再次讲一个关于我对BBR理解过程中的一个插曲,相同性质的插曲遍布在我对OpenVPN以及REUSEPORT的学习和理解的过程中。这些插曲都源自于我对它们最初的误解。
        初看了BBR的模型后,我以为BBR是不在乎丢包状态的,也就是说,作为拥塞算法来讲。BBR是看不到TCP层面的各种拥塞状态(比方是否处在高速重传等)的,它仅仅是不断地採集带宽和RTT这两项,并把它们放进两个各自的时间窗体内,然后分别地挑出最大带宽和最小RTT,以最大带宽作为基准Pacing rate。以最小RTT和最大带宽的乘积这个基准BDP作为基准窗体。
        然后再将Pacing Rate和窗体经过内在引擎的GAIN调和成终于的发送速率和拥塞窗体...




我觉得以上的理解是正确且简单的理解,并能够教别人迅速理解BBR算法的运行过程与传统算法不同。

在这个过程中,我们没有看到cong avoid,ssthresh。prr这类东西,也没有看到BBR针对丢包时的反应,这些就是差别,BBR对丢包无反应,假设依照传统的观点,TCP在1秒内成功发送(即全部穿过网络到达对端)了10个一模一样的数据包,每个数据包大小为1024字节,那么TCP会觉得此时的速率是1024Bps,依照BBR的观点,假设TCP在1秒内成功发送了10个一模一样的数据包。每个大小1024B,那么BBR会觉得此时的速率是1024*10Bps,这就是最根本的差别。
        事实真的如此吗?
        是的,真的如此!理由非常easy。仅仅有TCP的发送端和接收端才干理解TCP,中间的网络是不理解TCP的,因此指导拥塞控制的速率採集不应该区分数据包。这个速率与TCP的传输速率是有差别的。尽管事实如此,可是在细节上真的没有别的坑吗?BBR对于丢包重传真的没有反应吗?
        我一開始觉得是没有反应的,由于这样能够保持BBR的简单却又不影响公平性指标(BBR的收敛性体如今Drain Less以及Probe RTT)。
        我以前试着写过一个跟BBR算法相似的,思路就是上面说的那种,对丢包,重传,乱序这种不做不论什么反应。

与BBR原生实现相比。我的简版实现单流測试效果非常不错!那个时候我可能还没有深入分析过BBR的实现代码。


        可是后来发现。这好像不合伦理...幸亏我知道悬崖勒马这个词。同一时候也知道一路走到黑的后果。

究竟哪里出了问题呢?
        于是,我採用了多个流进行測试。果然,丢包数据急剧添加,造成的重传率也急剧添加。此时。我不得不深入分析BBR的实现细节了。这里就直接说结论吧。
        BBR在设置窗体大小的时候,并不是一直依照最大速率与最小RTT的乘积来设置的。假设TCP在Recovery或者Loss状态。那就保持窗体守恒,尽管并不是依照PRR算法来降窗,可是维持当前的窗体不变也有一种悬崖勒马的味道了。随后在TCP进入Open状态后,就会直接恢复窗体到进入Recovery或者Loss状态之前的值。关于这段代码,请看:

static void bbr_set_cwnd(struct sock *sk, const struct rate_sample *rs,
             u32 acked, u32 bw, int gain)
{
    struct tcp_sock *tp = tcp_sk(sk);
    struct bbr *bbr = inet_csk_ca(sk);
    u32 cwnd = 0, target_cwnd = 0;

    if (!acked)
        return;
    // 假设进入了Recovery或者Loss状态,就不再运行BDP来计算窗体了,而是直接僵住窗体,必要时还会缩减。

if (bbr_set_cwnd_to_recover_or_restore(sk, rs, acked, &cwnd)) goto done; // 假设在Open状态,才会运行max-bw*min-rtt来计算窗体 /* If we're below target cwnd, slow start cwnd toward target cwnd. */ target_cwnd = bbr_target_cwnd(sk, bw, gain); if (bbr_full_bw_reached(sk)) /* only cut cwnd if we filled the pipe */ cwnd = min(cwnd + acked, target_cwnd); else if (cwnd < target_cwnd || tp->delivered < TCP_INIT_CWND) cwnd = cwnd + acked; cwnd = max(cwnd, bbr_cwnd_min_target); done: tp->snd_cwnd = min(cwnd, tp->snd_cwnd_clamp); /* apply global cap */ if (bbr->mode == BBR_PROBE_RTT) /* drain queue, refresh min_rtt */ tp->snd_cwnd = min(tp->snd_cwnd, bbr_cwnd_min_target); }

当中,bbr_set_cwnd_to_recover_or_restore这个函数比較有意思。它的调用实际上表达的就是悬崖勒马。大致意思就是我上面文字所描写叙述的。进入Recovery或者Loss状态前,保存当前的窗体值,然后在整个异常状态维持的期间。以该窗体为基准,在缩减丢包量的基础上维持窗体的守恒:
static bool bbr_set_cwnd_to_recover_or_restore(
    struct sock *sk, const struct rate_sample *rs, u32 acked, u32 *new_cwnd)
{
    struct tcp_sock *tp = tcp_sk(sk);
    struct bbr *bbr = inet_csk_ca(sk);
    u8 prev_state = bbr->prev_ca_state, state = inet_csk(sk)->icsk_ca_state;
    u32 cwnd = tp->snd_cwnd;

    /* An ACK for P pkts should release at most 2*P packets. We do this
     * in two steps. First, here we deduct the number of lost packets.
     * Then, in bbr_set_cwnd() we slow start up toward the target cwnd.
     */
     // 注意。在Recovery状态。losses字段也能够是0!详细看NewReno针对Reno的改进以应对丢多包。
    if (rs->losses > 0)
        cwnd = max_t(s32, cwnd - rs->losses, 1);

    if (state == TCP_CA_Recovery && prev_state != TCP_CA_Recovery) {
        /* Starting 1st round of Recovery, so do packet conservation. */
        bbr->packet_conservation = 1;
        bbr->next_rtt_delivered = tp->delivered;  /* start round now */
        /* Cut unused cwnd from app behavior, TSQ, or TSO deferral: */
        cwnd = tcp_packets_in_flight(tp) + acked;
    } else if (prev_state >= TCP_CA_Recovery && state < TCP_CA_Recovery) {
        /* Exiting loss recovery; restore cwnd saved before recovery. */
        bbr->restore_cwnd = 1;
        bbr->packet_conservation = 0;
    }
    bbr->prev_ca_state = state;
    // 退出Recovery或者Loss状态的时候,恢复窗体值。

这个意思也包括在了我的QVegas算法中,仅仅是我并没有使用BBR带来的新框架。所以说我的实现看上去不是那么直接。 if (bbr->restore_cwnd) { /* Restore cwnd after exiting loss recovery or PROBE_RTT. */ cwnd = max(cwnd, bbr->prior_cwnd); bbr->restore_cwnd = 0; } if (bbr->packet_conservation) { *new_cwnd = max(cwnd, tcp_packets_in_flight(tp) + acked); return true; /* yes, using packet conservation */ } *new_cwnd = cwnd; return false; }

假设依照BBR的原始模型,以上这个细节是不存在的,诚然。这是一个不可或缺的优化。从这个优化中能够看出,在出现异常的时候悬崖勒马是多么的重要。无论如何,你能够试试,忽略上述的细节,在带宽不稳定或者出现全局同步的时候,你的连接的丢包和重传会像脱缰的疯马一样不受控制。而加上上述细节,无论在什么情况下。BBR均能够保证终于收敛。
----------------------------
在BBR窗体控制的细节展示的其收敛性之后,我写这一段的目的在于说明,理论上的模型和实际的实现细节差距还是蛮大的,在接触BBR之前。我先后在OpenVPN和REUSEPORT上看到过相似的坑。
OpenVPN
我一直以为OpenVPN是用TLS的记录协议或者DTLS来承载的,后来发现OpenVPN事实上区分了数据通道和控制通道,而仅仅有其控制通道採用了TLS协议,数据通道使用自己的私有协议。


REUSEPORT
我一直以为选择socket的哈希算法就是五元组混合后依照socket个数取模的,可是后来才知道不是这种。详细參见《Linux 4.6内核对TCP REUSEPORT的优化》,
        还好。终于Linux对REUSEPORT的实现回归了我最初的想法。毕竟那是一个正确的想法。

且听下回分解!

原文地址:https://www.cnblogs.com/wgwyanfs/p/7357587.html