什么是 ‘TCP Congestion Control’ (拥塞控制)?解析 CUBIC 与 Google BBR 算法在内核中的实现逻辑

尊敬的各位技术同行,大家好。

今天,我们将深入探讨网络通信的核心——TCP拥塞控制。在现代互联网基础设施中,TCP协议承载了绝大部分的数据传输,而其拥塞控制机制,正是确保网络稳定、高效运行的关键所在。我们将从拥塞控制的基石原理出发,逐步剖析两种在业界占据重要地位的拥塞控制算法:CUBIC和Google BBR,并深入探讨它们在内核中的实现逻辑。

1. TCP拥塞控制的基石:理解与需求

1.1 什么是网络拥塞?

想象一下,一条高速公路,如果车辆的数量超过了道路的设计容量,就会发生交通堵塞。在网络世界中,情况与此类似。当发送方以过高的速率向网络发送数据,超过了网络中某个链路(如路由器、交换机)的处理能力或传输带宽时,就会发生网络拥塞。

网络拥塞的典型表现包括:

  • 数据包丢失 (Packet Loss):路由器队列溢出,导致数据包被丢弃。
  • 延迟增加 (Increased Latency):数据包在路由器队列中等待时间变长,导致端到端延迟显著增加。
  • 吞吐量下降 (Throughput Degradation):由于丢包和重传,有效数据传输速率降低。

1.2 拥塞控制的目标

TCP拥塞控制的核心目标是:

  • 最大化网络吞吐量 (Maximize Throughput):尽可能高效地利用可用网络带宽。
  • 最小化数据包丢失和延迟 (Minimize Packet Loss and Latency):避免网络过载,减少不必要的重传和等待。
  • 确保公平性 (Ensure Fairness):多个TCP流共享同一个瓶颈链路时,应公平地分配带宽。
  • 快速收敛 (Fast Convergence):当网络状况发生变化时,拥塞控制算法应能迅速调整发送速率。

1.3 拥塞控制的演进:从AIMD到智能感知

早期的TCP拥塞控制算法,如Tahoe、Reno,主要基于丢包作为拥塞信号。它们的核心思想是AIMD (Additive Increase, Multiplicative Decrease),即“加性增、乘性减”。

  • 慢启动 (Slow Start):连接建立初期,发送方从一个很小的窗口(通常是1-2个MSS,最大报文段大小)开始,每收到一个ACK,拥塞窗口 (CWND) 就增加1个MSS。这使得CWND呈指数级增长,快速探测可用带宽。
  • 拥塞避免 (Congestion Avoidance):当CWND达到慢启动阈值 (ssthresh) 时,进入拥塞避免阶段。此时,每收到一个RTT (Round Trip Time) 内的所有ACKs,CWND增加1个MSS。这使得CWND呈线性增长。
  • 快速重传 (Fast Retransmit):当发送方收到3个重复的ACK时,认为有数据包丢失,立即重传丢失的数据包,而不必等待重传定时器超时。
  • 快速恢复 (Fast Recovery):在快速重传之后,TCP进入快速恢复阶段。它将ssthresh设置为当前CWND的一半,然后CWND设置为ssthresh加上3个MSS。每收到一个重复ACK,CWND增加1个MSS。收到新的ACK后,退出快速恢复。

这种基于丢包的机制,在一定程度上解决了拥塞问题,但在高带宽、高延迟(BDP,Bandwidth-Delay Product,带宽延迟积很大的网络)环境下,其效率受到挑战。丢包意味着队列已经溢出,这是一种“亡羊补牢”式的策略,无法避免队列填充甚至溢出导致的延迟。

2. CUBIC:优化高带宽延迟积网络的先锋

2.1 CUBIC的诞生背景与核心思想

CUBIC作为Linux内核默认的TCP拥塞控制算法,出现是为了解决Reno/NewReno在BDP较大的网络中表现不佳的问题。Reno/NewReno的线性增长在面对高BDP网络时,需要很长时间才能充分利用带宽,且一旦发生丢包,直接将CWND减半,又需要很长时间才能恢复。

CUBIC的核心思想是:

  • 使用三次函数来调整拥塞窗口:在拥塞避免阶段,CUBIC不再线性增加CWND,而是使用一个三次函数来增长CWND。这个函数在距离上次丢包点较远时增长缓慢,接近上次丢包点时加速增长,然后再次放缓,形成一个“S”形曲线。
  • 独立于RTT的窗口增长:CUBIC的窗口增长主要依赖于自上次拥塞事件以来的时间,而不是像Reno那样依赖于RTT数量。这使得它在高RTT网络中也能更快地填充窗口。
  • W_max记录:CUBIC会记录上次拥塞事件发生时的CWND值 (W_max)。在恢复后,它会尝试逐渐接近并超越W_max,以探测是否有更多可用带宽。

2.2 CUBIC的工作机制

CUBIC主要包含以下几个阶段和组件:

  1. 慢启动 (Slow Start):与Reno类似,初始阶段CWND指数增长,直到达到ssthresh或发生丢包。
  2. 拥塞避免 (Congestion Avoidance)
    • 当进入拥塞避免阶段时,CUBIC会计算一个基于三次函数的CWND。
    • 核心公式:W(t) = C * (t - K)^3 + W_max
      • W(t):当前时刻t的CWND。
      • C:一个缩放因子,通常是一个较小的常数(如0.4)。
      • t:自上次拥塞事件以来的时间。
      • K:计算出的一个时间点,表示从CWND减半值增长到W_max所需的时间。K = cbrt(W_max * (1 - beta) / C),其中beta是乘性减系数(通常为0.7)。
      • W_max:上次拥塞事件发生时的CWND值。
    • 这个三次函数在 t < K 时,CWND增长较慢,远离上次拥塞点。当 t 接近 K 时,CWND增长加速,尝试快速恢复到 W_max。当 t > K 时,CWND继续增长,但增长速度再次放缓,进行更谨慎的探测。
    • 为了与Reno公平竞争,CUBIC还会计算一个TCP-Friendly窗口值 W_tcp,如果 W_cubic < W_tcp,则使用 W_tcp
  3. 拥塞事件处理 (Congestion Event)
    • 当发生丢包(收到3个重复ACK或RTO超时)时,CUBIC会记录当前CWND作为 W_max
    • 然后,CWND会进行乘性减,通常是 CWND = W_max * beta (beta通常为0.7)。同时,ssthresh也被设置为这个新的CWND值。
    • t 重置为0,开始新一轮的计时。
  4. 快速收敛 (Fast Convergence)
    • 当一个新的CUBIC流加入网络时,如果发现当前CWND W_curr 大于 W_max (即在探测新带宽时),并且之前的 W_max 已经有一个其他CUBIC流在使用,则会主动降低 W_max,以避免过度竞争,让新的流更快地获取带宽。这个机制有助于多CUBIC流在共享瓶颈时更快地达到公平。

2.3 CUBIC在内核中的实现逻辑 (概念性代码)

在Linux内核中,CUBIC的实现位于 net/ipv4/tcp_cubic.c。以下是其核心逻辑的简化和概念性描述:

// 定义CUBIC算法的结构体,包含其状态
struct cubic_state {
    u32     w_max;          // 上次拥塞事件发生时的窗口大小 (in MSS)
    u32     w_last_reset;   // 上次重置时间点 (in jiffies)
    u32     ssthresh;       // 慢启动阈值
    u32     bic_K;          // K值,用于三次函数
    u32     delay_min;      // 最小RTT,用于TCP-friendly计算
    u32     ack_cnt;        // 累计ACK计数,用于线性增长 (TCP-friendly)
    u32     cnt;            // 用于计算ack_cnt的辅助计数
    u32     epoch_start;    // 拥塞恢复后的起始时间点 (in jiffies)
    u32     offset;         // 用于快速收敛
    u32     last_cwnd;      // 用于快速收敛
    bool    found_max;      // 是否已经找到W_max
};

// CUBIC初始化函数
static void cubic_init(struct sock *sk) {
    struct cubic_state *cs = inet_csk_ca(sk);
    cs->w_max = 0;
    cs->w_last_reset = 0;
    cs->ssthresh = TCP_INFINITE_SSTHRESH; // 初始为无限大
    cs->bic_K = 0;
    cs->delay_min = 0xFFFFFFFF; // 初始为最大值
    cs->ack_cnt = 0;
    cs->cnt = 0;
    cs->epoch_start = 0;
    cs->offset = 0;
    cs->last_cwnd = 0;
    cs->found_max = false;
    // ... 其他初始化
}

// CUBIC处理ACK事件,调整CWND
static void cubic_acked(struct sock *sk, const struct ack_sample *s) {
    struct tcp_sock *tp = tcp_sk(sk);
    struct cubic_state *cs = inet_csk_ca(sk);

    // 更新最小RTT
    if (s->rtt_us > 0 && s->rtt_us < cs->delay_min) {
        cs->delay_min = s->rtt_us;
    }

    // 慢启动阶段
    if (tp->snd_cwnd <= tp->snd_ssthresh) {
        // 传统的慢启动,指数增长
        tp->snd_cwnd = min(tp->snd_cwnd + s->pkts_acked, tp->snd_ssthresh + 1);
        cs->ack_cnt = 0; // 重置ACK计数
    }
    // 拥塞避免阶段
    else {
        // 计算自上次拥塞以来的时间 't'
        u32 t = jiffies - cs->epoch_start;
        if (t == 0) t = 1; // 避免除以零

        // 计算基于三次函数的CWND (W_cubic)
        // W(t) = C * (t - K)^3 + W_max
        // 这里需要进行复杂的浮点运算或定点数模拟
        // 简化:假设cubic_function_calc 返回新的CWND增量
        u32 target_cwnd = cubic_function_calc(cs->w_max, cs->bic_K, t, CUBIC_C);

        // 与TCP-Friendly窗口比较
        // W_tcp = (3/2 * W_max * beta) / (RTT * sqrt(C))
        // 简化:假设tcp_friendly_calc 返回新的CWND增量
        u32 tcp_friendly_cwnd = tcp_friendly_calc(tp->snd_cwnd, cs->delay_min);

        // 选择更大的窗口,但不能超过最大值
        if (target_cwnd > tcp_friendly_cwnd) {
            tp->snd_cwnd = min(tp->snd_cwnd + (target_cwnd - tp->snd_cwnd), tp->snd_cwnd + s->pkts_acked);
        } else {
            // 如果TCP-Friendly更大,则采用线性增长
            // cwnd = cwnd + s->pkts_acked * (1 / cwnd)
            cs->ack_cnt += s->pkts_acked;
            if (cs->ack_cnt >= tp->snd_cwnd) {
                tp->snd_cwnd++;
                cs->ack_cnt = 0;
            }
        }
    }
}

// CUBIC处理拥塞事件 (丢包或RTO)
static u32 cubic_cong_avoid(struct sock *sk, u32 ack, u32 rtt, u32 in_flight, bool loss) {
    struct tcp_sock *tp = tcp_sk(sk);
    struct cubic_state *cs = inet_csk_ca(sk);

    if (!loss) { // 如果不是丢包,则只是更新W_max
        if (tp->snd_cwnd > cs->w_max) {
            cs->w_max = tp->snd_cwnd;
            cs->found_max = true;
        }
        return tp->snd_cwnd;
    }

    // 发生丢包,记录W_max并进行乘性减
    if (tp->snd_cwnd < cs->w_max) {
        // 快速收敛机制:如果当前CWND小于历史W_max,说明之前可能过度探测,
        // 降低W_max以让其他流有机会
        cs->w_max = tp->snd_cwnd; // 调整W_max
    } else {
        cs->w_max = tp->snd_cwnd;
    }
    cs->found_max = true; // 找到了新的W_max

    // 计算K值: K = cbrt(W_max * (1 - beta) / C)
    // 简化:假设cubic_calc_K 返回计算好的K
    cs->bic_K = cubic_calc_K(cs->w_max, CUBIC_BETA, CUBIC_C);

    // 更新ssthresh和CWND
    tp->snd_ssthresh = tp->snd_cwnd * CUBIC_BETA; // 通常beta为0.7
    tp->snd_cwnd = tp->snd_ssthresh; // CWND直接减半 (或乘0.7)

    cs->epoch_start = jiffies; // 重置时间起点
    cs->ack_cnt = 0; // 重置ACK计数
    return tp->snd_cwnd;
}

注意: 上述代码是高度简化的概念性伪代码,实际内核实现涉及大量的细节,如定点数运算、计时器管理、状态机转换、各种边缘情况处理等。但它展示了CUBIC的核心逻辑:慢启动、基于三次函数的拥塞避免、以及拥塞事件处理。

2.4 CUBIC的优缺点

优点:

  • 高带宽利用率:在高BDP网络中,CUBIC能够更快地达到并维持高吞吐量,因为它不再依赖于RTT线性增长,且在拥塞点附近增长更快。
  • 稳定性:其三次函数在远离拥塞点时增长缓慢,有助于网络稳定。
  • TCP友好性:通过与TCP-Friendly窗口的比较,在一定程度上保证了与Reno等传统算法的公平性。
  • 广泛部署:作为Linux内核的默认算法,已在大规模生产环境中得到验证。

缺点:

  • 仍然基于丢包:CUBIC仍然将丢包作为主要的拥塞信号。这意味着在检测到拥塞之前,网络队列可能已经严重堆积,导致延迟增加(Bufferbloat问题)。
  • 对短流不友好:对于传输少量数据的短连接,CUBIC的探测机制可能不够迅速,导致吞吐量不足。
  • 队列填充:为了探测带宽,CUBIC会倾向于填充网络队列,这在高并发场景下可能加剧延迟。

3. Google BBR:基于带宽和RTT的拥塞控制革命

3.1 BBR的诞生背景与核心思想

Google BBR (Bottleneck Bandwidth and RTT) 是Google在2016年提出的一种全新的拥塞控制算法,其设计理念与传统的基于丢包的算法截然不同。BBR的诞生旨在解决CUBIC等算法在高BDP、高丢包率或存在Bufferbloat问题的网络中表现不佳的问题。

BBR的核心思想是:

  • 不依赖丢包作为主要拥塞信号:BBR不等待丢包发生,而是通过持续测量网络路径的瓶颈带宽 (Bottleneck Bandwidth, BtlBw)最小往返传播时间 (Minimum Round-Trip Propagation Time, RTprop) 来构建网络模型。
  • 直接驱动网络到其模型容量:BBR的目标是将发送速率保持在测得的BtlBw,并将在途数据量 (In-flight Data) 保持在BtlBw * RTprop (即BDP) 附近。这就像一个水管,BBR的目标是让水流速度达到水管的最大流量,同时只在水管中保持刚好充满水管的水量,不多也不少。
  • Pacing (定速发送):BBR通过精确的定速发送机制,将数据包平滑地注入网络,而不是像传统TCP那样以突发方式发送。

3.2 BBR的工作机制:四大状态机

BBR通过一个四状态的有限状态机来动态调整发送行为:

  1. STARTUP (启动)

    • 目标:快速探测并发现路径的真实BtlBw。
    • 行为:指数级增加发送速率 (pacing_rate),通常以gain=2/ln(2) (约2.89) 的因子倍增。同时,拥塞窗口 (CWND) 设置为gain * BDP,以确保有足够的在途数据来探测带宽。
    • 退出条件:当观测到的带宽增速停止或显著放缓时(例如,在一段时间内带宽没有显著增长),认为已经找到了瓶颈带宽,或队列开始堆积,进入DRAIN状态。
  2. DRAIN (排空)

    • 目标:在STARTUP阶段可能填充了过多的队列,DRAIN阶段旨在排空这些额外的数据,使在途数据量回到BDP大小。
    • 行为:将发送速率设置为1/gain (约0.34) 倍的当前BtlBw,即显著降低发送速率。CWND也相应调整。
    • 退出条件:当在途数据量下降到 BDP (即 BtlBw * RTprop) 大小,或者观测到的RTT降到接近RTprop时,进入PROBE_BW状态。
  3. PROBE_BW (探测带宽)

    • 目标:在维持高吞吐量的同时,周期性地探测是否有更高的可用带宽。这是BBR的“拥塞避免”阶段。
    • 行为:
      • 在一个RTT周期内,BBR会在几个子阶段之间切换,通过周期性地小幅增加发送速率 (例如 pacing_gain = 1.25) 来探测更高的BtlBw,并在其他时候降低发送速率 (例如 pacing_gain = 0.75) 来排空队列,以避免持续填充队列。
      • CWND通常设置为 BDP + 2 * MSS,以确保有足够的在途数据来维持BtlBw,并允许少量额外的探测。
    • 退出条件:周期性地进入PROBE_RTT状态,或者当观测到新的最小RTT时,重置探测周期。
  4. PROBE_RTT (探测RTT)

    • 目标:周期性地测量网络路径的最小RTT (RTprop)。由于队列可能被其他流量填满,导致RTT测量不准确,所以需要定期排空队列来获取真实的RTprop。
    • 行为:将拥塞窗口 CWND 降低到 4 * MSS (或更小),持续至少 200ms。这会强制排空网络中的所有队列,从而得到真实的RTprop。
    • 退出条件:200ms结束后,恢复到PROBE_BW状态。

3.3 BBR的关键测量值

  • BtlBw (Bottleneck Bandwidth):BBR通过观察在每个ACK到达时,已发送数据量除以时间来估算带宽。它会记住在过去一段时间内观察到的最大带宽值。
  • RTprop (Round-Trip Propagation Time):BBR记录在过去一段时间内观测到的最小RTT值。这是实际的传播延迟,不受队列排队延迟的影响。

3.4 BBR在内核中的实现逻辑 (概念性代码)

BBR的实现同样位于Linux内核中,文件为 net/ipv4/tcp_bbr.c。其核心逻辑比CUBIC更为复杂,因为它涉及到更精细的状态管理、带宽和RTT的持续测量、以及定速发送。

// 定义BBR算法的结构体,包含其状态和测量值
struct bbr {
    u32     state;              // 当前BBR状态 (STARTUP, DRAIN, PROBE_BW, PROBE_RTT)
    u32     pacing_gain;        // 发送速率增益
    u32     cwnd_gain;          // 拥塞窗口增益

    u32     full_bw;            // STARTUP阶段发现的完整带宽
    u32     full_bw_cnt;        // 探测到full_bw的次数
    u32     full_bw_thresh;     // 判定带宽饱和的阈值

    u32     min_rtt_us;         // 最小RTT (RTprop)
    u64     min_rtt_stamp;      // 测量min_rtt的时间戳

    u64     bw_hi;              // 历史最大带宽
    u64     bw_lo;              // 历史最小带宽 (用于PROBE_BW的探测)
    u64     bw;                 // 当前估计的带宽

    u32     probe_rtt_min_cwnd; // PROBE_RTT阶段的最小CWND
    u64     probe_rtt_stamp;    // PROBE_RTT开始时间戳
    bool    probe_rtt_done_stamp; // PROBE_RTT是否完成
    u32     probe_rtt_round_done; // PROBE_RTT是否完成一个回合

    u32     round_cnt;          // 记录RTT回合数
    u32     next_round_delivered; // 下一个回合结束时已发送的数据量

    u32     cycle_idx;          // PROBE_BW探测周期索引
    u32     cycle_stamp;        // PROBE_BW周期开始时间戳

    u32     max_inflight;       // 最大在途数据量限制

    // ... 其他辅助变量和定时器
};

// BBR初始化函数
static void bbr_init(struct sock *sk) {
    struct tcp_sock *tp = tcp_sk(sk);
    struct bbr *bbr = inet_csk_ca(sk);

    bbr->state = BBR_STARTUP;
    bbr->pacing_gain = BBR_HIGH_GAIN; // STARTUP阶段增益
    bbr->cwnd_gain = BBR_HIGH_GAIN;

    bbr->full_bw = 0;
    bbr->full_bw_cnt = 0;
    bbr->full_bw_thresh = 3; // 默认3次探测无显著带宽提升则认为饱和

    bbr->min_rtt_us = 0xFFFFFFFF; // 初始最大值
    bbr->min_rtt_stamp = jiffies;

    bbr->bw_hi = 0;
    bbr->bw_lo = 0;
    bbr->bw = 0; // 当前估计带宽

    bbr->probe_rtt_min_cwnd = tp->mss_cache * 4; // PROBE_RTT的最小CWND
    bbr->probe_rtt_stamp = 0;
    bbr->probe_rtt_done_stamp = false;
    bbr->probe_rtt_round_done = false;

    bbr->round_cnt = 0;
    bbr->next_round_delivered = 0;

    bbr->cycle_idx = 0;
    bbr->cycle_stamp = jiffies;

    bbr->max_inflight = bbr_initial_cwnd(tp); // 初始在途数据量
    // ... 其他初始化
}

// BBR处理ACK事件,更新测量值并调整状态/发送速率
static void bbr_acked(struct sock *sk, const struct ack_sample *s) {
    struct tcp_sock *tp = tcp_sk(sk);
    struct bbr *bbr = inet_csk_ca(sk);

    // 1. 更新最小RTT (RTprop)
    if (s->rtt_us > 0 && s->rtt_us < bbr->min_rtt_us) {
        bbr->min_rtt_us = s->rtt_us;
        bbr->min_rtt_stamp = jiffies;
    }
    // 定期重置min_rtt_us,以适应网络变化
    if (jiffies - bbr->min_rtt_stamp > BBR_PROBE_RTT_INTERVAL) {
        bbr->min_rtt_us = 0xFFFFFFFF;
    }

    // 2. 更新带宽估计 (BtlBw)
    // 根据s->delivered_time和s->delivered_ce_count计算瞬时带宽
    // 维护一个滑动窗口的最大带宽值
    bbr_update_bandwidth_estimate(bbr, s);

    // 3. 更新RTT回合计数 (用于BBR状态机)
    bbr_update_round(bbr, s->delivered);

    // 4. BBR状态机转换
    switch (bbr->state) {
        case BBR_STARTUP:
            // 检测带宽是否饱和,即带宽不再显著增长
            if (bbr_is_full_bandwidth(bbr)) {
                bbr->state = BBR_DRAIN;
                bbr->pacing_gain = BBR_DRAIN_GAIN; // 通常是1/HIGH_GAIN
                bbr->cwnd_gain = BBR_HIGH_GAIN;
            }
            break;

        case BBR_DRAIN:
            // 检测在途数据是否排空,即RTT是否接近min_rtt
            if (tcp_in_flight(tp) <= bbr_bdp(bbr) ||
                (jiffies - bbr->min_rtt_stamp > BBR_PROBE_RTT_INTERVAL &&
                 bbr->round_cnt > 0 && bbr->min_rtt_us != 0xFFFFFFFF)) {
                bbr->state = BBR_PROBE_BW;
                bbr_set_probe_bw_gains(bbr); // 设置PROBE_BW的循环增益
            }
            break;

        case BBR_PROBE_BW:
            // 周期性探测更高带宽,或进入PROBE_RTT
            bbr_update_probe_bw_state(bbr);
            if (bbr_needs_probe_rtt(bbr)) { // 如果需要探测RTT
                bbr->state = BBR_PROBE_RTT;
                bbr->pacing_gain = 1;
                bbr->cwnd_gain = 1;
                bbr->probe_rtt_stamp = jiffies;
                bbr->probe_rtt_done_stamp = false;
            }
            break;

        case BBR_PROBE_RTT:
            // 维持低CWND一段时间,测量RTprop
            if (jiffies - bbr->probe_rtt_stamp > BBR_PROBE_RTT_DURATION) {
                bbr->probe_rtt_done_stamp = true;
                // 恢复到PROBE_BW状态
                bbr->state = BBR_PROBE_BW;
                bbr_set_probe_bw_gains(bbr);
            }
            break;
    }

    // 5. 根据当前状态和测量值,调整pacing_rate和CWND
    tcp_set_pacing_rate(sk, bbr_pacing_rate(bbr)); // 设置定速发送速率
    tp->snd_cwnd = bbr_cwnd(bbr); // 计算并设置拥塞窗口
}

// BBR处理拥塞事件 (丢包或RTO)
// BBR不将丢包作为主要拥塞信号,但仍然需要处理
static void bbr_cong_avoid(struct sock *sk, u32 ack, u32 rtt, u32 in_flight, bool loss) {
    struct bbr *bbr = inet_csk_ca(sk);
    struct tcp_sock *tp = tcp_sk(sk);

    if (loss) {
        // BBR对丢包不敏感,通常不会大幅降低CWND,
        // 但会确保在途数据量不超过BDP,并继续进行带宽探测
        // 可能会触发BBR_PROBE_RTT
        bbr_handle_loss(bbr, tp->snd_cwnd, in_flight);
    }
    // BBR的CWND主要由bbr_acked中的逻辑计算,不在此处做传统的拥塞避免
}

注意: 上述代码同样是高度简化的概念性伪代码。BBR的实际实现非常精巧,涉及到复杂的带宽滤波器、RTT抖动处理、精确的计时器、定点数运算以及各种边缘情况的优化。但核心的测量、状态转换和基于模型调整发送速率的逻辑是清晰的。

3.5 BBR的优缺点

优点:

  • 高吞吐量、低延迟:通过直接测量BtlBw和RTprop,BBR能够避免队列填充,从而显著降低端到端延迟,同时最大化带宽利用率。
  • 鲁棒性强:对丢包不敏感,在丢包率较高的网络中表现依然出色,因为它不将丢包作为拥塞的主要信号。
  • 适应性好:能够很好地适应各种网络环境,包括长肥管道、无线网络、数据中心等。
  • 避免Bufferbloat:通过将在途数据量保持在BDP附近,BBR有效避免了路由器队列的过度填充。

缺点:

  • 公平性挑战:在与CUBIC等基于丢包的算法竞争时,BBR可能会表现得过于激进,倾向于“抢占”带宽,导致其他流饿死。这是因为CUBIC会因丢包而退让,而BBR则不会。
  • 实现复杂性:BBR的算法逻辑比CUBIC复杂得多,需要精确的带宽和RTT测量,以及精细的状态机管理。
  • 对瞬时网络变化敏感:某些情况下,瞬时的测量误差可能导致BBR行为不稳定。
  • 初期探测可能不够激进:在某些极端高带宽场景下,BBR的STARTUP阶段可能不如CUBIC的指数增长那样迅速填满管道。

4. CUBIC与Google BBR的比较与共存

下表总结了CUBIC和BBR的主要特点和差异:

特性 CUBIC Google BBR
拥塞信号 丢包 (Loss-based) 带宽和RTT测量 (Model-based)
控制目标 避免丢包,最大化吞吐量 最大化BtlBw,最小化RTprop
窗口增长 三次函数 (拥塞避免) & 指数 (慢启动) 基于BDP和增益,通过pacing_rate控制
在途数据量 倾向于填充队列,直到丢包 尽量保持在BDP附近 (BtlBw * RTprop)
延迟表现 可能导致高延迟 (Bufferbloat) 通常保持低延迟,避免Bufferbloat
带宽利用率 在高BDP网络中表现良好,但仍受丢包限制 高效利用带宽,尤其是在高丢包和高延迟网络
公平性 与Reno等算法相对公平 与丢包算法竞争时可能过于激进
实现复杂度 相对简单 复杂,需要精确测量和状态管理
典型应用场景 传统有线网络,对延迟不极度敏感的场景 高BDP、高丢包、无线网络、数据中心,对延迟敏感

4.1 内核中的选择与配置

在Linux系统中,用户可以通过 sysctl 命令来查看和配置当前使用的TCP拥塞控制算法。

# 查看当前默认的拥塞控制算法
sysctl net.ipv4.tcp_congestion_control
# 输出示例:net.ipv4.tcp_congestion_control = cubic

# 查看系统支持的所有拥塞控制算法
sysctl net.ipv4.tcp_available_congestion_control
# 输出示例:net.ipv4.tcp_available_congestion_control = bbr cubic reno

要更改默认的拥塞控制算法,可以:

  1. 临时修改
    sudo sysctl -w net.ipv4.tcp_congestion_control=bbr
  2. 永久修改
    编辑 /etc/sysctl.conf 文件,添加或修改以下行:

    net.ipv4.tcp_congestion_control = bbr

    然后运行 sudo sysctl -p 使其生效。

对于特定的路由规则,也可以指定拥塞控制算法:

sudo ip route change default via <gateway_ip> congctl bbr

4.2 共存与未来

CUBIC和BBR各自有其优势和适用场景。CUBIC作为传统的优秀算法,在许多网络环境中表现稳定。BBR则代表了拥塞控制的未来方向,通过更智能的网络模型感知,在复杂的网络条件下提供了卓越的性能。

在混合网络环境中,BBR与CUBIC等基于丢包的算法之间的公平性仍然是一个活跃的研究领域。Google已经发布了BBRv2版本,旨在解决与传统算法共存时的公平性问题,并进一步提升性能。未来的拥塞控制算法可能会更加智能化,结合机器学习等技术,实现更细粒度的网络状态感知和更动态的策略调整。

5. 展望:持续演进的拥塞控制

TCP拥塞控制是网络协议栈中最具挑战性也最活跃的研究领域之一。从最初的AIMD,到CUBIC的三次函数,再到BBR的模型驱动,每一次迭代都深刻地影响了互联网的性能和用户体验。随着5G、卫星互联网、边缘计算等新技术的普及,网络环境将变得更加复杂多样,对拥塞控制算法提出了更高的要求。我们期待未来能看到更多创新的算法出现,它们将更智能地适应瞬息万变的网络条件,为全球用户提供更快、更稳定、更低延迟的网络服务。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注