聊一聊tcp 拥塞控制 三

拥塞控制状态处理

/* 
open状态: open状态是常态, 这种状态下tcp 发送放通过优化后的快速路径来接收处理ack,当一个ack到达时, 发送方根据拥塞窗口是小于还是大于 满启动阈值,
    按照慢启动或者拥塞避免来增大拥塞窗口
disorder 状态: 当发送方收到 DACK 或者SACK的时候, 将变为disorder 状态, 再次状态下拥塞窗口不做调整,但是没到一个新到的段  就回触发发送一个新的段发送出去
        此时TCP 遵循发包守恒原则,就是一个新包只有在一个老的包离开网络后才发送
cwr 状态:发送发被通知出现拥塞通知, 直接告知!! 比喻通过icmp 源端抑制 等方式通知,当收到拥塞通知时,发送方并不是立刻减少拥塞窗口, 而是每个一个新到
            的ack减小一个段 知道窗口减小到原来的一半为止,发送方在减小窗口过程中如果没有明显重传,就回保持cwr 状态, 但是如果出现明显重传,就回被
            recovery 或者loss 中断而进入 loss recovery 状态;
recovery状态:在收到足够多的连续重复的ack 后,发送方重传第一个没有被确认的段,进入recovery 状态,默认情况下 连续收到三个重复的ack 就回进入recovery状态,
            在recovery状态期间,拥塞窗口的大小每隔一个新到的确认就会减少一个段, 和cwr 一样 出于拥塞控制期间,这种窗口减少 终止于大小等于ssthresh,也就是
                        进入recovery状态时窗口的一半。发送方重传被标记为丢失的段,或者根据包守恒原则 发送数据,发送方保持recovery 状态直到所有recovery状态中
             被 发送的数据被确认,此时recovery状态就回变为open,超时重传可能中断recovery状态
Loss 状态
:当一个RTO到期,发送方进入Loss 状态 , 所有正在发送的段都被标记为丢失段,拥塞窗口设置为一个段。发送方启动满启动算法增大窗口。Loss 状态是
                拥塞窗口在被重置为一个段后增大,但是recovery状态是拥塞窗口只能被减小, Loss 状态不能被其他状态中断,所以只有所有loss 状态下开始传输的数据
                 得到确认后,才能到open状态, 也就是快速重传不能在loss 状态下触发。
                 当一个RTO 超时, 或者收到ack 的确认已经被以前的sack 确认过, 则意味着我们记录的sack信息不能反应接收方实际的状态,
                 此时就回进入Loss 状态。
(1)Open:Normal state, no dubious events, fast path.
(2)Disorder:In all respects it is Open, but requres a bit more attention.
          It is entered when we see some SACKs or dupacks. It is split of Open
          mainly to move some processing from fast path to slow one.
(3)CWR:cwnd was reduced due to some Congestion Notification event.
          It can be ECN, ICMP source quench, local device congestion.
(4)Recovery:cwnd was reduced, we are fast-retransmitting.
(5)Loss:cwnd was reduced due to RTO timeout or SACK reneging.
              

Process an event, which can update packets-in-flight not trivially.
 * Main goal of this function is to calculate new estimate for left_out,
 * taking into account both packets sitting in receiver's buffer and
 * packets lost by network.
 *
 * Besides that it does CWND reduction, when packet loss is detected
 * and changes state of machine.
 *
 * It does _not_ decide what to send, it is made in function
 * tcp_xmit_retransmit_queue().
 
 tcp_fastretrans_alert() is entered:

(1)each incoming ACK, if state is not Open
(2)when arrived ACK is unusual, namely:
          SACK
          Duplicate ACK
          ECN ECE

 */
 /*FACK的全称是forward acknowledgement,FACK通过记录SACK块中系列号最大(forward-most)的SACK块来推测丢包信息,
 在linux中使用fackets_out这个状态变量来记录FACK信息。我们之前介绍SACK重传时候说过在SACK下需要3个dup ACK来触发快速重传(3个为默认值),
 而SACK下的dup ACK的定义与传统的非SACK下定义又略有不同(详细请参考前面的文章和示例)。当使能FACK的时候,
 实际上我们可以通过一个SACK块信息来推测丢包情况进而触发快速重传。比如server端依次发出P1(0-9)、P2(10-19)、P3(20-29)、P4(30-39)、P5(40-49),
 假设client端正常收到了P1包并回复了ACK确认包,P2、P3、P4则由于网络拥塞等原因丢失,client在收到P5时候回复一个Ack=10的确认包,
 并携带P5有SACK块信息(40-50),这样server在收到P1的确认包和P5的dup ACK时候,就可以根据dup ACK中的SACK信息得知client端收到了P1报文和P5报文,
 计算出P1和P5两个数据包中间间隔了3个数据包,达到了dup ACK门限(默认为3),进而推测出P2报文丢失。

 */
static void tcp_fastretrans_alert(struct sock *sk, const int acked,
                  const int prior_unsacked,
                  bool is_dupack, int flag)
{
    struct inet_connection_sock *icsk = inet_csk(sk);
    struct tcp_sock *tp = tcp_sk(sk);
    //is_dupack = !(flag & (FLAG_SND_UNA_ADVANCED | FLAG_NOT_DUP));/* 判断是不是重复的ACK*/
    bool do_lost = is_dupack || ((flag & FLAG_DATA_SACKED) && //  新来的一个sack
                    (tcp_fackets_out(tp) > tp->reordering));//tcp_fackets_out 返回 "空洞-乱序"的大小, 如果超过3 个 (默认) 就认为丢包。
                    // tp->reordering  表示 重复dupack 度量值 ; 默认为3 
    int fast_rexmit = 0;

    /* 如果packet_out 等待被ack的报文为0,那么不可能有sacked_out(被sack 确认的段) */
    if (WARN_ON(!tp->packets_out && tp->sacked_out))
        tp->sacked_out = 0;
    /* fack中fack'd packets 的计数至少需要依赖一个SACK的段.--- 未确认和已选择确认之间有段*/
    if (WARN_ON(!tp->sacked_out && tp->fackets_out))
        tp->fackets_out = 0;

    /* Now state machine starts.
     * A. ECE, hence prohibit cwnd undoing, the reduction is required. 
     禁止拥塞窗口撤销,并开始减小拥塞窗口。*/
    if (flag & FLAG_ECE)
        tp->prior_ssthresh = 0;

    /* B. In all the states check for reneging SACKs. 
    检查是否为虚假的SACK,即ACK是否确认已经被SACK的数据
    如果接收到的 ACK 指向已记录的 SACK,这说明我们记录的 SACK 并没有反应接收方的真实状态。
    也就是说接收方现在已经处于严重的拥塞状态或者在处理上有bug,那么我们接下来就要按照重传超时的方式去处理。
    因为按照正常的逻辑流程,接受的 ACK不应该指向已记录的 SACK,而应指向 SACK 并未包含的,
    这说明接收方由于拥塞已经把 SACK 部分接收的段已经丢弃或者处理上有 BUG,
    我们必须需要重传*/
    if (tcp_check_sack_reneging(sk, flag))
        return;

    /* C. Check consistency of the current state.  确定left_out < packets_out 
    查看是否从发送队列发出的包的数量是否不小于发出主机的包的数量
    该函数的功能主要是判断 left_out 是否大于 packets_out, 当然,这是不可能的,因
为前者是已经发送离开主机的未被确认的段数,而后者是已经离开发送队列 (不一定离
开主机)但未确认的段数。故而,这里有一个WARN_ON,以便输出相应的警告信息
*/
    tcp_verify_left_out(tp);

    /* D. Check state exit conditions. State can be terminated
     *    when high_seq is ACKed. */
    if (icsk->icsk_ca_state == TCP_CA_Open) {
        WARN_ON(tp->retrans_out != 0);
        tp->retrans_stamp = 0;/* 清除上次重传阶段第一个重传段的发送时间*/
    } else if (!before(tp->snd_una, tp->high_seq)) {//tp->snd_una >= tp->high_seq
        switch (icsk->icsk_ca_state) {
        case TCP_CA_CWR:
            /* CWR is to be held something *above* high_seq
             * is ACKed for CWR bit to reach receiver. */
            if (tp->snd_una != tp->high_seq) { // 第一个if 是》= 现在只需要判断 》 就行
                tcp_end_cwnd_reduction(sk);//需要snd_una > high_seq才能撤销---结束窗口减小
                tcp_set_ca_state(sk, TCP_CA_Open);//回到 OPen 状态
            }
            break;

        case TCP_CA_Recovery:
         /*TCP_CA_Recovery拥塞状态接收到ACK报文,其ack_seq序号确认了high_seq之前的所有报文(SND.UNA >= high_seq),
high_seq记录了进入拥塞时的最大发送序号SND.NXT,故表明对端接收到了SND.NXT之前的所有报文,未发生丢包,需要撤销拥塞状态*/
            if (tcp_is_reno(tp))//判断对方是否提供了 SACK 服务,提供,返回 0, 否则返回 1
                tcp_reset_reno_sack(tp);//设置 sacked_out 为 0
            if (tcp_try_undo_recovery(sk))//尝试从 Recovery 状态撤销 成功,就直接返回
                return;
            tcp_end_cwnd_reduction(sk);//结束拥塞窗口缩小
            break;
        }
    }

    /* Use RACK to detect loss */
    if (sysctl_tcp_recovery & TCP_RACK_LOST_RETRANS &&
        tcp_rack_mark_lost(sk))
        flag |= FLAG_LOST_RETRANS;

    /* E. Process state. */
    switch (icsk->icsk_ca_state) {
    case TCP_CA_Recovery:
        if (!(flag & FLAG_SND_UNA_ADVANCED)) {//判断是否没有段被确认 也就是 ack报文是否将send_un增长了
            if (tcp_is_reno(tp) && is_dupack)////判断是否启用了 SACK, 未启用返回 1, 并且收到的是重复的 ACK
                tcp_add_reno_sack(sk);///* 增加sacked_out (记录接收到的重复的 ACK 数量 没有启用sack 就是 dupack了),检查是否出现reorder*/
        } else {
        /*于TCP_CA_Recovery拥塞状态,如果ACK报文没有确认全部的进入拥塞时SND.NXT(high_seq)之前的数据,仅确认了一部分(FLAG_SND_UNA_ADVANCED),
        执行撤销函数tcp_try_undo_partial*/
            if (tcp_try_undo_partial(sk, acked, prior_unsacked, flag))
                return;
            /* Partial ACK arrived. Force fast retransmit. */
            do_lost = tcp_is_reno(tp) ||
                  tcp_fackets_out(tp) > tp->reordering;
        }
        /*对于处在TCP_CA_Recovery拥塞状态的套接口,ACK报文并没有推进SND.UNA序号,或者,
        在partial-undo未执行的情况下,尝试进行DSACK相关的撤销操作,由函数tcp_try_undo_dsack完成。*/
        if (tcp_try_undo_dsack(sk)) {
            tcp_try_keep_open(sk);
            return;
        }
        break;
    case TCP_CA_Loss:
        tcp_process_loss(sk, flag, is_dupack);
        if (icsk->icsk_ca_state != TCP_CA_Open &&
            !(flag & FLAG_LOST_RETRANS))
            return;
        /* Change state if cwnd is undone or retransmits are lost */
    default:
        if (tcp_is_reno(tp)) {//判断是否开启了 SACK,没启用返回 1
            if (flag & FLAG_SND_UNA_ADVANCED)// 也就是 snd_unack 序列号增长了 所以重置 
                tcp_reset_reno_sack(tp);//重置 sacked sacked_out 数据 被sack‘d 的数据可以clear了
            if (is_dupack)
                tcp_add_reno_sack(sk);//如果是 dupack 则记录并且sack_out ++
        }

        if (icsk->icsk_ca_state <= TCP_CA_Disorder)
            tcp_try_undo_dsack(sk);
            //确定能够离开 Disorder 状态,而进入 Recovery 状态。如果不进入 Recovery 状态,判断可否进入 OPen 状态。
        if (!tcp_time_to_recover(sk, flag)) {
            tcp_try_to_open(sk, flag, prior_unsacked);//如果不进入 Recovery 状态,判断可否进入 OPen 状态。
            return;
        }

        /* MTU probe failure: don't reduce cwnd */
        if (icsk->icsk_ca_state < TCP_CA_CWR &&
            icsk->icsk_mtup.probe_size &&
            tp->snd_una == tp->mtu_probe.probe_seq_start) {
            tcp_mtup_probe_failed(sk);
            /* Restores the reduction we did in tcp_mtup_probe() */
            tp->snd_cwnd++;
            tcp_simple_retransmit(sk);
            return;
        }

        /* Otherwise enter Recovery state */
        tcp_enter_recovery(sk, (flag & FLAG_ECE));
        fast_rexmit = 1;
    }
    //如果接收到重复的 ACK 或者重传队首的段超
    //则要为确定丢失的段更新记分牌
    if (do_lost)///* 更新记分牌,标志丢失和超时的数据包,增加lost_out */
        tcp_update_scoreboard(sk, fast_rexmit);
    tcp_cwnd_reduction(sk, prior_unsacked, fast_rexmit, flag);
    tcp_xmit_retransmit_queue(sk);//重传重传队列中那些标记为 LOST 的段,同时重置 RTO 定时器。
}

检查接收放是否违约

/* If ACK arrived pointing to a remembered SACK, it means that our
 * remembered SACKs do not reflect real state of receiver i.e.
 * receiver _host_ is heavily congested (or buggy).
 *
 * To avoid big spurious retransmission bursts due to transient SACK
 * scoreboard oddities that look like reneging, we give the receiver a
 * little time (max(RTT/2, 10ms)) to send us some more ACKs that will
 * restore sanity to the SACK scoreboard. If the apparent reneging
 * persists until this RTO then we'll clear the SACK scoreboard.
 如果接收到的确认 ACK 指向之前记录的 SACK,这说明之前记录的 SACK 并没有
反映接收方的真实状态。接收路径上很有可能已经有拥塞发生或者接收主机正在经历严
重的拥塞甚至处理出现了 BUG,因为按照正常的逻辑流程,接收的 ACK 不应该指向已
1988.9. TCP ACK CHAPTER 8. 非核心代码分析
记录的 SACK,而应该指向 SACK 后面未接收的地方。通常情况下,此时接收方已经删
除了保存到失序队列中的段。
为了避免短暂奇怪的看起来像是违约的 SACK 导致更大量的重传,我们给接收者
一些时间, 即 max(RTT/2; 10ms) 以便于让他可以给我们更多的 ACK,从而可以使得
SACK 的记分板变得正常一点。如果这个表面上的违约一直持续到重传时间结束,我们
就把 SACK 的记分板清除掉
 */
static bool tcp_check_sack_reneging(struct sock *sk, int flag)
{
    if (flag & FLAG_SACK_RENEGING) {// 接收方违约了 
        struct tcp_sock *tp = tcp_sk(sk);
        unsigned long delay = max(usecs_to_jiffies(tp->srtt_us >> 4),
                      msecs_to_jiffies(10));//计算超时重传时间
            //更新超时重传定时器
        inet_csk_reset_xmit_timer(sk, ICSK_TIME_RETRANS,
                      delay, TCP_RTO_MAX);
        return true;
    }
    return false;
}

ACK CHECK 标志

SACK/ RENO/ FACK是否启用

/* These functions determine how the current flow behaves in respect of SACK
 * handling. SACK is negotiated with the peer, and therefore it can vary
 * between different flows.
 *
 * tcp_is_sack - SACK enabled
 * tcp_is_reno - No SACK
 * tcp_is_fack - FACK enabled, implies SACK enabled
 */
static inline int tcp_is_sack(const struct tcp_sock *tp)
{
    return tp->rx_opt.sack_ok;
}

static inline bool tcp_is_reno(const struct tcp_sock *tp)
{
    return !tcp_is_sack(tp);
}

static inline bool tcp_is_fack(const struct tcp_sock *tp)
{
    return tp->rx_opt.sack_ok & TCP_FACK_ENABLED;
}

static inline void tcp_enable_fack(struct tcp_sock *tp)
{
    tp->rx_opt.sack_ok |= TCP_FACK_ENABLED;
}

  Reneging 的意思就是违约,接收方有权把已经报给发送端 SACK 里的数据给丢了。当然,我们肯定是不鼓励这样做的,因为这个事会把问题复杂化。但是,接收方可能会由于一些极端情况这么做,比如要把内存给别的更重要的东西。所以,发送方也不能完全依赖 SACK,主要还是要依赖 ACK,并维护 Time-Out。如果后续的 ACK 没有增长,那么还是要把 SACK 的东西重传,另外,接收端这边永远不能把 SACK 的包标记为 Ack。   注意:SACK 会消费发送方的资源,试想,如果一个攻击者给数据发送方发一堆 SACK 的选项,这会导致发送方开始要重传甚至遍历已经发出的数据,这会消耗很多发送端的资源

  SACK Selective Acknowledgment(SACK),这种方式需要在 TCP 头里加一个 SACK 的东西,ACK 还是 Fast Retransmit 的 ACK,SACK 则是汇报收到的数据碎版。这样,在发送端就可以根据回传的 SACK 来知道哪些数据到了,哪些没有到。于是就优化了 Fast Retransmit 的算法。当然,这个协议需要两边都支持。

  D-SACK Duplicate SACK 又称 D-SACK,其主要使用了 SACK 来告诉发送方有哪些数据被重复接收了。D-SACK 使用了 SACK 的第一个段来做标志,如果 SACK 的第一个段的范围被 ACK 所覆盖,那么就是 D-SACK。如果 SACK 的第一个段的范围被 SACK 的第二个段覆盖,那么就是 D-SACK。引入了 D-SACK,有这么几个好处: 

  • 可以让发送方知道,是发出去的包丢了,还是回来的 ACK 包丢了。
  • 是不是自己的 timeout 太小了,导致重传。
  • 网络上出现了先发的包后到的情况(又称 reordering)
  • 网络上是不是把我的数据包给复制了。

理一下几个概念:

/* left_out = sacked_out + lost_out 
    sacked_out:Packets, which arrived to receiver out of order and hence not ACKed. With SACK this  number is simply amount of SACKed data. 
Even without SACKs it is easy to give pretty reliable  estimate of this number, counting duplicate ACKs.

lost_out:Packets lost by network. TCP has no explicit loss notification feedback from network
        (for now). It means that this number can be only guessed. Actually, it is the heuristics to predict  lossage that distinguishes different algorithms.
        F.e. after RTO, when all the queue is considered as lost, lost_out = packets_out and in_flight = retrans_out.
*/
static inline unsigned int tcp_left_out(const struct tcp_sock *tp)
{
    return tp->sacked_out + tp->lost_out;
}

/* This determines how many packets are "in the network" to the best
 * of our knowledge.  In many cases it is conservative, but where
 * detailed information is available from the receiver (via SACK
 * blocks etc.) we can make more aggressive calculations.
 *
 * Use this for decisions involving congestion control, use just
 * tp->packets_out to determine if the send queue is empty or not.
 *
 * Read this equation as:
 *
 *    "Packets sent once on transmission queue" MINUS
 *    "Packets left network, but not honestly ACKed yet" PLUS
 *    "Packets fast retransmitted"
 
 packets_out is SND.NXT - SND.UNA counted in packets.
 retrans_out is number of retransmitted segments.
left_out is number of segments left network, but not ACKed yet.

 */
static inline unsigned int tcp_packets_in_flight(const struct tcp_sock *tp)
{
    return tp->packets_out - tcp_left_out(tp) + tp->retrans_out;
}

各状态的退出

Loss

  • icsk->icsk_retransmits = 0; /*超时重传次数归0*/
  • tcp_try_undo_recovery(sk);
  • 检查是否需要undo,不管undo成功与否,都返回Open态。

CWR

If seq number greater than high_seq is acked, it indicates that the CWR indication has reached the peer TCP, call tcp_complete_cwr() to bring down the cwnd to ssthresh value.

 

Disorder

启用sack,则tcp_try_undo_dsack(sk),交给它处理。

否则,tp->undo_marker = 0;

Recovery

tcp_try_undo_recovery(sk);

 

(1)CWR状态

Q: 什么时候进入CWR状态?

A: 如果检测到ACK包含ECE标志,表示接收方通知发送法进行显示拥塞控制。

@tcp_try_to_open():

if (flag & FLAG_ECE)

tcp_enter_cwr(sk, 1);

tcp_enter_cwr()函数分析

它主要做了:

1. 重新设置慢启动阈值。

2. 清除undo需要的标志,不允许undo。

3. 记录此时的最高序号(high_seq = snd_nxt),用于判断退出时机。

4. 添加CWR标志,用于通知接收方它已经做出反应。

5. 设置此时的状态为TCP_CA_CWR。

Q: 在CWR期间采取什么措施?

A: 拥塞窗口每隔一个确认段减小一个段,即每收到2个确认将拥塞窗口减1,直到拥塞窗口等于慢启动阈值为止。

 

(2)Disorder状态

Q: 什么时候进入Disorder状态?

A: 如果检测到有被sacked的数据包,或者有重传的数据包,则进入Disorder状态。

当然,之前已经确认不能进入Loss或Recovery状态了。

判断条件: sacked_out、lost_out、retrans_out、undo_marker不为0。

Q: 在Disorder期间采取什么措施?

A: 1. 设置CA状态为TCP_CA_Disorder。

2. 记录此时的最高序号(high_seq = snd_nxt),用于判断退出时机。

3. 微调拥塞窗口,防止爆发式传输。

In Disorder state TCP is still unsure of genuiness of loss, after receiving acks with sack there may be

a clearing ack which acks many packets non dubiously in one go. Such a clearing ack may cause a

packet burst in the network, to avoid this cwnd size is reduced to allow no more than max_burst (usually 3)

number of packets.

(3)Open状态

因为Open状态是正常的状态,是状态处理的最终目的,所以不需要进行额外处理。

 

上一篇:Wireshark抓包分析TCP的三次握手


下一篇:2.Kafka的工作原理及数据丢失、数据重复问题