IPVS的Persistent持续调度

如下ipvsadm配置命令,开启persistent选项之后,ipvs将来之同一个客户端的请求全部调度到一个固定的真实服务器上。对于SSL和FTP这类,其多个报文之间是相互关联的协议,需要开启此功能。但是对于NAT转发模式,由于NAT将对端口号进行修改,FTP服务需要使用ip_vs_ftp模块才能正常工作。

命令行选项netmask默认为255.255.255.255,即仅对一个客户端执行持续调度。如下指定掩码为255.255.255.0,可对同一个网段内的所有客户端,调度到同一个目的服务器进行处理。

# ipvsadm -A -t 207.175.44.110:80 -s rr --persistent 500 --netmask 255.255.255.0

ipvsadm工具将命令行选项p(–persistent)参数,转换为虚拟服务的标志IP_VS_SVC_F_PERSISTENT,和超时时间,下发到内核中。最长时间为MAX_TIMEOUT(31天),如果不指定时长,默认为300秒。

static int parse_options(int argc, char **argv, struct ipvs_command_entry *ce, unsigned int *options, unsigned int *format)
{
    while ((c=poptGetNextOpt(context)) >= 0){
        switch (c) {
        case 'p':
            set_option(options, OPT_PERSISTENT);
            ce->svc.flags |= IP_VS_SVC_F_PERSISTENT;
            ce->svc.timeout = parse_timeout(optarg, 1, MAX_TIMEOUT);
            break;

持续调度

在主调度函数ip_vs_schedule中,在执行通常的调度处理之前,首先执行的就是以下虚拟服务是否开启了持续调度的判断,即标志IP_VS_SVC_F_PERSISTENT,如果为真,使用持续调度处理函数ip_vs_sched_persist先行进行处理。

struct ip_vs_conn *ip_vs_schedule(struct ip_vs_service *svc, struct sk_buff *skb,
           struct ip_vs_proto_data *pd, int *ignored, struct ip_vs_iphdr *iph)
{
    /* Persistent service
     */
    if (svc->flags & IP_VS_SVC_F_PERSISTENT)
        return ip_vs_sched_persist(svc, skb, cport, vport, ignored, iph);

在ipvs函数ip_vs_sched_persist中,如果没有指定当前报文的连接模板,首先创建模板,在创建相应连接结构。后续的报文,如果匹配此模板,将据此模板生成连接结构,而不再执行调度,这样确保了根据模板生成的连接,具有相同的目的服务器,以实现持续调度的功能。

注意以下的客户端地址和掩码的与操作,可实现前述的将一个网段内的所有客户端调度到同一个目的服务器上。

static struct ip_vs_conn *ip_vs_sched_persist(struct ip_vs_service *svc, struct sk_buff *skb, 
            __be16 src_port, __be16 dst_port, int *ignored, struct ip_vs_iphdr *iph)   
{ 
    if (likely(!ip_vs_iph_inverse(iph))) {   
        src_addr = &iph->saddr;    
        dst_addr = &iph->daddr;    
    } else {
        src_addr = &iph->daddr;    
        dst_addr = &iph->saddr;    
    }

    /* Mask saddr with the netmask to adjust template granularity */
#ifdef CONFIG_IP_VS_IPV6
    if (svc->af == AF_INET6)
        ipv6_addr_prefix(&snet.in6, &src_addr->in6, (__force __u32) svc->netmask);
    else
#endif
        snet.ip = src_addr->ip & svc->netmask;

如果配置的IPVS虚拟服务指定了服务端口号,根据端口号是否为FTP服务端口,创建的连接模板有所不同。如果非FTP服务端口,将创建模板:<caddr, 0, vaddr, vport, daddr, dport>,第二项客户端源端口指定为0意味着所有客户端的报文,都将调度到相同的目的地址(最好两项daddr和dport)。反之,如果虚拟服务为FTP服务端口,将创建模板: <caddr, 0, vaddr, 0, daddr, 0>,与第一个模板不同,此连接模板的最后一项dport为零,表明虽然会调度到同一个真实目的服务器,但是目的端口号不是固定的,这是由于在FTP被动模式下,FTP服务端的数据链路端口号不是固定的。

    {
        int protocol = iph->protocol;
        const union nf_inet_addr *vaddr = dst_addr;
        __be16 vport = 0;

        if (dst_port == svc->port) {
            /* non-FTP template: <protocol, caddr, 0, vaddr, vport, daddr, dport>
             * FTP template:     <protocol, caddr, 0, vaddr, 0, daddr, 0>
             */
            if (svc->port != FTPPORT)
                vport = dst_port;
        } else {

与以上不同,如果配置IPVS虚拟服务时未指定端口号,或者是使用防火墙标记fwmark配置的虚拟服务,将创建不同于以上的连接模板。对于fwmark配置方式,生成模板:<IPPROTO_IP,caddr,0,fwmark,0,daddr,0>;对于未指定端口号,生成模板:<protocol,caddr,0,vaddr,0,daddr,0>。

            /* Note: persistent fwmark-based services and persistent port zero service are handled here.
             * fwmark template:     <IPPROTO_IP,caddr,0,fwmark,0,daddr,0>
             * port zero template:  <protocol,caddr,0,vaddr,0,daddr,0>
             */
            if (svc->fwmark) {
                protocol = IPPROTO_IP;
                vaddr = &fwmark;
            }
        }
        /* return *ignored = -1 so NF_DROP can be used */
        if (ip_vs_conn_fill_param_persist(svc, skb, protocol, &snet, 0, vaddr, vport, &param) < 0) {
            *ignored = -1;
            return NULL;
        }
    }

随后使用ip_vs_ct_in_get函数根据以上生成的param参数结构,查询是否为已存在的模板。如果已经存在,进一步检查此连接模板是否可用(主要为模板中的目的服务器是否可用),由函数ip_vs_check_template完成。如果以上条件不满足,将执行重新调度,获取新的目的服务器。

    /* Check if a template already exists */
    ct = ip_vs_ct_in_get(&param);
    if (!ct || !ip_vs_check_template(ct, NULL)) {
        struct ip_vs_scheduler *sched;

        /* No template found or the dest of the connection template is not available. return *ignored=0 i.e. ICMP and NF_DROP
         */
        sched = rcu_dereference(svc->scheduler);
        if (sched) {
            /* read svc->sched_data after svc->scheduler */
            smp_rmb();
            dest = sched->schedule(svc, skb, iph);
        } else {
            dest = NULL;
        }
        if (!dest) {
            IP_VS_DBG(1, "p-schedule: no dest found.\n");
            kfree(param.pe_data);
            *ignored = 0;
            return NULL;
        }

得到新的目的服务器之后,随后创建一个新的连接模板。对于IPVS配置中指定端口号,并且其不等于FTP的情况,在模板中固定目的服务器的目的端口。新创建的连接指定标志IP_VS_CONN_F_TEMPLATE。

        if (dst_port == svc->port && svc->port != FTPPORT)
            dport = dest->port;

        /* Create a template
         * This adds param.pe_data to the template, and thus param.pe_data will be destroyed when the template expires */
        ct = ip_vs_conn_new(&param, dest->af, &dest->addr, dport, IP_VS_CONN_F_TEMPLATE, dest, skb->mark);

        ct->timeout = svc->timeout;

如果连接模板可用,直接取出其目的服务器结构。

    } else {
        /* set destination with the found template */
        dest = ct->dest;
        kfree(param.pe_data);
    }

依据目的服务器结构,为当前的报文创建一个连接结构,函数ip_vs_control_add将新建连接的control指针指向模板连接,并增加模板连接结构中的统计计数n_control。

    dport = dst_port;
    if (dport == svc->port && dest->port)
        dport = dest->port;

    flags = (svc->flags & IP_VS_SVC_F_ONEPACKET && iph->protocol == IPPROTO_UDP) ? IP_VS_CONN_F_ONE_PACKET : 0;

    /* Create a new connection according to the template */
    ip_vs_conn_fill_param(svc->ipvs, svc->af, iph->protocol, src_addr, src_port, dst_addr, dst_port, &param);

    cp = ip_vs_conn_new(&param, dest->af, &dest->addr, dport, flags, dest, skb->mark);

    /* Add its control */
    ip_vs_control_add(cp, ct);

    return cp;

连接模板查询

以下查询函数ip_vs_ct_in_get分为两个部分,第一如果PE(Persistent Engine)存在,使用其匹配函数ct_match进行匹配查找。

/* Get reference to connection template */
struct ip_vs_conn *ip_vs_ct_in_get(const struct ip_vs_conn_param *p)
{   
    struct ip_vs_conn *cp;
        
    hash = ip_vs_conn_hashkey_param(p, false);

    hlist_for_each_entry_rcu(cp, &ip_vs_conn_tab[hash], c_list) {
        if (unlikely(p->pe_data && p->pe->ct_match)) {
            if (cp->ipvs != p->ipvs)
                continue;
            if (p->pe == cp->pe && p->pe->ct_match(p, cp)) {
                if (__ip_vs_conn_get(cp))
                    goto out;
            }
            continue;
        }   

否则,通过对比以下项参数进行匹配:1)协议族; 2)客户端地址; 3)虚拟服务地址; 4)虚拟服务端口; 5)客户端端口; 6)协议; 7)相同的ipvs。最重要的是遍历到的连接的flags必须要有IP_VS_CONN_F_TEMPLATE标志。

        if (cp->af == p->af &&
            ip_vs_addr_equal(p->af, p->caddr, &cp->caddr) &&
            /* protocol should only be IPPROTO_IP if * p->vaddr is a fwmark */
            ip_vs_addr_equal(p->protocol == IPPROTO_IP ? AF_UNSPEC : p->af, p->vaddr, &cp->vaddr) &&
            p->vport == cp->vport && p->cport == cp->cport &&
            cp->flags & IP_VS_CONN_F_TEMPLATE &&
            p->protocol == cp->protocol &&
            cp->ipvs == p->ipvs) {
            if (__ip_vs_conn_get(cp))
                goto out;
        }
    }
    cp = NULL;

  out:
    return cp;

目前IPVS支持的PE引擎只有SIP,其连接匹配函数为ip_vs_sip_ct_match,在这里增加了对PE数据的比较。对于SIP而言,其PE数据为呼叫ID(call id)。

static bool ip_vs_sip_ct_match(const struct ip_vs_conn_param *p, struct ip_vs_conn *ct)

{
    bool ret = false;

    if (ct->af == p->af &&
        ip_vs_addr_equal(p->af, p->caddr, &ct->caddr) &&
        /* protocol should only be IPPROTO_IP if d_addr is a fwmark */
        ip_vs_addr_equal(p->protocol == IPPROTO_IP ? AF_UNSPEC : p->af, p->vaddr, &ct->vaddr) &&
        ct->vport == p->vport &&
        ct->flags & IP_VS_CONN_F_TEMPLATE &&
        ct->protocol == p->protocol &&
        ct->pe_data && ct->pe_data_len == p->pe_data_len &&
        !memcmp(ct->pe_data, p->pe_data, p->pe_data_len))
        ret = true;

    return ret;

内核版本 4.15

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