IPVS虚拟服务

以下两者方式创建ipvs虚拟服务。前者指定为目的地址和端口为:207.175.44.110:80,协议为TCP(-t选项)的流量提供虚拟服务,采用的调度方式为轮询Round-Robin(-rr)。后者指定为防火墙标记(Firewall-Mark)为1的流量提供虚拟服务,调度方式采用与前者相同的轮询方式。

ipvsadm -A -t 207.175.44.110:80 -s rr
ipvsadm -A -f 1  -s rr

对于后者,可间接的通过如下命令指定流量的特征。此iptables命令实现的效果与前者相同。

# iptables  -A PREROUTING -t mangle -d 207.175.44.110/32 -p tcp --dport 80 -j MARK --set-mark 1
#
# iptables -t mangle -L -n --line-number                                                       
Chain PREROUTING (policy ACCEPT)
num  target     prot opt source               destination         
          
4    MARK       tcp  --  0.0.0.0/0            207.175.44.110       tcp dpt:80 MARK set 0x1

虚拟服务添加

ipvsadm默认通过通用netlink接口下发配置命令。无论是通过netlink还是setsockopt,在内核中最后执行添加虚拟服务的函数都是同一个ip_vs_add_service。不过,在执行添加操作之前,内核需要检查是否已经存在相关的虚拟服务,由函数__ip_vs_service_find来完成。

已添加的虚拟服务保存在全局链表数组ip_vs_svc_table中,数组的每个元素为一个链表,数组的索引是由命名空间成员netns_ipvs的地址、虚拟服务的地址族、协议、虚拟地址和虚拟端口计算而来的hash值。在函数__ip_vs_service_find的查找过程中,如果发现已存在和将要配置的虚拟服务的地址族、虚拟地址、端口号、协议和命名空间都完全相同的虚拟服务,表明已有相同的虚拟服务存在,返回已存在的虚拟服务结构体地址。对于添加操作,返回EEXIST错误码。

需要注意的是使用firewall-mark添加的虚拟服务保存在全局链表ip_vs_svc_fwm_table中,其查找函数为__ip_vs_svc_fwm_find。

static inline struct ip_vs_service *
__ip_vs_service_find(struct netns_ipvs *ipvs, int af, __u16 protocol, const union nf_inet_addr *vaddr, __be16 vport)
{
    struct ip_vs_service *svc;

    /* Check for "full" addressed entries */
    hash = ip_vs_svc_hashkey(ipvs, af, protocol, vaddr, vport);

    hlist_for_each_entry_rcu(svc, &ip_vs_svc_table[hash], s_list) {
        if ((svc->af == af)
            && ip_vs_addr_equal(af, &svc->addr, vaddr)
            && (svc->port == vport)
            && (svc->protocol == protocol)
            && (svc->ipvs == ipvs)) {
            /* HIT */
            return svc;
        }
}

虚拟服务添加函数为ip_vs_add_service。首先,如果ipvsadm命令指定了调度器的名称,需要通过函数ip_vs_scheduler_get根据名称进行查找,以判断该调度器是否注册在系统中,内核中的所有注册的调度器都保存在全局链表ip_vs_schedulers中,此函数即是遍历链表,比较名称的过程。如果没有找到,返回错误。

static int ip_vs_add_service(struct netns_ipvs *ipvs, struct ip_vs_service_user_kern *u, struct ip_vs_service **svc_p)
{
    int ret = 0, i;
    struct ip_vs_scheduler *sched = NULL;
    struct ip_vs_pe *pe = NULL;
    struct ip_vs_service *svc = NULL;

    /* Lookup the scheduler by 'u->sched_name' */
    if (strcmp(u->sched_name, "none")) {
        sched = ip_vs_scheduler_get(u->sched_name);
        if (!sched) {
            pr_info("Scheduler module ip_vs_%s not found\n", u->sched_name);
            ret = -ENOENT;
            goto out_err;
        }
    }

目前内核支持的调度方式有:Round Robin(rr)、Weighted Round Robin(wrr)、Least-Connection(lc)、Weighted Least-Connection(wlc)、Locality-Based Least-Connection(lblc)、Locality-Based Least-Connection with Replication(lblcr)、Destination Hashing(dh)、Source Hashing(sh)、Shortest Expected Delay(sed)和Never Queue(nq)。

与以上的调度器类似,所以内核注册的PE(Persistence Engine)都保存在全局链表ip_vs_pe中,函数ip_vs_pe_getbyname通过ipvsadm指定的PE名称进行遍历查找,目前ipvs系统仅支持sip一种PE。SIP用于确保call-id相同的SIP流量调度到相同的真实服务器。

if (u->pe_name && *u->pe_name) {
    pe = ip_vs_pe_getbyname(u->pe_name);
    if (pe == NULL) {
        pr_info("persistence engine module ip_vs_pe_%s " "not found\n", u->pe_name);
        ret = -ENOENT;
        goto out_err;
    }
}

此处是一个对IPv6地址掩码的合法性检查,前缀长度必须在[1,128]之间。

#ifdef CONFIG_IP_VS_IPV6
    if (u->af == AF_INET6) {
        __u32 plen = (__force __u32) u->netmask;

        if (plen < 1 || plen > 128) {
            ret = -EINVAL;
            goto out_err;
        }
    }
#endif

以上的所有检查完成之后,开始分配新的虚拟服务ip_vs_service类型结构体,以及根据ipvsadm参数初始化结构体的各个成员变量。

svc = kzalloc(sizeof(struct ip_vs_service), GFP_KERNEL);

svc->stats.cpustats = alloc_percpu(struct ip_vs_cpu_stats);
for_each_possible_cpu(i) {
    struct ip_vs_cpu_stats *ip_vs_stats;
    ip_vs_stats = per_cpu_ptr(svc->stats.cpustats, i);
    u64_stats_init(&ip_vs_stats->syncp);
}

/* I'm the first user of the service */
atomic_set(&svc->refcnt, 0);

svc->af = u->af;
svc->protocol = u->protocol;
ip_vs_addr_copy(svc->af, &svc->addr, &u->addr);
svc->port = u->port;
svc->fwmark = u->fwmark;
svc->flags = u->flags;
svc->timeout = u->timeout * HZ;
svc->netmask = u->netmask;
svc->ipvs = ipvs;

INIT_LIST_HEAD(&svc->destinations);
spin_lock_init(&svc->sched_lock);
spin_lock_init(&svc->stats.lock);

如果在ipvsadm命令中指定了调度器,如上文提到的rr调度器,在此处绑定调度器。

/* Bind the scheduler */
if (sched) {
    ret = ip_vs_bind_scheduler(svc, sched);
    if (ret)
        goto out_err;
    sched = NULL;
}

对于Round Robin调度器,其绑定函数实际上为ip_vs_rr_init_svc,用于将虚拟服务的成员destinations赋予sched_data成员,即此调度器的数据就是各个真实的服务器。

    static int ip_vs_rr_init_svc(struct ip_vs_service *svc)
    {
        svc->sched_data = &svc->destinations;
        return 0;
    }

添加虚拟服务函数的末尾,由ip_vs_svc_hash将新创建的虚拟服务添加到全局链表中,ip_vs_svc_table或者ip_vs_svc_fwm_table链表。

    /* Bind the ct retriever */
    RCU_INIT_POINTER(svc->pe, pe);
    pe = NULL;

    /* Hash the service into the service table */
    ip_vs_svc_hash(svc);

    *svc_p = svc;
    /* Now there is a service - full throttle */
    ipvs->enable = 1;
    return 0;

虚拟服务编辑

虚拟服务的地址族、虚拟地址、端口号和协议等4个字段是不可修改的,唯一的标识了一个虚拟服务。可修改的字段包括调度方式、掩码和PE等,具体参见以下的函数ip_vs_edit_service的内容。

ipvsadm -E -t 207.175.44.110:80 -s rr
ipvsadm -E -f 1  -s rr

以下为内核中修改虚拟服务的执行函数ip_vs_edit_service,其中开头对调度器和PE的检查与创建虚拟服务函数ip_vs_add_service中的处理相同。真正的修改代码集中在调度器的更换、PE更换,虚拟服务的标志字段、timeout和netmask字段的更新。

static int ip_vs_edit_service(struct ip_vs_service *svc, struct ip_vs_service_user_kern *u)
{
    struct ip_vs_scheduler *sched = NULL, *old_sched;
    struct ip_vs_pe *pe = NULL, *old_pe = NULL;

    old_sched = rcu_dereference_protected(svc->scheduler, 1);
    if (sched != old_sched) {
        if (old_sched) {
            ip_vs_unbind_scheduler(svc, old_sched);
            RCU_INIT_POINTER(svc->scheduler, NULL);
            /* Wait all svc->sched_data users */
            synchronize_rcu();
        }
        /* Bind the new scheduler */
        if (sched) {
            ret = ip_vs_bind_scheduler(svc, sched);
            if (ret) {
                ip_vs_scheduler_put(sched);
                goto out;
            }
        }
    }

调度器的更换涉及老的调度器的解绑定,由函数ip_vs_unbind_scheduler完成;以及新的调度器的绑定操作,由函数ip_vs_bind_scheduler完成,其实际上调用的是各个调度器自身注册的初始化函数。

由于此虚拟服务已经在创建时添加到了全局虚拟服务链表中,参数flags需要增加IP_VS_SVC_F_HASHED标志,表明不在进行重新添加。

    svc->flags = u->flags | IP_VS_SVC_F_HASHED;
    svc->timeout = u->timeout * HZ;
    svc->netmask = u->netmask;

    old_pe = rcu_dereference_protected(svc->pe, 1);
    if (pe != old_pe) {
        rcu_assign_pointer(svc->pe, pe);
        /* check for optional methods in new pe */
        new_pe_conn_out = (pe && pe->conn_out) ? true : false;
        old_pe_conn_out = (old_pe && old_pe->conn_out) ? true : false;
        if (new_pe_conn_out && !old_pe_conn_out)
            atomic_inc(&svc->ipvs->conn_out_counter);
        if (old_pe_conn_out && !new_pe_conn_out)
            atomic_dec(&svc->ipvs->conn_out_counter);
    }

虚拟服务删除

以下ipvsadm命令删除指定的虚拟服务。

ipvsadm -D -t 207.175.44.110:80
ipvsadm -D -f 1

内核中的虚拟服务删除操作由函数ip_vs_del_service完成,其为函数ip_vs_unlink_service的封装函数。

static int ip_vs_del_service(struct ip_vs_service *svc)
{
    if (svc == NULL)
        return -EEXIST;
    ip_vs_unlink_service(svc, false);

    return 0;
}

在函数ip_vs_unlink_service中,首先将操作的虚拟服务由全局服务链表中移除,其次调用核心函数__ip_vs_del_service删除虚拟服务。

static void ip_vs_unlink_service(struct ip_vs_service *svc, bool cleanup)
{
    /* Hold svc to avoid double release from dest_trash */
    atomic_inc(&svc->refcnt);
    /*
     * Unhash it from the service table
     */
    ip_vs_svc_unhash(svc);

    __ip_vs_del_service(svc, cleanup);
}

删除操作包括:解绑定虚拟服务的调度器;解绑定PE;遍历虚拟服务中的真实服务器链表destinations,移除所有的真实服务器。

static void __ip_vs_del_service(struct ip_vs_service *svc, bool cleanup)
{
    struct ip_vs_dest *dest, *nxt;
    struct ip_vs_scheduler *old_sched;
    struct ip_vs_pe *old_pe;
    struct netns_ipvs *ipvs = svc->ipvs;

    ip_vs_stop_estimator(svc->ipvs, &svc->stats);

    /* Unbind scheduler */
    old_sched = rcu_dereference_protected(svc->scheduler, 1);
    ip_vs_unbind_scheduler(svc, old_sched);
    ip_vs_scheduler_put(old_sched);

    /* Unbind persistence engine, keep svc->pe */
    old_pe = rcu_dereference_protected(svc->pe, 1);
    if (old_pe && old_pe->conn_out)
        atomic_dec(&ipvs->conn_out_counter);
    ip_vs_pe_put(old_pe);

    list_for_each_entry_safe(dest, nxt, &svc->destinations, n_list) {
        __ip_vs_unlink_dest(svc, dest, 0);
        __ip_vs_del_dest(svc->ipvs, dest, cleanup);
    }

    if (svc->port == FTPPORT)
        atomic_dec(&ipvs->ftpsvc_counter);
    else if (svc->port == 0)
        atomic_dec(&ipvs->nullsvc_counter);

虚拟服务清空

使用以下的ipvsadm命令清空所有的虚拟服务。

ipvsadm --clear 或
ipvsadm -C

内核中的函数ip_vs_flush负责处理此操作。其循环ip_vs_svc_table和ip_vs_svc_fwm_table数组(两个长度为IP_VS_SVC_TAB_SIZE的数组),以及遍历每个数组成员链表,利用上节介绍的函数ip_vs_unlink_service,来清空当前网络命名空间中的所有虚拟服务。

static int ip_vs_flush(struct netns_ipvs *ipvs, bool cleanup)
{
    struct ip_vs_service *svc;
    struct hlist_node *n;

    /* Flush the service table hashed by <netns,protocol,addr,port> */
    for(idx = 0; idx < IP_VS_SVC_TAB_SIZE; idx++) {
        hlist_for_each_entry_safe(svc, n, &ip_vs_svc_table[idx], s_list) {
            if (svc->ipvs == ipvs)
                ip_vs_unlink_service(svc, cleanup);
        }
    }

    /* Flush the service table hashed by fwmark */
    for(idx = 0; idx < IP_VS_SVC_TAB_SIZE; idx++) {
        hlist_for_each_entry_safe(svc, n, &ip_vs_svc_fwm_table[idx], f_list) {
            if (svc->ipvs == ipvs)
                ip_vs_unlink_service(svc, cleanup);
        }
    }

内核版本 4.15

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