聊一聊tcp 拥塞控制 六 RACk

一、RACK概述 

  RACK(Recent ACKnowledgment)是一种新的基于时间的丢包探测算法,RACK的目的是取代传统的基于dupthresh门限的各种快速重传及其变种。前面介绍的各种基于dup ACK的快速重传算法及其变种通过修改dupthresh门限等手段,有些可以迅速的探测到丢包,有些可以精确的探测丢包,但是没有能同时达到迅速和精确两个目标的算法。

但是尾部丢包的问题怎么解决???

  目前RACK相比FACK/SACK  主要用于解决尾部丢包和二次重传的问题,但是它应该可以FACK,dupACK并驾齐驱,理解起来也比较直观。不过对于流尾部丢包,也需要一个额外的定时器来辅助。

  RACK基本思想:如果发送端收到的确认包中的SACK选项确认收到了一个数据包,那么在这个数据包之前发送的数据包要么是在传输过程中发生了乱序,要么是发生了丢包。RACK并不记录数据被(s)ACK的时间,而是在收到ACK的时候,记录被该ACK确认的数据包的发送时间,在这些发送时间中取Recent,即最晚发送的。RACK的思想是,记录这个Recent (s)ACK所确认数据包的发送时间T.rack,然后给定一个时间窗口reo_wnd,在时间T.rack-reo_wnd之前发送的未被确认的数据包均被标记为LOST,然后这些数据包会被交给发送逻辑去发送。这非常符合常理。

  RACK可以修复丢包而不用等一个比较长的RTO超时,RACK可以用于快速恢复也可以用于超时恢复,既可以利用初传的数据包探测丢包也可以利用重传的数据包探测丢包,而且可以探测初传丢包也可以探测重传丢包,因此RACK是一个适应多种场景的丢包恢复机制。

 

在现今的网络环境中乱序传输是一个比较常见的场景,使用dupthresh和dup ACK来做丢包探测的可靠性越来越低。同时因为传统的基于系列号空间的乱序度来探测丢包时,如果发生报文重传,初传报文和重传报文在系列号空间就会重叠。而RACK基于时间的乱序来探测丢包的时候,重传报文和初传报文在时间线上是不重叠的,因此RACK可以同时利用初传报文和重传报文来探测丢包。

RACK RFC参考

 

   This document presents a new loss detection algorithm called RACK("Recent ACKnowledgment").  RACK uses the notion of time instead of
the conventional packet or sequence counting approaches for detecting losses.  RACK deems a packet lost if some packet sent sufficiently
later has been delivered.  It does this by recording packet  transmission times and inferring losses using cumulative
acknowledgments or selective acknowledgment (SACK) TCP options.
In the last couple of years we have been observing several
   increasingly common loss and reordering patterns in the Internet:
   1.  Lost retransmissions.  Traffic policers [POLICER16] and burst
       losses often cause retransmissions to be lost again, severely
       increasing TCP latency.
   2.  Tail drops.  Structured request-response traffic turns more
       losses into tail drops.  In such cases, TCP is application-
       limited, so it cannot send new data to probe losses and has to
       rely on retransmission timeouts (RTOs).
   3.  Reordering.  Link layer protocols (e.g., 802.11 block ACK) or
       routers' internal load-balancing can deliver TCP packets out of
       order.  The degree of such reordering is usually within the order
       of the path round trip time.
RACK_detect_loss():
    min_timeout = 0

    For each packet, Packet, in the scoreboard:
        If Packet is already SACKed, ACKed,
           or marked lost and not yet retransmitted:
            Skip to the next packet

        If Packet.xmit_ts > RACK.xmit_ts:
            Skip to the next packet
        If Packet.xmit_ts == RACK.xmit_ts AND // Timestamp tie breaker
           Packet.end_seq > RACK.end_seq
            Skip to the next packet

        timeout = Packet.xmit_ts + RACK.RTT + RACK.reo_wnd + 1
        If Now() >= timeout
            Mark Packet lost
        Else If (min_timeout == 0) or (timeout is before min_timeout):
            min_timeout = timeout

    If min_timeout != 0
        Arm a timer to call RACK_detect_loss() after min_timeout
 rack的优点:rack最大的不同之处在于基于时间,而以前的算法都是基于序列号,所以可以很轻易地解决二次重传的问题。不过对于流尾部丢包,也需要一个额外的定时器来辅助。

 

 RACK核心逻辑

/*
 * 在一次(s)ACK的处理过程中更新被确认的数据包的最晚发送时间rack.mstamp。
 * xmit_time-当前处理的被确认的数据包的发送时间
 * sacked-当前处理的被确认的数据包的sacked标记,被选择确认过吗?被重传过吗?等等。
 */
void tcp_rack_advance(struct tcp_sock *tp, const struct skb_mstamp *xmit_time, u8 sacked);
/*
 * 根据tcp_rack_advance记录的最晚发送的被确认的数据包的发送时间rack.mstamp以及
 * 重传队列里未被选择确认的数据包的发送时间skb.mstamp的差,判断是否标记为LOST。
 * RACK内置有一个twin,凡是符合rack.mstamp-skb.mstamp>twin的数据包,均标记为LOST。
 * 如果该数据包被重传过,那么清除其被重传过的印记!
 */
int tcp_rack_mark_lost(struct sock *sk);

1).处理ACK携带的信息(TCP头的ACK号或者选项中sACK块)时调用tcp_rack_advance;
2).在发送ACK并非顺序ACK时,进入异常Alert时,调用tcp_rack_mark_lost。

这个RACK机制的简单性就在于,它不再区分正常的顺序ACK以及SACK,它只比较时间戳,不管发送顺序如何,只基于确认携带的信息来决定一个数据包是不是要被重传。

   然而,在乱序的情况下,你可能会认为RACK机制可能会误传很多并未丢失(实际上是乱序到达或者ACK乱序反馈)的数据包,事实上这里就体现了reo_wnd时间窗口的作用,RACK的时间序并非严格的时间序,它是一个带有缓冲的准时间序机制。当然要是reo_wnd 设计不好,那就起反作用了。。。。如果reo_wnd很小,连接又有乱序情况,很容易误判一大片丢包,造成传输效率低下的问题。

void tcp_write_timer_handler(struct sock *sk)
{
    .......
     event = icsk->icsk_pending;
 
     switch (event) {
    case ICSK_TIME_REO_TIMEOUT:
        tcp_rack_reo_timeout(sk);
        break;
     case ICSK_TIME_EARLY_RETRANS:
         tcp_resume_early_retransmit(sk);
         break;
        ......
        }
}

  当某次判断的乱序时间窗口内只要有一个包未能判断丢包,则会启动这个REO定时器,该定时器的超时时间就是乱序时间窗口值,定时器处理函数中调用tcp_rack_reo_timeout()判断数据包丢失,就解决了ack驱动,有效的解决了尾部丢包问题

/* We have waited long enough to accommodate reordering. Mark the expired
 * packets lost and retransmit them.
 */
void tcp_rack_reo_timeout(struct sock *sk)
{
    struct tcp_sock *tp = tcp_sk(sk);
    u32 timeout, prior_inflight;
    u32 lost = tp->lost;

    prior_inflight = tcp_packets_in_flight(tp);
    tcp_rack_detect_loss(sk, &timeout);/*判断丢包*/
    if (prior_inflight != tcp_packets_in_flight(tp)) {/*满足条件,说明rack有新判断出丢包*/
        if (inet_csk(sk)->icsk_ca_state != TCP_CA_Recovery) {/*之前未丢包,切换到recovery状态*/
            tcp_enter_recovery(sk, false);
            if (!inet_csk(sk)->icsk_ca_ops->cong_control)
                tcp_cwnd_reduction(sk, 1, tp->lost - lost, 0);
        }
        tcp_xmit_retransmit_queue(sk);/*有判断出新丢包,则进行重传*/
    }
    if (inet_csk(sk)->icsk_pending != ICSK_TIME_RETRANS)/*重置为RTO定时器*/
        tcp_rearm_rto(sk);
}

丢失报文检查函数

/* RACK loss detection (IETF draft draft-ietf-tcpm-rack-01):
 *
 * Marks a packet lost, if some packet sent later has been (s)acked.
 * The underlying idea is similar to the traditional dupthresh and FACK
 * but they look at different metrics:
 *
 * dupthresh: 3 OOO packets delivered (packet count)
 * FACK: sequence delta to highest sacked sequence (sequence space)
 * RACK: sent time delta to the latest delivered packet (time domain)
 *
 * The advantage of RACK is it applies to both original and retransmitted
 * packet and therefore is robust against tail losses. Another advantage
 * is being more resilient to reordering by simply allowing some
 * "settling delay", instead of tweaking the dupthresh.
 *
 * When tcp_rack_detect_loss() detects some packets are lost and we
 * are not already in the CA_Recovery state, either tcp_rack_reo_timeout()
 * or tcp_time_to_recover()'s "Trick#1: the loss is proven" code path will
 * make us enter the CA_Recovery state.
 */
static void tcp_rack_detect_loss(struct sock *sk, u32 *reo_timeout)
{
    struct tcp_sock *tp = tcp_sk(sk);
    struct sk_buff *skb, *n;
    u32 reo_wnd;

    *reo_timeout = 0;
    reo_wnd = tcp_rack_reo_wnd(sk);/*获取乱序时间窗口值*/
    list_for_each_entry_safe(skb, n, &tp->tsorted_sent_queue,
                 tcp_tsorted_anchor) {/*遍历传输队列*/
        struct tcp_skb_cb *scb = TCP_SKB_CB(skb);
        s32 remaining;

        /* Skip ones marked lost but not yet retransmitted */
        if ((scb->sacked & TCPCB_LOST) &&
            !(scb->sacked & TCPCB_SACKED_RETRANS))
            continue;/*忽略已经标记丢失但未重传的skb*/

            /*已经判断完最近被(s)ack确认skb的之前所有的包*/
        if (!tcp_skb_sent_after(tp->rack.mstamp,
                    tcp_skb_timestamp_us(skb),
                    tp->rack.end_seq, scb->end_seq))
            break;

        /* A packet is lost if it has not been s/acked beyond
         * the recent RTT plus the reordering window.
         前报文的发送时间戳+最近测量的RTT+乱序窗口时长,小于当前TCP时间,即认为此报文已经丢失
         *//*小于等于零,可以判断为丢包,大于零,为需要在额外等待的时间*/
        remaining = tcp_rack_skb_timeout(tp, skb, reo_wnd);
        if (remaining <= 0) {
            tcp_mark_skb_lost(sk, skb);
            list_del_init(&skb->tcp_tsorted_anchor);
        } else {
            /* Record maximum wait time *//*记录需要等待最长的额外时间,用该值重置REO定时器*/
            *reo_timeout = max_t(u32, *reo_timeout, remaining);
        }
    }
}

时间窗口值

  时间窗口值不在是max(1ms, min_rtt/4)这样比较固定的值,是可以根据客户端反馈回来的D-SACK信息进行动态调整的,并且是以轮数为单位进行调整。当某一轮收到一个D-SACK数据包,就将时间窗口增加一个min_rtt/4,当然时间窗口值不能超过当前srtt的值,最大可以增加到64min_rtt的时间窗口值。当连续十六轮都没有再收到过D-SACK数据包,就将时间窗口值重置为min_rtt/4;但是要是不支持D_SACK 就玩完了。

/* Updates the RACK's reo_wnd based on DSACK and no. of recoveries.
 *
 * If a DSACK is received that seems like it may have been due to reordering
 * triggering fast recovery, increment reo_wnd by min_rtt/4 (upper bounded
 * by srtt), since there is possibility that spurious retransmission was
 * due to reordering delay longer than reo_wnd.
 *
 * Persist the current reo_wnd value for TCP_RACK_RECOVERY_THRESH (16)
 * no. of successful recoveries (accounts for full DSACK-based loss
 * recovery undo). After that, reset it to default (min_rtt/4).
 *
 * At max, reo_wnd is incremented only once per rtt. So that the new
 * DSACK on which we are reacting, is due to the spurious retx (approx)
 * after the reo_wnd has been updated last time.
 *
 * reo_wnd is tracked in terms of steps (of min_rtt/4), rather than
 * absolute value to account for change in rtt.
 * 时间窗口值是第二比较大的改变,时间窗口值不在是max(1ms, min_rtt/4)这样比较固定的值,
 * 是可以根据客户端反馈回来的D-SACK信息进行动态调整的,
 * 并且是以轮数为单位进行调整。当某一轮收到一个D-SACK数据包,就将时间窗口增加一个min_rtt/4,
 * 当然时间窗口值不能超过当前srtt的值,最大可以增加到64min_rtt的时间窗口值。
 * 当连续十六轮都没有再收到过D-SACK数据包,就将时间窗口值重置为min_rtt/4
 * 新版rack机制相比之下,确实有很大的改善,但是动态调整机制需要客户端支持D-SACK机制
 */
void tcp_rack_update_reo_wnd(struct sock *sk, struct rate_sample *rs)
{
    struct tcp_sock *tp = tcp_sk(sk);
/*TCP_RACK_STATIC_REO_WND为0x02,如果设置了该值,就回到旧版的静态时间窗口功能,默认是开启0x01*/
    if ((READ_ONCE(sock_net(sk)->ipv4.sysctl_tcp_recovery) &
         TCP_RACK_STATIC_REO_WND) ||
        !rs->prior_delivered)
        return;

    /* Disregard DSACK if a rtt has not passed since we adjusted reo_wnd 
    刚调整完乱序时间窗口,还未经过一轮,则忽略处理改d-sack*/
    if (before(rs->prior_delivered, tp->rack.last_delivered))
        tp->rack.dsack_seen = 0;

    /* Adjust the reo_wnd if update is pending */
    if (tp->rack.dsack_seen) {
        tp->rack.reo_wnd_steps = min_t(u32, 0xFF,
                           tp->rack.reo_wnd_steps + 1);/*将时间窗口增加一个min_rtt/4*/
        tp->rack.dsack_seen = 0;
        tp->rack.last_delivered = tp->delivered;
        tp->rack.reo_wnd_persist = TCP_RACK_RECOVERY_THRESH;/*重置为16轮*/
    } else if (!tp->rack.reo_wnd_persist) {/*连续16轮没再收到D-SACK信息,则认为网络乱序已经变好,将时间窗口值变小*/
        tp->rack.reo_wnd_steps = 1;
    }
}

 

  1. RACK最大的优势是,它可以用于检测在之前发送的所有数据包(无论是原始数据传输还是重传)是否丢失。

示例1:尾部丢失

考虑一个发送窗口包含三个数据包(P1、P2、P3)的发送者,其中P1和P3丢失。假设每个数据包的发送至少在前一个数据包的发送后经过RACK.reo_wnd(默认为1毫秒)。当收到P2的SACK时,RACK将标记P1为丢失,并触发P1的重传(R1)。当累积确认了R1时,RACK将标记P3为丢失,发送者将重传P3作为R3。这个示例说明RACK如何能够在交易的末尾修复某些数据包的丢失,而无需任何定时器。请注意,传统的重复ACK阈值、RFC5681、RFC6675以及前向确认FACK算法都无法检测这种类型的丢失,因为这需要特定的数据包或序列计数。

示例2:丢失的重传

考虑一个窗口包含三个数据包(P1、P2、P3)的发送者,其中P1和P2丢失。假设每个数据包的发送至少在前一个数据包的发送后经过RACK.reo_wnd(默认为1毫秒)。当SACK了P3时,RACK将标记P1和P2为丢失,它们将作为R1和R2重传。假设R1再次丢失(作为尾部丢失),但SACK了R2,RACK将再次标记R1为丢失,以进行重新传输。同样,传统的三次重复ACK阈值方法、RFC6675和前向确认FACK算法都无法检测到这种类型的丢失。这种类型的重传丢失在TCP受速率限制时非常常见,特别是在使用具有大桶深度和低速率限制的令牌桶策略器时,因为标准的拥塞控制需要多个往返行程来将速率降低到策略器的速率以下。

示例3:(轻微的)重排序

考虑一种常见的重排序事件:发送了一个窗口的数据包(P1、P2、P3)。P1和P2携带MSS字节的完整负载,但P3由于应用程序限制的行为只有1个字节的负载。假设发送方先前已检测到了重排序(例如,通过实现[REORDER-DETECT]中的算法),因此RACK.reo_wnd是min_RTT/4。现在P3被重排序并首先交付,然后才是P1和P2。只要P1和P2在min_RTT/4内交付,RACK将不会考虑P1和P2为丢失。但是,如果P1和P2在重排序窗口之外被交付,那么RACK仍然会错误地标记P1和P2为丢失。文本中还提到了如何减少这种误报的情况。

这些示例表明,当发送者受应用程序限制(常见于交互式请求/响应流量)或接收窗口限制(常见于使用接收窗口来控制发送者的应用程序)时,RACK特别有用。此外,RACK在一些实现中(例如Linux)与TCP分段卸载(TSO)非常高效。RACK总是将整个TSO块标记为丢失,因为同一TSO块中的数据包具有相同的传输时间戳。相比之下,基于计数的算法(例如RFC3517、RFC5681)可能仅标记TSO块中的某些数据包为丢失,强制栈执行昂贵的TSO块分段或有选择地标记得分板中的单个数据包为丢失。

不足之处:

  1. RACK需要发送方记录每个数据包的传输时间:RACK要求发送方记录每个数据包的传输时间,以毫秒或更精确的时钟粒度。已经用于往返时延(RTT)估算的TCP实现不需要添加新的每个数据包的状态。但尚未记录数据包传输时间的实现需要添加每个数据包的内部状态(通常每个数据包需要4或8个八位字节),以跟踪传输时间。与传统方法相比,传统方法只需要一个变量来跟踪重复ACK阈值。

  2. 调整重新排序窗口:RACK使用重新排序窗口大小为最小RTT的四分之一(min_rtt / 4)。它使用最小RTT来适应由于数据包在略微不同的路径上传输(例如,基于路由器的并行方案)或在较低链路层中乱序传递(例如,使用链路层重传的无线链路)引入的重排序。或者,RACK可以使用RTT估算中使用的平滑RTT(RFC6298)。但是,平滑RTT可能由于拥塞和缓冲区过度拥塞而显著膨胀多个数量级,这将导致过于保守的重新排序窗口和慢速的丢失检测。此外,RACK使用最小RTT的四分之一,因为Linux TCP在其实现中使用相同的因子来延迟“早期重传”(RFC5827),以减少在存在重排序时的虚假丢失检测,而经验表明这似乎运行得相当不错。

潜在的改进:一种潜在的改进是通过测量时间上的重排序程度来进一步调整重新排序窗口,而不是使用数据包距离。但这需要存储每个数据包的传递时间戳,对于某些得分板实现来说,支持每个数据包的传递时间戳可能会比较困难,因为它们可能会将已经SACKed的数据包合并在一起,以支持更快的得分板索引。然而,文本中承认目前的度量标准可以通过进一步的研究来改进。

总之,这些不足之处主要涉及到RACK需要更多的数据包状态信息,以及重新排序窗口的固定因子可能不适用于所有网络环境。它需要更精确的时钟粒度,这在某些情况下可能需要修改现有TCP实现。同时,改进这些方面可能需要进一步的研究和发展。

与其他丢失恢复算法的关系:

  1. 主要动机:RACK的主要动机是最终提供一种简单和通用的替代标准丢失恢复算法[RFC5681][RFC6675][RFC5827][RFC4653]以及非标准算法[FACK][THIN-STREAM]的算法。尽管RACK可以作为这些算法之上的辅助丢失检测,但这并不是必需的,因为RACK隐含地包含了它们的大部分功能。

  2. 与其他算法的对比:[RFC5827][RFC4653][THIN-STREAM]根据当前或先前的数据包飞行大小动态调整重复ACK阈值。RACK采取了不同的方法,仅使用一个ACK事件和一个重新排序窗口。RACK可以看作是扩展的早期重传[RFC5827],它没有FlightSize限制,但具有额外的重新排序窗口。[FACK]在某种意义上可以看作是FACK的一种广义形式,它在时间空间而不是序列空间中运作,从而更好地处理重新排序、应用程序限制的流量和丢失的重传。

  3. 建议:然而,RACK仍然是一种实验性算法。由于最古老的丢失检测算法,即3次重复ACK阈值[RFC5681]已经被标准化和广泛部署,因此我们建议TCP实现同时使用RACK和[RFC5681]第3.2节中规定的算法以确保兼容性。

  4. 兼容性:RACK与标准RTO[RFC6298]、RTO重新启动[RFC7765]、F-RTO[RFC5682]和Eifel算法[RFC3522]兼容,并且不会干扰它们。这是因为RACK仅通过使用ACK事件来检测丢失。它既不改变计时器计算,也不检测虚假超时。

  5. 与Tail Loss Probe(TLP)的协同工作:此外,RACK自然地与Tail Loss Probe [TLP]协同工作得很好,因为尾部丢失探测会产生ACK或SACK,这可以被RACK用于检测更多的丢失。RACK可以用于放宽TLP对使用FACK和重传最高序列数据包的要求,因为RACK对数据包序列号是不可知的,而是使用传输时间。因此,TLP可以修改为重传第一个未确认的数据包,这可以改善应用程序的延迟。

Interaction with congestion control
  1. 故意解耦丢失检测和拥塞控制:RACK故意将丢失检测与拥塞控制分开。RACK仅用于检测丢失,不会修改拥塞控制算法[RFC5681][RFC6937]。然而,RACK可能比传统的重复ACK阈值方法更早或更晚地检测到丢失。由RACK标记为丢失的数据包在拥塞控制认为适当时(例如,使用[RFC6937])不应立即重传。

  2. 适用于快速恢复和RTO恢复:RACK适用于快速恢复和[RFC5681]中的重传超时(RTO)后的恢复。不区分快速恢复或RTO恢复是不必要的,因为RACK仅基于数据包的传输时间顺序。当通过RTO重传的数据包被确认时,RACK将标记在RTO之前发送的任何未确认的数据包为丢失,因为这些数据包自发送以来至少经过了一个往返时延(RTT)。

总之,RACK不直接参与拥塞控制,但它可以帮助提早或延迟检测数据包的丢失。任何由RACK标记为丢失的数据包应在拥塞控制认为适当时才进行重传。此外,RACK适用于快速恢复和RTO恢复,无需区分它们,因为它的工作原理仅基于数据包的传输时间顺序。

https://redwingz.blog.csdn.net/article/details/106062180

posted @ 2021-11-19 19:06  codestacklinuxer  阅读(143)  评论(0编辑  收藏  举报