SFB队列

SFB(Stochastic Fair Blue)是一个FIFO类型的队列算法,基于类似于BLUE算法的记账机制,来标识非响应性质的流,并且限制其速率(这类流不处理ECN或者丢包事件)。SFB的记账系统由L*N个桶(bin)组成,其中L表示级别,N表示每个级别的桶的数量。Linux内核中使用8个级别,每个级别16个桶用于记账。

 37 #define SFB_BUCKET_SHIFT 4
 38 #define SFB_NUMBUCKETS  (1 << SFB_BUCKET_SHIFT) /* N bins per Level */
 39 #define SFB_BUCKET_MASK (SFB_NUMBUCKETS - 1)
 40 #define SFB_LEVELS  (32 / SFB_BUCKET_SHIFT) /* L */

另外,SFB维护着L数量的独立哈希函数,对应于记账系统中的L个级别,每个哈希函数将数据流映射到其对应级别的某个桶中,这些桶用于记录匹配流的队列占用情况。SFB的每个桶维持着一个类似于BLUE算法的marking/dropping probability (Pm)值,每次桶被占用的使用更新此值。例如,在某个报文到达队列后,根据L个级别的相应哈希函数,将其哈希到每个级别的N个桶中的一个。如果摸个映射到某个桶的报文数量超过一定的阈值,此桶的Pm值将增加;如果桶中的报文数据降为零,减小Pm值。

如下为使用伪代码描述的以上介绍的SFB记账系统算法。

B[l][n]: L x N array of bins (L levels, N bins per level)
enque()
    Calculate hash function values h0, h1, ..., hL-1;
    Update bins at each level
    for i = 0 to L - 1
        if (B[i][hi].qlen > bin size)
            B[i][hi].pm += delta;
            Drop packet;
        else if (B[i][hi].qlen == 0)
            B[i][hi].pm -= delta;
    pmin = min(B[0][h0].pm .. B[L][hL].pm);
    if (pmin == 1)
        ratelimit()
    else
        Mark/drop with probability pmin;

对于某个非响应的流,其会很快将SFB记账系统中L个级别的每个级别对应的哈希桶的Pm值拉高为1。对于响应式的流,其有可能与非响应流共同使用某几个级别中的桶,但是,只要非响应流的数量不是远远大于桶的数量,响应式流就有很大可能哈希到至少一个没有被非响应式占用的桶,以上算法可见,流的最小Pm值为每个级别中其对应的哈希桶的Pm值中的最小值,所有,响应式流将保持一个正常的Pm值。报文的(ECN)标记依据最小Pm值实现,如果Pmin为1,SFB认为此流为一个非响应式流,将对其进行限速(penalty_rate)。

如下图所示,一个非响应式流将它映射到的所有哈希桶的Pm(marking probablilities)值都增加到了1,而图中的TCP流(响应式流)在级别0与非响应式流哈希到了同一个桶中,但是在其它级别中二者完全在不同的桶中,所以,TCP流的最小Pmin没有受到影响,其值为0.2。另一方面,由于非响应式流的最小Pm为1,其被标识为非响应流,并且进行限速处理。

在这里插入图片描述

基于以上SFB的介绍, 响应式流也有可能也非响应式流共用所有相同的哈希桶,导致其被误判定为非响应流。对于一个L级别,每个级别B个桶的SFB,如果非响应式流的数量为M,那么,响应式流被误判的可能p由以下公式给出:

p = [ 1 − ( 1 − 1 B ) M ] L p = \left [ 1 - \left ( 1-\frac{1}{B} \right )^{M} \right ]^{L} p=[1(1B1)M]L

对于Linux内核的8级别、每级别16个桶的情况,带入以上公式可得:

p = [ 1 − ( 1 − 1 16 ) M ] 8 p = \left [ 1 - \left ( 1-\frac{1}{16} \right )^{M} \right ]^{8} p=[1(1161)M]8

举例来说,对于数量为10(M=10)的非响应式流的系统,响应式流被误判的可能性为0.2%。为解决此问题,SFB每隔一段时间执行重哈希操作,将误判限制在一定的时间内。

以下为tc命令中sfb的帮助信息和配置命令。

$ tc qdisc add sfb help
What is "help"?
Usage: ... sfb [ rehash SECS ] [ db SECS ]
            [ limit PACKETS ] [ max PACKETS ] [ target PACKETS ]
            [ increment FLOAT ] [ decrement FLOAT ]
            [ penalty_rate PPS ] [ penalty_burst PACKETS ]
$ 
$ sudo tc qdisc add dev ens38 handle 1: root sfb rehash 600000 db 60000 limit 1000 max 25 target 20 increment 0.00050 decrement 0.00005 penalty_rate 10 penalty_burst 20
$ 
$ tc qdisc show dev ens38
qdisc sfb 1: root refcnt 2 limit 1000 max 25 target 20
  increment 0.00050 decrement 0.00005 penalty rate 10 burst 20 (600000ms 60000ms)
$ 

以上配置的实际上都是SFB的默认参数,参见以下的sfb_default_ops结构的成员赋值。limit参数为零的话,将在初始化过程中重新赋值为网络设备的发送队列长度(例如:1000)。

static const struct tc_sfb_qopt sfb_default_ops = {
    .rehash_interval = 600 * MSEC_PER_SEC,
    .warmup_time = 60 * MSEC_PER_SEC,
    .limit = 0,
    .max = 25,
    .bin_size = 20,
    .increment = (SFB_MAX_PROB + 500) / 1000, /* 0.1 % */
    .decrement = (SFB_MAX_PROB + 3000) / 6000,
    .penalty_rate = 10,
    .penalty_burst = 20,
};  

以下命令用于显示sfb的相关运行信息:

$ tc -s qdisc show dev ens38 
qdisc sfb 1: root refcnt 2 limit 1000 max 25 target 20
  increment 0.00050 decrement 0.00005 penalty rate 10 burst 20 (600ms 60ms)
 Sent 1032 bytes 12 pkt (dropped 0, overlimits 0 requeues 0) 
 backlog 0b 0p requeues 0 
  earlydrop 0 penaltydrop 0 bucketdrop 0 queuedrop 0 childdrop 0 marked 0
  maxqlen 0 maxprob 0.00000 avgprob 0.00000 

SFB入队列

如下SFB入队列函数sfb_enqueue,当队列长度超过或者等于限制值limit时,丢弃报文。

static int sfb_enqueue(struct sk_buff *skb, struct Qdisc *sch, struct sk_buff **to_free)
{

    struct sfb_sched_data *q = qdisc_priv(sch);
    struct Qdisc *child = q->qdisc;
    struct tcf_proto *fl;
    u32 p_min = ~0;
    u32 minqlen = ~0;
    u32 r, sfbhash;
    u32 slot = q->slot;
    int ret = NET_XMIT_SUCCESS | __NET_XMIT_BYPASS;

    if (unlikely(sch->q.qlen >= q->limit)) {
        qdisc_qstats_overlimit(sch);
        q->stats.queuedrop++;
        goto drop;
    }

如果重新进行哈希操作的时间间隔大于零,判断是否时间已到,成立的话进行扰数的更新和slot的切换,参见函数sfb_swap_slot,并且更新重哈希时间戳。否则,如果到了双缓存的预热时间,开启双缓存double_buffering预热,变量warmup_time定义了在重哈希之前的多长时间为预热时间。

    if (q->rehash_interval > 0) {
        unsigned long limit = q->rehash_time + q->rehash_interval;

        if (unlikely(time_after(jiffies, limit))) {
            sfb_swap_slot(q);
            q->rehash_time = jiffies;
        } else if (unlikely(!q->double_buffering && q->warmup_time > 0 &&
                    time_after(jiffies, limit - q->warmup_time))) {
            q->double_buffering = true;
        }
    }

SFB可使用tc配置的外部classifier分类器,也可使用内部的哈希计算值(默认情况),两种情况都要增加扰数(perturbation-每个slot有不同的扰数)。

    fl = rcu_dereference_bh(q->filter_list);
    if (fl) {
        u32 salt;

        /* If using external classifiers, get result and record it. */
        if (!sfb_classify(skb, fl, &ret, &salt))
            goto other_drop;
        sfbhash = jhash_1word(salt, q->bins[slot].perturbation);
    } else {
        sfbhash = skb_get_hash_perturb(skb, q->bins[slot].perturbation);
    }


    if (!sfbhash)
        sfbhash = 1;
    sfb_skb_cb(skb)->hashes[slot] = sfbhash;

得到的哈希值为32bit,内核的SFB实现使用8个级别(SFB_LEVELS),每个级别16个桶(SFB_BUCKET_SHIFT=4),这样每4位的哈希值可覆盖一个级别内所有的桶。如果哈希桶的队列长度为零,降低Pm值。反之,如果队列长度大于设定的桶大小(bin_size,这里为20),增加Pm的值。

另外,在循环过程中,记录下最小的队列长度,以及Pm的最小值(p_min)。

    for (i = 0; i < SFB_LEVELS; i++) {
        u32 hash = sfbhash & SFB_BUCKET_MASK;
        struct sfb_bucket *b = &q->bins[slot].bins[i][hash];

        sfbhash >>= SFB_BUCKET_SHIFT;
        if (b->qlen == 0)
            decrement_prob(b, q);
        else if (b->qlen >= q->bin_size)
            increment_prob(b, q);
        if (minqlen > b->qlen)
            minqlen = b->qlen;
        if (p_min > b->p_mark)
            p_min = b->p_mark;
    }

如果最小队列长度(minqlen)已经大于设定的最大值,将报文丢弃。

    slot ^= 1;
    sfb_skb_cb(skb)->hashes[slot] = 0;

    if (unlikely(minqlen >= q->max)) {
        qdisc_qstats_overlimit(sch);
        q->stats.bucketdrop++;
        goto drop;
    }

以上的slot与1的异或操作,已经反转了slot的值。在Pmin的值大于等于SFB_MAX_PROB的情况下,如果启用了double_buffering预热功能,依据报文的哈希结果,对预备slot的哈希桶进行类似的操作。最后,调整到enqueue执行,将报文添加到队列中。

    if (unlikely(p_min >= SFB_MAX_PROB)) {
        /* Inelastic flow */
        if (q->double_buffering) {
            sfbhash = skb_get_hash_perturb(skb, q->bins[slot].perturbation);
            if (!sfbhash)
                sfbhash = 1;
            sfb_skb_cb(skb)->hashes[slot] = sfbhash;

            for (i = 0; i < SFB_LEVELS; i++) {
                u32 hash = sfbhash & SFB_BUCKET_MASK;
                struct sfb_bucket *b = &q->bins[slot].bins[i][hash];

                sfbhash >>= SFB_BUCKET_SHIFT;
                if (b->qlen == 0)
                    decrement_prob(b, q);
                else if (b->qlen >= q->bin_size)
                    increment_prob(b, q);
            }
        }
        if (sfb_rate_limit(skb, q)) {
            qdisc_qstats_overlimit(sch);
            q->stats.penaltydrop++;
            goto drop;
        }
        goto enqueue;
    }

如果以上条件不成立,进行随机报文丢弃(RED/ECN)处理。即Pmin的值小于SFB_MAX_PROB,取一个随机值,如果此随机值(r)小于Pmin,并且报文支持ECN,设置marked标记,否则,丢弃报文。但是,在此之前,先判断一下Pmin是否过大,如果大于SFB_MAX_PROB的一半,并且随机值r的一半小于Pmin超出(SFB_MAX_PROB / 2)的值,直接丢弃报文,不进行ECN标记判断。

    r = prandom_u32() & SFB_MAX_PROB;

    if (unlikely(r < p_min)) {
        if (unlikely(p_min > SFB_MAX_PROB / 2)) {
            /* If we're marking that many packets, then either
             * this flow is unresponsive, or we're badly congested.
             * In either case, we want to start dropping packets.
             */
            if (r < (p_min - SFB_MAX_PROB / 2) * 2) {
                q->stats.earlydrop++;
                goto drop;
            }
        }
        if (INET_ECN_set_ce(skb)) {
            q->stats.marked++;
        } else {
            q->stats.earlydrop++;
            goto drop;
        }
    }

以下为qdisc的入队列函数,成功入队列之后,使用函数increment_qlen增加SFB哈希桶中相应的队列长度。

enqueue:
    ret = qdisc_enqueue(skb, child, to_free);
    if (likely(ret == NET_XMIT_SUCCESS)) {
        qdisc_qstats_backlog_inc(sch, skb);
        sch->q.qlen++;
        increment_qlen(skb, q);
    } else if (net_xmit_drop_count(ret)) {
        q->stats.childdrop++;
        qdisc_qstats_drop(sch);
    }

SFB出队列

SFB的出队列处理相对来说比较简单,调用SFB的函数decrement_qlen减少哈希桶中对于bin的队列长度。

static struct sk_buff *sfb_dequeue(struct Qdisc *sch)
{
    struct sfb_sched_data *q = qdisc_priv(sch);
    struct Qdisc *child = q->qdisc;
    struct sk_buff *skb;

    skb = child->dequeue(q->qdisc);

    if (skb) {
        qdisc_bstats_update(sch, skb);
        qdisc_qstats_backlog_dec(sch, skb);
        sch->q.qlen--;
        decrement_qlen(skb, q);
    }

    return skb;
}

SFB虚拟队列长度

在成功执行qdisc_enqueue之后,调用increment_qlen增加队列长度。这里分别对两个slot:0和1,分别执行增加操作。对于队列长度的递减decrement_qlen函数,原理相同,分别递减slot0和1中相应的哈希桶队列长度。

static void increment_qlen(const struct sk_buff *skb, struct sfb_sched_data *q)
{
    u32 sfbhash;

    sfbhash = sfb_hash(skb, 0);
    if (sfbhash)
        increment_one_qlen(sfbhash, 0, q);

    sfbhash = sfb_hash(skb, 1);
    if (sfbhash)
        increment_one_qlen(sfbhash, 1, q);
}

遍历指定slot中,每个级别(SFB_LEVELS)中哈希对应的桶,如果其队列长度小于0xFFFF,将其递增。队列的递减函数decrement_one_qlen,原理与此相同。

static void increment_one_qlen(u32 sfbhash, u32 slot, struct sfb_sched_data *q)
{
    int i;
    struct sfb_bucket *b = &q->bins[slot].bins[0][0];

    for (i = 0; i < SFB_LEVELS; i++) {
        u32 hash = sfbhash & SFB_BUCKET_MASK;

        sfbhash >>= SFB_BUCKET_SHIFT;
        if (b[hash].qlen < 0xFFFF)
            b[hash].qlen++;
        b += SFB_NUMBUCKETS; /* next level */
    }
}

标记概率Pm计算

当队列长度等于零时,减低Pm的值,如下函数decrement_prob。递减的步长decrement可通过TC命令设置,默认为:(SFB_MAX_PROB + 3000) / 6000。

static void decrement_prob(struct sfb_bucket *b, struct sfb_sched_data *q)
{
    b->p_mark = prob_minus(b->p_mark, q->decrement);
}

当队列长度大于等于bin_size时(以上TC命令设置为20,其也是默认值),增加Pm值。递增的步长increment可通过TC命令设置,默认为:(SFB_MAX_PROB + 500) / 1000。

static void increment_prob(struct sfb_bucket *b, struct sfb_sched_data *q)
{
    b->p_mark = prob_plus(b->p_mark, q->increment);
}

Pm值使用Q0.16格式表示,即没有整数部分,小数部分由16bit表示。默认情况下,increment的值等于:(65535 + 500)/1000 = 66.035,除以SFB_MAX_PROB的话,相当于0.001007。decrement的默认值为:(65535 + 3000)/6000 = 11.4225,除以SFB_MAX_PROB的话,相当于0.000174。

#define SFB_MAX_PROB 0xFFFF

static const struct tc_sfb_qopt sfb_default_ops = {
    .rehash_interval = 600 * MSEC_PER_SEC,
    .warmup_time = 60 * MSEC_PER_SEC,
    .limit = 0,
    .max = 25,
    .bin_size = 20,
    .increment = (SFB_MAX_PROB + 500) / 1000, /* 0.1 % */
    .decrement = (SFB_MAX_PROB + 3000) / 6000,

以下为Pm的增减函数prob_plus和prob_minus,其取值范围为:[0, 65535]。

static u32 prob_plus(u32 p1, u32 p2)
{
    u32 res = p1 + p2;

    return min_t(u32, res, SFB_MAX_PROB);
}

static u32 prob_minus(u32 p1, u32 p2)
{
    return p1 > p2 ? p1 - p2 : 0;
}

SFB哈希扰数

如下函数sfb_swap_slot,在切换slot时,更新之前使用的slot的扰数值(perturbation),关闭双缓存预热开关。

static void sfb_init_perturbation(u32 slot, struct sfb_sched_data *q)
{
    q->bins[slot].perturbation = prandom_u32();
}

static void sfb_swap_slot(struct sfb_sched_data *q)
{
    sfb_init_perturbation(q->slot, q);
    q->slot ^= 1;
    q->double_buffering = false;
}

非响应性流限速

如果penalty_rate或者penalty_burst设置为零,丢弃非响应性流的报文。初始化时tokens_avail设置为与penalty_burst相同的值,token_time为初始化时的时间戳。所以,开始时一直递减可用的token值(tokens_avail),并且不丢报文,不限速。当前tokens_avail递减到小于1时,检查经过了多长时间(不大于10秒钟),根据此时间长度更新可用的令牌值,其值不能大于penalty_burst给出的上限值。tokens_avail小于1时,进行限速。

static bool sfb_rate_limit(struct sk_buff *skb, struct sfb_sched_data *q)
{
    if (q->penalty_rate == 0 || q->penalty_burst == 0)
        return true;

    if (q->tokens_avail < 1) {
        unsigned long age = min(10UL * HZ, jiffies - q->token_time);

        q->tokens_avail = (age * q->penalty_rate) / HZ;
        if (q->tokens_avail > q->penalty_burst)
            q->tokens_avail = q->penalty_burst;
        q->token_time = jiffies;
        if (q->tokens_avail < 1)
            return true;
    }

    q->tokens_avail--;
    return false;

内核版本 5.0

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