IPVS调度算法之LBLCR

LBLCR(Locality-Based Least-Connection with Replication)调度算法,与LBLC调度算法类似,只是加入了真实目的服务器集合的概念,

调度器注册

LBLCR调度器的定义结构为ip_vs_lblcr_scheduler,使用函数register_ip_vs_scheduler注册到IPVS的调度器系统中。

static struct ip_vs_scheduler ip_vs_lblcr_scheduler =
{
        .name =                 "lblcr",
        .refcnt =               ATOMIC_INIT(0),
        .module =               THIS_MODULE,
        .n_list =               LIST_HEAD_INIT(ip_vs_lblcr_scheduler.n_list),
        .init_service =         ip_vs_lblcr_init_svc,
        .done_service =         ip_vs_lblcr_done_svc,
        .schedule =             ip_vs_lblcr_schedule,
};

static int __init ip_vs_lblcr_init(void)
{
        ret = register_ip_vs_scheduler(&ip_vs_lblcr_scheduler);
        if (ret)
                unregister_pernet_subsys(&ip_vs_lblcr_ops);
        return ret;
}

虚拟服务初始化

如下命令,在添加虚拟服务时,指定使用lblcr调度器:

# ipvsadm -A -t 207.175.44.110:80 -s lblcr

内核在虚拟服务(ip_vs_bind_scheduler函数)绑定调度器时,调用调度器的init_service函数指针。对于LBLCR调度器,即以下的ip_vs_lblcr_init_svc函数。在此函数中,分配一个ip_vs_lblcr_table结构作为虚拟服务的调度私有数据(sched_data)。 除此之外,启动一个60秒钟(CHECK_EXPIRE_INTERVAL)的定时器,用于垃圾缓存项的回收,超时处理函数为ip_vs_lblcr_check_expire。

static int ip_vs_lblcr_init_svc(struct ip_vs_service *svc)
{
        struct ip_vs_lblcr_table *tbl;

        /* Allocate the ip_vs_lblcr_table for this service
         */
        tbl = kmalloc(sizeof(*tbl), GFP_KERNEL);
        if (tbl == NULL)
                return -ENOMEM;

        svc->sched_data = tbl;

        /* Initialize the hash buckets
         */
        for (i = 0; i < IP_VS_LBLCR_TAB_SIZE; i++) {
                INIT_HLIST_HEAD(&tbl->bucket[i]);
        }
        tbl->max_size = IP_VS_LBLCR_TAB_SIZE*16;
        tbl->rover = 0;
        tbl->counter = 1;
        tbl->dead = 0;
        tbl->svc = svc;

        /* Hook periodic timer for garbage collection
         */
        timer_setup(&tbl->periodic_timer, ip_vs_lblcr_check_expire, 0);
        mod_timer(&tbl->periodic_timer, jiffies + CHECK_EXPIRE_INTERVAL);

在删除虚拟服务或者修改虚拟服务所使用的调度器时,内核需在函数ip_vs_unbind_scheduler中解绑调度器,此时如果调度器实现了done_service回调指针函数,将在此函数中被调用。对于LBLCR调度器,为以下函数:

static void ip_vs_lblcr_done_svc(struct ip_vs_service *svc)
{
        struct ip_vs_lblcr_table *tbl = svc->sched_data;

        /* remove periodic timer */
        del_timer_sync(&tbl->periodic_timer);

        /* got to clean up table entries here */
        ip_vs_lblcr_flush(svc);

        /* release the table itself */
        kfree_rcu(tbl, rcu_head);
}

以上函数执行删除init_service函数中启动的定时器(periodic_timer), 释放ip_vs_lblcr_table结构所占用内存, 以及表项集合ip_vs_lblcr_entry所占用内存。

static void ip_vs_lblcr_flush(struct ip_vs_service *svc)
{
    struct ip_vs_lblcr_table *tbl = svc->sched_data;
    struct ip_vs_lblcr_entry *en;
    struct hlist_node *next;

    spin_lock_bh(&svc->sched_lock);
    tbl->dead = 1;
    for (i = 0; i < IP_VS_LBLCR_TAB_SIZE; i++) {
        hlist_for_each_entry_safe(en, next, &tbl->bucket[i], list) {
            ip_vs_lblcr_free(en);
        }
    }
    spin_unlock_bh(&svc->sched_lock);
}

调度数据

在LBLCR算法中,私有调度数据sched_data指向ip_vs_lblcr_table类型的结构。

struct ip_vs_lblcr_table {
        struct rcu_head         rcu_head;
        struct hlist_head       bucket[IP_VS_LBLCR_TAB_SIZE];  /* hash bucket */
        atomic_t                entries;        /* number of entries */
        int                     max_size;       /* maximum size of entries */
        struct timer_list       periodic_timer; /* collect stale entries */
        struct ip_vs_service    *svc;           /* pointer back to service */
        int                     rover;          /* rover for expire check */
        int                     counter;        /* counter for no expire */
        bool                    dead;
};

其中bucket链表数组的大小默认为1K(IP_VS_LBLCR_TAB_SIZE)。 可通过内核配置CONFIG_IP_VS_LBLCR_TAB_BITS进行修改。

#ifndef CONFIG_IP_VS_LBLCR_TAB_BITS
#define CONFIG_IP_VS_LBLCR_TAB_BITS      10
#endif
#define IP_VS_LBLCR_TAB_BITS     CONFIG_IP_VS_LBLCR_TAB_BITS
#define IP_VS_LBLCR_TAB_SIZE     (1 << IP_VS_LBLCR_TAB_BITS)

结构ip_vs_lblcr_entry为链表中的表项结构。 注意与LBLC调度算法不同, 在表项结构缓存的并不是某个真实服务器结构, 而是另外一个ip_vs_dest_set结构。

struct ip_vs_lblcr_entry {
        struct hlist_node       list;
        int                     af;             /* address family */
        union nf_inet_addr      addr;           /* destination IP address */
        struct ip_vs_dest_set   set;            /* destination server set */
        unsigned long           lastuse;        /* last used time */
        struct rcu_head         rcu_head;
};

ip_vs_dest_set结构表示一个真实服务器的集合, 集合中的真实服务服务器缓存在链表项结构ip_vs_dest_set_elem中。

struct ip_vs_dest_set {
        atomic_t                size;           /* set size */
        unsigned long           lastmod;        /* last modified time */
        struct list_head        list;           /* destination list */
};

struct ip_vs_dest_set_elem {
        struct list_head        list;          /* list link */
        struct ip_vs_dest       *dest;          /* destination server */
        struct rcu_head         rcu_head;
};

调度处理

LBLCR调度函数的处理大致分成两个部分: 第一部分由4个小步骤组成。

a)在虚拟服务的调度数据(sched_data)中,根据报文的目的地址(daddr)查找缓存的表项(ip_vs_lblcr_entry), 之后由表项中的真实服务器集合结构中继续查找负荷最小的真实服务器。其权重值大于零,并且没有超负荷,当前连接仍调度到此真实目的服务器上。

static struct ip_vs_dest *ip_vs_lblcr_schedule(struct ip_vs_service *svc, const struct sk_buff *skb, struct ip_vs_iphdr *iph)
{
    struct ip_vs_lblcr_table *tbl = svc->sched_data;
    struct ip_vs_dest *dest;
    struct ip_vs_lblcr_entry *en;

    /* First look in our cache */
    en = ip_vs_lblcr_get(svc->af, tbl, &iph->daddr);
    if (en) {
        en->lastuse = jiffies;

        /* Get the least loaded destination */
        dest = ip_vs_dest_set_min(&en->set);

b) 如果当前目的服务器集合中有多个目的服务器(超过1个), 并且此集合本身有超过一天(默认)的时间为进行过修改(集合元素的插入/删除), 使用函数ip_vs_dest_set_max找到集合中负荷最重的真实服务器,将其移出此集合。

        /* More than one destination + enough time passed by, cleanup */
        if (atomic_read(&en->set.size) > 1 && time_after(jiffies, en->set.lastmod + sysctl_lblcr_expiration(svc))) {
            spin_lock_bh(&svc->sched_lock);
            if (atomic_read(&en->set.size) > 1) {
                struct ip_vs_dest *m = ip_vs_dest_set_max(&en->set);
                if (m)
                    ip_vs_dest_set_erase(&en->set, m);
            }
            spin_unlock_bh(&svc->sched_lock);
        }

c) 如果集合中匹配到的真实目的服务器没有过载, 直接将新连接调度到此服务器。

        /* If the destination is not overloaded, use it */
        if (dest && !is_overloaded(dest, svc))
                goto out;

d) 否则, 使用函数__ip_vs_lblcr_schedule进行调度选择, 并将新选择的目的服务器添加到当前的集合中。

        /* The cache entry is invalid, time to schedule */
        dest = __ip_vs_lblcr_schedule(svc);
        if (!dest) {
            ip_vs_scheduler_err(svc, "no destination available");
            return NULL;
        }

        /* Update our cache entry */
        spin_lock_bh(&svc->sched_lock);
        if (!tbl->dead)
            ip_vs_dest_set_insert(&en->set, dest, true);
        spin_unlock_bh(&svc->sched_lock);
        goto out;
    }

第二步, 如果没有匹配LBLCR缓存, 使用函数__ip_vs_lblcr_schedule进行调度, 并将新选择的目的服务器插入到lblcr缓存中(如果相应集合未创建,需首先创建集合)。

    /* No cache entry, time to schedule */
    dest = __ip_vs_lblcr_schedule(svc);
    if (!dest) {
        IP_VS_DBG(1, "no destination available\n");
        return NULL;
    }

    /* If we fail to create a cache entry, we'll just use the valid dest */
    spin_lock_bh(&svc->sched_lock);
    if (!tbl->dead)
        ip_vs_lblcr_new(tbl, &iph->daddr, svc->af, dest);
    spin_unlock_bh(&svc->sched_lock);

out:
    return dest;
}

以下我们看一看, 调度处理过程中遇到的函数。 核心的为以下的重新调度函数__ip_vs_lblcr_schedule, 其算法与在WLC调度器中介绍的完全相同。 具体可参见: https://blog.csdn.net/sinat_20184565/article/details/100567193 中的介绍。 主要用于在虚拟服务所关联的真实服务器链表中找到负荷最小(以连接数量为依据)的真实服务器。

static inline struct ip_vs_dest *__ip_vs_lblcr_schedule(struct ip_vs_service *svc)
{
    struct ip_vs_dest *dest, *least;
    int loh, doh;

    list_for_each_entry_rcu(dest, &svc->destinations, n_list) {
        if (dest->flags & IP_VS_DEST_F_OVERLOAD)
            continue;

        if (atomic_read(&dest->weight) > 0) {
            least = dest;
            loh = ip_vs_dest_conn_overhead(least);
            goto nextstage;
        }
    }
    return NULL;

    /* Find the destination with the least load.
     */
nextstage:
    list_for_each_entry_continue_rcu(dest, &svc->destinations, n_list) {
        if (dest->flags & IP_VS_DEST_F_OVERLOAD)
            continue;

        doh = ip_vs_dest_conn_overhead(dest);
        if ((__s64)loh * atomic_read(&dest->weight) > (__s64)doh * atomic_read(&least->weight)) {
            least = dest;
            loh = doh;
        }
    }
    return least;
}

真实服务器集合(lblcr表项)匹配函数ip_vs_lblcr_get, 其以当前连接的目的IP地址为依据, 首先通过计算hash值找到相应链表头, 之后遍历链表, 找到IP地址相同的集合项。

/* Get ip_vs_lblcr_entry associated with supplied parameters. */
static inline struct ip_vs_lblcr_entry *ip_vs_lblcr_get(int af, struct ip_vs_lblcr_table *tbl, const union nf_inet_addr *addr)
{
        unsigned int hash = ip_vs_lblcr_hashkey(af, addr);
        struct ip_vs_lblcr_entry *en;

        hlist_for_each_entry_rcu(en, &tbl->bucket[hash], list)
                if (ip_vs_addr_equal(af, &en->addr, addr))
                        return en;
        return NULL;
}

真实服务器集合的插入函数ip_vs_dest_set_insert, 当重新调度算法选择了一个新的真实服务器时, 使用此函数将其插入到当前的集合中, 并更新集合的修改时间戳lastmod。

static void ip_vs_dest_set_insert(struct ip_vs_dest_set *set, struct ip_vs_dest *dest, bool check)
{
    struct ip_vs_dest_set_elem *e;

    if (check) {
        list_for_each_entry(e, &set->list, list) {
            if (e->dest == dest)
                return;
        }
    }
    e = kmalloc(sizeof(*e), GFP_ATOMIC);
    if (e == NULL)
        return;

    ip_vs_dest_hold(dest);
    e->dest = dest;
    list_add_rcu(&e->list, &set->list);
    atomic_inc(&set->size);

    set->lastmod = jiffies;
}

函数ip_vs_dest_set_min负责查找集合中负荷最小的真实服务器, 相反函数ip_vs_dest_set_max负责查找集合中负荷最大的真实服务器。 两者原理相同, 在这里仅介绍一下ip_vs_dest_set_min函数。

首先在集合中找到第一个未过载, 并且权重值weight大于零, 而且可用的真实服务器, 并记录下其overhead值。 其次从其位置向后继续遍历集合, 找到负荷最小的真实服务器。 负荷的计算为: overhead除以weight权重值, 两个目的服务器的比较公式如下:

h1/w1 > h2/w2 换算为乘法: h1w2 > h2w1

函数实现如下:

static inline struct ip_vs_dest *ip_vs_dest_set_min(struct ip_vs_dest_set *set)
{
    register struct ip_vs_dest_set_elem *e;
    struct ip_vs_dest *dest, *least;
    int loh, doh;

    /* select the first destination server, whose weight > 0 */
    list_for_each_entry_rcu(e, &set->list, list) {
        least = e->dest;
        if (least->flags & IP_VS_DEST_F_OVERLOAD)
            continue;

        if ((atomic_read(&least->weight) > 0) && (least->flags & IP_VS_DEST_F_AVAILABLE)) {
            loh = ip_vs_dest_conn_overhead(least);
            goto nextstage;
        }
    }
    return NULL;

    /* find the destination with the weighted least load */
nextstage:
    list_for_each_entry_continue_rcu(e, &set->list, list) {
        dest = e->dest;
        if (dest->flags & IP_VS_DEST_F_OVERLOAD)
                continue;

        doh = ip_vs_dest_conn_overhead(dest);
        if (((__s64)loh * atomic_read(&dest->weight) > (__s64)doh * atomic_read(&least->weight)) && (dest->flags & IP_VS_DEST_F_AVAILABLE)) {
                least = dest;
                loh = doh;
        }
    }
    return least;
}

以下为lblcr表项插入函数ip_vs_lblcr_new,其封装调用了函数ip_vs_dest_set_insert,用于向集合中插入目的服务器结构, 在集合已经存在的情况下,二者功能完全相同。但是,在集合不存在的情况下,函数ip_vs_lblcr_new将向创建集合本身。

static inline struct ip_vs_lblcr_entry *ip_vs_lblcr_new(struct ip_vs_lblcr_table *tbl, const union nf_inet_addr *daddr,
                u16 af, struct ip_vs_dest *dest)
{
    struct ip_vs_lblcr_entry *en;

    en = ip_vs_lblcr_get(af, tbl, daddr);
    if (!en) {
        en = kmalloc(sizeof(*en), GFP_ATOMIC);
        if (!en)
            return NULL;

        en->af = af;
        ip_vs_addr_copy(af, &en->addr, daddr);
        en->lastuse = jiffies;

        /* initialize its dest set */
        atomic_set(&(en->set.size), 0);
        INIT_LIST_HEAD(&en->set.list);

        ip_vs_dest_set_insert(&en->set, dest, false);

        ip_vs_lblcr_hash(tbl, en);
        return en;
    }
    ip_vs_dest_set_insert(&en->set, dest, true);
    return en;
}

关于真实服务器的过载判断, 在lblcr算法中, 除了标志IP_VS_DEST_F_OVERLOAD以外, 还使用函数is_overloaded进行判断。过载是相对的,在真实服务器的活动连接数量值超过其权重值后, 并且在真实服务器链表中还存在连接数量小于其权重的一半的真实服务器, 以下函数即认为目的服务器过载, 否则,如果所有真实服务器的活动连接数量都高于各自权重值的一半时,认为真实服务器未过载。

/* If this destination server is overloaded and there is a less loaded server, then return true.
 */
static inline int is_overloaded(struct ip_vs_dest *dest, struct ip_vs_service *svc)
{
    if (atomic_read(&dest->activeconns) > atomic_read(&dest->weight)) {
        struct ip_vs_dest *d;

        list_for_each_entry_rcu(d, &svc->destinations, n_list) {
            if (atomic_read(&d->activeconns)*2 < atomic_read(&d->weight)) {
                return 1;
            }
        }
    }
    return 0;
}

超时处理

如下超时处理函数,如果ip_vs_lblcr_table结构中的counter为宏COUNT_FOR_FULL_EXPIRATION(30)的倍数, 由于超时时长为60秒,意味着经过了30分钟, 调用一次ip_vs_lblcr_full_check, 进行整个缓存的检查。

static void ip_vs_lblcr_check_expire(struct timer_list *t)
{
    struct ip_vs_lblcr_table *tbl = from_timer(tbl, t, periodic_timer);
    struct ip_vs_service *svc = tbl->svc;
    unsigned long now = jiffies;
    struct ip_vs_lblcr_entry *en;
    struct hlist_node *next;

    if ((tbl->counter % COUNT_FOR_FULL_EXPIRATION) == 0) {
        /* do full expiration check */
        ip_vs_lblcr_full_check(svc);
        tbl->counter = 1;
        goto out;
    }
    if (atomic_read(&tbl->entries) <= tbl->max_size) {
        tbl->counter++;
        goto out;
    }

如果缓存数量已经超过最大值(默认为16K), 此值可通过内核配置进行修改(CONFIG_IP_VS_LBLCR_TAB_BITS), 执行以下的清理操作。 清理数量(goal)的计算为两个部分组成: 第一清理掉已经超出max_size的值; 第二再清理掉超出值的三分之一, 最多清理掉max_size值一半的缓存项。

    goal = (atomic_read(&tbl->entries) - tbl->max_size)*4/3;
    if (goal > tbl->max_size/2)
        goal = tbl->max_size/2;

在确定了清理数量之后遍历整个缓存链表, 删除已经超时的缓存集合项, 即超过ENTRY_TIMEOUT(6分钟)时长没有使用过的集合项。 结构ip_vs_lblcr_table的成员rover用来保存本次遍历结束时的位置, 以便下次执行时由此结束位置开始。

    for (i = 0, j = tbl->rover; i < IP_VS_LBLCR_TAB_SIZE; i++) {
        j = (j + 1) & IP_VS_LBLCR_TAB_MASK;

        spin_lock(&svc->sched_lock);
        hlist_for_each_entry_safe(en, next, &tbl->bucket[j], list) {
            if (time_before(now, en->lastuse+ENTRY_TIMEOUT))
                continue;
            ip_vs_lblcr_free(en);
            atomic_dec(&tbl->entries);
            goal--;
        }
        spin_unlock(&svc->sched_lock);
        if (goal <= 0)
            break;
    }
    tbl->rover = j;
out:
    mod_timer(&tbl->periodic_timer, jiffies+CHECK_EXPIRE_INTERVAL);
}

全局清理函数ip_vs_lblcr_full_check, 每30分钟执行一次, 用来清理超过一天(24小时)未使用的缓存集合项。 此超时时间控制参见函数sysctl_lblcr_expiration。

static inline void ip_vs_lblcr_full_check(struct ip_vs_service *svc)
{
    struct ip_vs_lblcr_table *tbl = svc->sched_data;
    unsigned long now = jiffies;
    struct ip_vs_lblcr_entry *en;
    struct hlist_node *next;

    for (i = 0, j = tbl->rover; i < IP_VS_LBLCR_TAB_SIZE; i++) {
        j = (j + 1) & IP_VS_LBLCR_TAB_MASK;

        spin_lock(&svc->sched_lock);
        hlist_for_each_entry_safe(en, next, &tbl->bucket[j], list) {
            if (time_after(en->lastuse + sysctl_lblcr_expiration(svc), now))
                continue;

            ip_vs_lblcr_free(en);
            atomic_dec(&tbl->entries);
        }
        spin_unlock(&svc->sched_lock);
    }
    tbl->rover = j;
}

默认情况下次超时时间为DEFAULT_EXPIRATION(24小时), 也可通过proc文件/proc/net/ipv4/vs/lblcr_expiration进行修改。

static int sysctl_lblcr_expiration(struct ip_vs_service *svc)
{
#ifdef CONFIG_SYSCTL
    return svc->ipvs->sysctl_lblcr_expiration;
#else
    return DEFAULT_EXPIRATION;
#endif
}

内核版本 4.15

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