TCP快速恢复算法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由两个部分组成。一是当网络中报文数量(pipe)大于ssthresh时,通常是丢失较少报文时的恢复开始阶段的情况,根据ACK报文的到达成比例的降低拥塞窗口。例如对于Cubic算法,此部分通过每接收到10个ACK确认报文(10报文被对端所接收),发送7个报文的方式,将拥塞窗口降低30%(等于Cubic设置的ssthresh值)。

第二个部分是,如果网络中报文数量(pipe)小于ssthresh,通常是丢失多个报文或者应用程序在恢复阶段没有数据可发送的情况,阻止拥塞窗口的降低。RFC6937中定义了两种Reduction Bound算法:Conservative Reduction Bound (CRB)和Slow Start Reduction Bound (SSRB),前者严格遵守报文守恒机制;而后者类似SlowStart,比CRB更具侵略性,对于每个接收到的ACK报文,SSRB允许额外多发送一个数据报文。

PRR初始化

在进入TCP_CA_Recovery或者TCP_CA_CWR拥塞状态时,调用函数tcp_init_cwnd_reduction初始化PRR相关参数。prr_delivered记录在进入Recovery/CWR状态后接收端收到的报文数量,而变量prr_out用于统计进入Recovery之后,发送的报文数量。

snd_ssthresh为拥塞算法(默认Cubic,参见函数bictcp_recalc_ssthresh)计算的ssthresh值,最终,拥塞窗口将收敛与此值。

static void tcp_init_cwnd_reduction(struct sock *sk)
{
    struct tcp_sock *tp = tcp_sk(sk);

    ...
    tp->prior_cwnd = tp->snd_cwnd;
    tp->prr_delivered = 0;
    tp->prr_out = 0;
    tp->snd_ssthresh = inet_csk(sk)->icsk_ca_ops->ssthresh(sk);
    tcp_ecn_queue_cwr(tp);

PRR更新cwnd

如下函数tcp_cwnd_reduction,如果pipe大于snd_ssthresh,即delta小于零,执行PRR算法的第一个成比例部分,如下为RFC6937给出的算法:

    RecoverFS = snd.nxt-snd.una // FlightSize at the start of recovery
    
    On every ACK during recovery compute:
    
    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
    }

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

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) {
        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) {
        sndcnt = min_t(int, delta,
                   max_t(int, tp->prr_delivered - tp->prr_out,
                     newly_acked_sacked) + 1);
    } else {
        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;

如果当前ACK报文确认了之前重传的数据,并且没有进一步标记新的重传数据的丢失,使用PRR算法的第二部分计算发送数量。

RFC6937中定义的PRR算法的第二部分如下(else部分,linux使用PRR-SSRB部分)。如果由于应用程序缺少报文发送将导致prr_delivered的值大于prr_out的值,内核取其与新确认报文数量newly_acked_sacked值,两者之间的最大值,另外再加上1(MSS),最后,取以上结果和delta之间的较小值。此阶段增加拥塞窗口,趋近于ssthresh。

相比于PRR-CRB,SSRB的加1操作,并非严格遵守报文守恒原则,

    if (pipe > ssthresh) {
        ...
    } 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)
    }

最后,如果当前ACK报文没有确认重传数据,确认的为正常数据,或者确认了新的重传数据的丢失,发送数量设定类似于PRR-CRB,不同点在于内核使用delta与newly_acked_sacked之间的最小值。而PRR-CRB使用的是delta与prr_delivered - tp->prr_out的差值之间的最小值。此种情况下,报文发送数量不像PRR-SSRB激进,原因是一方面丢失报文较少;或者另一方面,丢失报文较多,网络拥塞严重。

    sndcnt = min(delta, newly_acked_sacked);

以上函数tcp_cwnd_reduction在tcp_ack处理完成ACK报文之后,在函数tcp_cong_control中调用。注意BBR拥塞算法(实现了自身的cong_control控制)不使用PRR。

static void tcp_cong_control(struct sock *sk, u32 ack, u32 acked_sacked,
                 int flag, const struct rate_sample *rs)
{
    const struct inet_connection_sock *icsk = inet_csk(sk);

    if (icsk->icsk_ca_ops->cong_control) {
        icsk->icsk_ca_ops->cong_control(sk, rs);
        return;
    }

    if (tcp_in_cwnd_reduction(sk)) {
        /* Reduce cwnd if state mandates */
        tcp_cwnd_reduction(sk, acked_sacked, flag);

更新prr_out

变量prr_out记录CWR/Recovery拥塞状态发送的报文数量,内核使用函数tcp_in_cwnd_reduction判断套接口是否处于CWR/Recovery拥塞状态。

static inline bool tcp_in_cwnd_reduction(const struct sock *sk)
{
    return (TCPF_CA_CWR | TCPF_CA_Recovery) &
           (1 << inet_csk(sk)->icsk_ca_state);
}

如下TCP报文发送函数tcp_write_xmit,如果套接口处于CWR/Recovery拥塞状态,增加prr_out的值。

static bool tcp_write_xmit(struct sock *sk, unsigned int mss_now, int nonagle,
               int push_one, gfp_t gfp)
{
    ...
    if (likely(sent_pkts)) {
        if (tcp_in_cwnd_reduction(sk))
            tp->prr_out += sent_pkts;

如下TCP报文重传函数tcp_xmit_retransmit_queue,如果套接口处于CWR/Recovery拥塞状态,增加prr_out的值。

void tcp_xmit_retransmit_queue(struct sock *sk)
{
    ...
    rtx_head = tcp_rtx_queue_head(sk);
    skb = tp->retransmit_skb_hint ?: rtx_head;
    max_segs = tcp_tso_segs(sk, tcp_current_mss(sk));
    skb_rbtree_walk_from(skb) {

        if (tcp_retransmit_skb(sk, skb, segs))
            return;

        NET_ADD_STATS(sock_net(sk), mib_idx, tcp_skb_pcount(skb));

        if (tcp_in_cwnd_reduction(sk))
            tp->prr_out += tcp_skb_pcount(skb);

内核版本 5.0

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页