欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

IPVS虚拟服务

程序员文章站 2024-03-13 16:09:09
...

以下两者方式创建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

相关标签: ipvs