TCP混合慢启动

混合慢启动(Hybrid Slow Start)使用二个信息来决定SlowStart阶段到拥塞避免阶段(Congestion Avoidance)的转换,一是ACK Train的长度;二是报文延迟的增长。ACK Train的长度为在一个RTT周期内,紧密相邻的ACK报文到达的时间间隔之和,内核默认间隔不大于2ms的一系列ACK报文为ACK Train。每个RTT周期,计算一次ACK Train长度,与估算的最小路径发送延迟进行对比,

SlowStart阶段报文延迟的增长也可能意味着路径中的瓶颈路由器已经发生了拥塞。由于报文的突发性传输,瓶颈路由器可能暂时发送拥塞,但是整个RTT时间内的平均发送速率小于瓶颈路由器的带宽,这就导致其在一个RTT周期内,缓存的增长和清空。这种情况下,报文的延时将不能作为拥塞的信号,为解决此问题,我们使用每个报文Train开头的几个报文的延时数据,它们不太受到突发传输的影响。所以,在RTT周期内前几个报文的延时的增长意味着在瓶颈链路中出现的其它的流量。

混合慢启动基于以上的两个信息设置ssthresh,强制SlowStart阶段结束,进入拥塞避免阶段(Congestion Avoidance)。

HYSTART默认参数

宏HYSTART_ACK_TRAIN和HYSTART_DELAY分别表示混合慢启动使用的两种判断方法。

/* Two methods of hybrid slow start */
#define HYSTART_ACK_TRAIN   0x1
#define HYSTART_DELAY       0x2

对于第二种方法,以下为报文延时相关的参数。最小8个采样,延时的合理范围为:[32, 128]。

/* Number of delay samples for detecting the increase of delay */
#define HYSTART_MIN_SAMPLES 8
#define HYSTART_DELAY_MIN   (4U<<3)
#define HYSTART_DELAY_MAX   (16U<<3)
#define HYSTART_DELAY_THRESH(x) clamp(x, HYSTART_DELAY_MIN, HYSTART_DELAY_MAX)

混合慢启动位于内核的Cubic拥塞算法模块中,默认情况下此功能是开启状态(hystart=1),同时采用以上接收的ACK Train和报文延时进行检查(hystart_detect)。混合慢启动的最低拥塞窗口限定为16(hystart_low_window)。默认组成ACK-Train的报文间隔为2毫秒(hystart_ack_delta)。

如果动态加载Cubic模块,可在加载时修改hystart的相关参数。

static int hystart __read_mostly = 1;
static int hystart_detect __read_mostly = HYSTART_ACK_TRAIN | HYSTART_DELAY;
static int hystart_low_window __read_mostly = 16;
static int hystart_ack_delta __read_mostly = 2;

HYSTART初始化与周期

参见函数bictcp_hystart_reset,用于重新设置hystart的相关参数。在hystart检测的每个开始周期,也是使用此函数初始化。

static inline u32 bictcp_clock(void)
{
    ...
    return jiffies_to_msecs(jiffies);
}
static inline void bictcp_hystart_reset(struct sock *sk)
{
    struct tcp_sock *tp = tcp_sk(sk);
    struct bictcp *ca = inet_csk_ca(sk);

    ca->round_start = ca->last_ack = bictcp_clock();
    ca->end_seq = tp->snd_nxt;
    ca->curr_rtt = 0;
    ca->sample_cnt = 0;    

在Cubic的初始化函数bictcp_init中,如果启用了Hystart(hystart为真),初始化Hystart相关参数。

static void bictcp_init(struct sock *sk)
{
    struct bictcp *ca = inet_csk_ca(sk);

    bictcp_reset(ca);

    if (hystart)
        bictcp_hystart_reset(sk);

    if (!hystart && initial_ssthresh)
        tcp_sk(sk)->snd_ssthresh = initial_ssthresh;

其次,当套接口的进入TCP_CA_Loss拥塞状态时,表明之后要开始SlowStart恢复阶段,调用bictcp_hystart_reset函数重设hystart参数,开启新一轮Hystart检测。注意这里也会调用bictcp_reset,其中,将found设置为0,表明还未发现SlowStart的退出点。

static void bictcp_state(struct sock *sk, u8 new_state)
{
    if (new_state == TCP_CA_Loss) {
        bictcp_reset(inet_csk_ca(sk));
        bictcp_hystart_reset(sk);
    }
}
static inline void bictcp_reset(struct bictcp *ca)
{
    ca->cnt = 0;
    ...
    ca->delay_min = 0;
    ca->epoch_start = 0;
    ca->ack_cnt = 0;
    ca->tcp_cwnd = 0;
    ca->found = 0;
}

最后,在SlowStart阶段,ACK确认序号在hystart的结束序号(end_seq)之后,表明当前的检测已经完成,开始新一轮的Hystart检测,重设hystart参数。如下bictcp_cong_avoid函数。

static void bictcp_cong_avoid(struct sock *sk, u32 ack, u32 acked)
{
    struct tcp_sock *tp = tcp_sk(sk);
    struct bictcp *ca = inet_csk_ca(sk);

    if (!tcp_is_cwnd_limited(sk))
        return;
    
    if (tcp_in_slow_start(tp)) {
        if (hystart && after(ack, ca->end_seq))
            bictcp_hystart_reset(sk);
        acked = tcp_slow_start(tp, acked);
        if (!acked)
            return;
    } 

Hystart检测

在tcp_ack处理ACK报文的过程中,将根据ACK报文确认的数据情况,清理重传队列,之后调用拥塞控制的pkts_acked函数指针,对于Cubic而言,其为bictcp_acked。目前,Cubic和Hystart仅使用到了ack_sample结构中的rtt_us字段。

static int tcp_clean_rtx_queue(struct sock *sk, u32 prior_fack,
                   u32 prior_snd_una, struct tcp_sacktag_state *sack)
{
    ...
    if (icsk->icsk_ca_ops->pkts_acked) {
        struct ack_sample sample = { .pkts_acked = pkts_acked,
                         .rtt_us = sack->rate->rtt_us,
                         .in_flight = last_in_flight };

        icsk->icsk_ca_ops->pkts_acked(sk, &sample);
    }

如下函数bictcp_acked,变量delay_min保存最小的RTT值。在拥塞窗口大于hystart定义的最低窗口值16(hystart_low_window)时,hystart才开始执行。

static void bictcp_acked(struct sock *sk, const struct ack_sample *sample)
{   
    const struct tcp_sock *tp = tcp_sk(sk);
    struct bictcp *ca = inet_csk_ca(sk);
    ...
    
    delay = (sample->rtt_us << 3) / USEC_PER_MSEC;
    if (delay == 0)
        delay = 1;
    
    /* first time call or link delay decreases */
    if (ca->delay_min == 0 || ca->delay_min > delay)
        ca->delay_min = delay;
    
    /* hystart triggers when cwnd is larger than some threshold */
    if (hystart && tcp_in_slow_start(tp) &&
        tp->snd_cwnd >= hystart_low_window)
        hystart_update(sk, delay);

如下Hystart检查函数hystart_update,首先,如果found如果设置了HYSTART_ACK_TRAIN或者HYSTART_DELAY标志位,这里依据启用的hystart检测方式,即hystart_detect的值,表明已经找到SlowStart的退出点,不在执行检测。

static void hystart_update(struct sock *sk, u32 delay)
{
    struct tcp_sock *tp = tcp_sk(sk);
    struct bictcp *ca = inet_csk_ca(sk);

    if (ca->found & hystart_detect)
        return;

其次,如果启用了HYSTART_ACK_TRAIN检测方式,并且当前ACK报文时间戳(此函数在tcp_ack中调用,所以now相当于ACK报文的时间戳),与上一个ACK报文时间戳小于设定的ACK-Train间隔(变量hystart_ack_delta,默认2ms,函数bictcp_clock返回的为ms为单位的时间戳),更新last_ack时间戳为当前ACK报文时间戳。

如果当前时间减去此轮ACK-Train测量开始时间戳round_start,大于最小RTT延时的一半(delay_min保存了8倍的最小RTT延时),表明网络情况恶化,退出SlowStart,将当前拥塞窗口设置为ssthresh。

    if (hystart_detect & HYSTART_ACK_TRAIN) {
        u32 now = bictcp_clock();

        /* first detection parameter - ack-train detection */
        if ((s32)(now - ca->last_ack) <= hystart_ack_delta) {
            ca->last_ack = now;
            if ((s32)(now - ca->round_start) > ca->delay_min >> 4) {
                ca->found |= HYSTART_ACK_TRAIN;
                NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPHYSTARTTRAINDETECT);
                NET_ADD_STATS(sock_net(sk), LINUX_MIB_TCPHYSTARTTRAINCWND,
                          tp->snd_cwnd);
                tp->snd_ssthresh = tp->snd_cwnd;
            }
        }
    }

最后,如果启用了HYSTART_DELAY检测方式,并且当前采用的数量小于hystart定义的HYSTART_MIN_SAMPLES(8),如果当前测量轮次的curr_rtt为零或者大于当前报文的RTT,更新curr_rtt。

否则,如果已经采用HYSTART_MIN_SAMPLES个报文,并且,采样到的curr_rtt值大于最小的RTT值加上1/8被的最小RTT值,即当curr_rtt的值大于9/8倍的最小RTT(delay_min)时,认为延时增加过大,退出SlowStart,将当前拥塞窗口值设置为ssthresh。

    if (hystart_detect & HYSTART_DELAY) {
        /* obtain the minimum delay of more than sampling packets */
        if (ca->sample_cnt < HYSTART_MIN_SAMPLES) {
            if (ca->curr_rtt == 0 || ca->curr_rtt > delay)
                ca->curr_rtt = delay;

            ca->sample_cnt++;
        } else {
            if (ca->curr_rtt > ca->delay_min +
                HYSTART_DELAY_THRESH(ca->delay_min >> 3)) {
                ca->found |= HYSTART_DELAY;
                NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPHYSTARTDELAYDETECT);
                NET_ADD_STATS(sock_net(sk), LINUX_MIB_TCPHYSTARTDELAYCWND,
                          tp->snd_cwnd);
                tp->snd_ssthresh = tp->snd_cwnd;

注意curr_rtt和delay_min保存的都是8倍的原值,而宏定义HYSTART_DELAY_THRESH,将delay_min>>3的值限定在了[4, 16]之间。

内核版本 5.0

©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页