快速恢复算法PRR

  PRR算法(Proportional Rate Reduction)决定在丢包恢复(Loss Recovery)期间,对应于每个ACK报文,可发送的报文数量。目的是:1)快速平稳的从Loss中恢复;2)恢复之后拥塞窗口收敛与ssthresh。主要是为了解决Linux内核之前采用的恢复算法Rate-halving存在的一些弊端:

  • 在恢复阶段,为防止burst发送,内核将拥塞窗口设置为pipe+1,然而,如果由于应用程序没有数据可发送,最早将导致拥塞窗口降低为1,即使仅丢失了一个报文。
  • 在恢复之后,将拥塞窗口降低太多(一半甚至更低),但是当前内核的默认Cubic算法将ssthresh降至之前拥塞窗口的70%,太低的拥塞窗口将造成性能损失。
  • ACK报文的丢失,将导致更少报文的发送。
  • 在丢失多个报文时,容易触发RTO超时。

 

PRR的核心思想是基于ACK确认的反馈来动态调整发送速率,确保发送窗口不会超出网络实际处理能力,同时尽量避免发送窗口低估带宽。

  1. 发送窗口大小根据接收到的ACK动态调整,确保拥塞窗口(cwnd)和流量控制窗口(rwnd)保持适当的比例。当网络中报文数量(pipe)大于ssthresh时,通常是丢失较少报文时的恢复开始阶段的情况,根据ACK报文的到达成比例的降低拥塞窗口。例如对于Cubic算法,此部分通过每接收到10个ACK确认报文(10报文被对端所接收),发送7个报文的方式,将拥塞窗口降低30%(等于Cubic设置的ssthresh值)
  2. 如果网络中报文数量(pipe)小于ssthresh,通常是丢失多个报文或者应用程序在恢复阶段没有数据可发送的情况,阻止拥塞窗口的降低。RFC6937中定义了两种Reduction Bound算法:Conservative Reduction Bound (CRB)和Slow Start Reduction Bound (SSRB),
    1. PRR-CRB(Conservative Reduction Bound)严格遵守报文守恒机制;
    2.  PRR-SSRB(Slow Start Reduction Bound)类似SlowStart,对于每个接收到的ACK报文,SSRB允许额外多发送一个数据报文。

 

PRR初始化

/* The cwnd reduction in CWR and Recovery uses the PRR algorithm in RFC 6937.
 * It computes the number of packets to send (sndcnt) based on packets newly
 * delivered:
 *   1) If the packets in flight is larger than ssthresh, PRR spreads the
 *    cwnd reductions across a full RTT.
 *   2) Otherwise PRR uses packet conservation to send as much as delivered.
 *      But when the retransmits are acked without further losses, PRR
 *      slow starts cwnd up to ssthresh to speed up the recovery.

 在进入TCP_CA_Recovery或者TCP_CA_CWR拥塞状态时,调用函数tcp_init_cwnd_reduction
 初始化PRR相关参数。prr_delivered记录在进入Recovery/CWR状态后接收端收到的报文数量,
 而变量prr_out用于统计进入Recovery之后,发送的报文数量
 */
static void tcp_init_cwnd_reduction(struct sock *sk)
{
    struct tcp_sock *tp = tcp_sk(sk);

    tp->high_seq = tp->snd_nxt;
    tp->tlp_high_seq = 0;
    tp->snd_cwnd_cnt = 0;
    tp->prior_cwnd = tp->snd_cwnd;//拥塞前的窗口大小,在进入拥塞恢复时记录的值
    tp->prr_delivered = 0;
    tp->prr_out = 0;
    //snd_ssthresh为拥塞算法;计算的ssthresh值,最终,拥塞窗口将收敛与此值。
    //默认Cubic,参见函数bictcp_recalc_ssthresh)
    tp->snd_ssthresh = inet_csk(sk)->icsk_ca_ops->ssthresh(sk);
    tcp_ecn_queue_cwr(tp);
}

 

PRR更新cwnd

/*
On every ACK during recovery compute:
     RecoverFS = snd.nxt-snd.una // FlightSize at the start of recovery
     DeliveredData = change_in(snd.una) + change_in(SACKd)
     prr_delivered += DeliveredData
     pipe = (RFC 6675 pipe algorithm)
     if (pipe > ssthresh) {
        // Proportional Rate Reduction
        sndcnt = CEIL(prr_delivered * ssthresh / RecoverFS) - prr_out
     } else {
        // Two versions of the Reduction Bound
        if (conservative) {    // PRR-CRB
          limit = prr_delivered - prr_out
        } else {               // PRR-SSRB
          limit = MAX(prr_delivered - prr_out, DeliveredData) + MSS
        }
        // Attempt to catch up, as permitted by limit
        sndcnt = MIN(ssthresh - pipe, limit)
     }

  On any data transmission or retransmission:

     prr_out += (data sent) // strictly less than or equal to sndcnt

*/

 

 

   进入Recovery时的拥塞窗口prior_cwnd表示算法中的RecoverFS的值,变量dividend首先增加了prior_cwnd-1的值,在除去prior_cwnd,达到了算法中CEIL的效果。此阶段需要等比例的减小拥塞窗口,比例为:snd_ssthresh/prior_cwnd

 tp->prr_delivered :PRR 期间累计的被确认数据量

tp->prr_out:PRR 期间发送的报文段数量,用于确保发送速率不会超过目标。

  如果pipe大于snd_ssthresh,即delta小于零执行PRR算法的第一个成比例部分,如下为RFC6937给出的算法:进入Recovery时的拥塞窗口prior_cwnd表示算法中的RecoverFS的值,
变量dividend首先增加了prior_cwnd-1的值,在除去prior_cwnd,达到了算法中CEIL的效果.此阶段需要等比例的减小拥塞窗口,比例为:snd_ssthresh/prior_cwnd。
  如果当前ACK报文确认了之前重传的数据,并且没有进一步标记新的重传数据的丢失,使用PRR算法的第二部分计算发送数量。 RFC6937中定义的PRR算法的第二部分如下(linux使用PRR-SSRB部分)。 如果由于应用程序缺少报文发送将导致prr_delivered的值大于prr_out的值,
内核取其与新确认报文数量newly_acked_sacked值,两者之间的最大值,另外再加上1(MSS),最后,取以上结果和delta之间的较小值。此阶段增加拥塞窗口,趋近于ssthresh。
  如果当前ACK报文没有确认重传数据,确认的为正常数据,或者确认了新的重传数据的丢失,发送数量设定类似于PRR-CRB,不同点在于内核使用delta与newly_acked_sacked之间的最小值。
  而PRR-CRB使用的是delta与prr_delivered - tp->prr_out的差值之间的最小值。 此种情况下,报文发送数量不像PRR-SSRB激进,原因是一方面丢失报文较少;或者另一方面,丢失报文较多,网络拥塞严重
void tcp_cwnd_reduction(struct sock *sk, int newly_acked_sacked, int flag)
{
    struct tcp_sock *tp = tcp_sk(sk);
    int sndcnt = 0;
    int delta = tp->snd_ssthresh - tcp_packets_in_flight(tp);

    if (newly_acked_sacked <= 0 || WARN_ON_ONCE(!tp->prior_cwnd))
        return;

    tp->prr_delivered += newly_acked_sacked;
    if (delta < 0) {//如果pipe大于snd_ssthresh,即delta小于零
    /*执行PRR算法的第一个成比例部分,如下为RFC6937给出的算法:
    进入Recovery时的拥塞窗口prior_cwnd表示算法中的RecoverFS的值,
    变量dividend首先增加了prior_cwnd-1的值,在除去prior_cwnd,达到了算法中CEIL的效果.
    此阶段需要等比例的减小拥塞窗口,比例为:snd_ssthresh/prior_cwnd。
    
    */
        u64 dividend = (u64)tp->snd_ssthresh * tp->prr_delivered +
                   tp->prior_cwnd - 1;
        sndcnt = div_u64(dividend, tp->prior_cwnd) - tp->prr_out;
    } else if ((flag & (FLAG_RETRANS_DATA_ACKED | FLAG_LOST_RETRANS)) ==
           FLAG_RETRANS_DATA_ACKED) {
           /*如果当前ACK报文确认了之前重传的数据,并且没有进一步标记新的重传数据的丢失,使用PRR算法的第二部分计算发送数量。
    RFC6937中定义的PRR算法的第二部分如下(linux使用PRR-SSRB部分)。
    如果由于应用程序缺少报文发送将导致prr_delivered的值大于prr_out的值,
    内核取其与新确认报文数量newly_acked_sacked值,两者之间的最大值,另外再加上1(MSS),
    最后,取以上结果和delta之间的较小值。此阶段增加拥塞窗口,趋近于ssthresh。*/
        sndcnt = min_t(int, delta,
                   max_t(int, tp->prr_delivered - tp->prr_out,
                     newly_acked_sacked) + 1);
    } else {
        /*如果当前ACK报文没有确认重传数据,确认的为正常数据,或者确认了新的重传数据的丢失,
    发送数量设定类似于PRR-CRB,不同点在于内核使用delta与newly_acked_sacked之间的最小值。
    而PRR-CRB使用的是delta与prr_delivered - tp->prr_out的差值之间的最小值。
    此种情况下,报文发送数量不像PRR-SSRB激进,
    原因是一方面丢失报文较少;或者另一方面,丢失报文较多,网络拥塞严重
    */
        sndcnt = min(delta, newly_acked_sacked);
    }
    /* Force a fast retransmit upon entering fast recovery */
    /* 在进入快速恢复阶段时,强制发送至少一个报文(此时prr_out为零)。 */
    sndcnt = max(sndcnt, (tp->prr_out ? 0 : 1));
    tp->snd_cwnd = tcp_packets_in_flight(tp) + sndcnt;
}

 但是对新的6.8内核上述是5.10内核,其逻辑复合rfc文档 https://git.kernel.org/pub/scm/linux/kernel/git/netdev/net-next.git/commit/net?id=7e901ee7b6ab0b7c1a5e29b8513af23709285a29

 

During TCP fast recovery, the congestion control in charge is by
default the Proportional Rate Reduction (PRR) unless the congestion
control module specified otherwise (e.g. BBR).

Previously when tcp_packets_in_flight() is below snd_ssthresh PRR
would slow start upon receiving an ACK that
   1) cumulatively acknowledges retransmitted data
   and
   2) does not detect further lost retransmission

Such conditions indicate the repair is in good steady progress
after the first round trip of recovery. Otherwise PRR adopts the
packet conservation principle to send only the amount that was
newly delivered (indicated by this ACK).

This patch generalizes the previous design principle to include
also the newly sent data beside retransmission: as long as
the delivery is making good progress, both retransmission and
new data should be accounted to make PRR more cautious in slow
starting.

 

也就是both retransmission and new data should be accounted to make PRR more cautious in slow starting.

所以 newly_lost==0 && snd_una_advanced的时候sdncnt++

 

void tcp_cwnd_reduction(struct sock *sk, int newly_acked_sacked, int newly_lost, int flag)
{
    struct tcp_sock *tp = tcp_sk(sk);
    int sndcnt = 0;
    int delta = tp->snd_ssthresh - tcp_packets_in_flight(tp);

    if (newly_acked_sacked <= 0 || WARN_ON_ONCE(!tp->prior_cwnd))
        return;

    tp->prr_delivered += newly_acked_sacked;
    if (delta < 0) {
        u64 dividend = (u64)tp->snd_ssthresh * tp->prr_delivered +
                   tp->prior_cwnd - 1;
        sndcnt = div_u64(dividend, tp->prior_cwnd) - tp->prr_out;
    } else {
        sndcnt = max_t(int, tp->prr_delivered - tp->prr_out,
                   newly_acked_sacked);
        if (flag & FLAG_SND_UNA_ADVANCED && !newly_lost)
            sndcnt++;
        sndcnt = min(delta, sndcnt);
    }
    /* Force a fast retransmit upon entering fast recovery */
    sndcnt = max(sndcnt, (tp->prr_out ? 0 : 1));
    tcp_snd_cwnd_set(tp, tcp_packets_in_flight(tp) + sndcnt);
}

 

 

 prr-ss-30pkt-lost1_4.pkt 对应的报文

 

 

posted @ 2024-12-16 18:59  codestacklinuxer  阅读(128)  评论(0)    收藏  举报