IPVS系统的连接同步 - 准备阶段

以下命令启动和停止ipvs系统的连接同步功能,master和backup表示主同步进程;backup表示备份进程,接收master同步的连接信息:

# ipvsadm --start-daemon master --mcast-interface ens39 --syncid 2
# ipvsadm --start-daemon backup --mcast-interface ens39 --syncid 2
# ipvsadm --stop-daemon master
# ipvsadm --stop-daemon backup

注意,如果不指定–mcast-interface参数,ipvsadm命令默认使用eth0接口,如果系统中无此接口将导致配置失败。参数syncid如果不指定,默认为255。

内核处理接口

Linux内核中处理以上命令的函数由基于通用netlink接口的ip_vs_genl_set_daemon,和基于套接口的do_ip_vs_set_ctl。本质上两者最终调用的是相同的核心处理函数。以下我们以通用netlink接口为例,看一下内核的配置处理。

由结构ip_vs_genl_ops的定义可知,同步daemon的开启和关闭使用相同的函数ip_vs_genl_set_daemon进行处理。

static const struct genl_ops ip_vs_genl_ops[] = {
    {
        .cmd    = IPVS_CMD_NEW_DAEMON,
        .flags  = GENL_ADMIN_PERM,
        .policy = ip_vs_cmd_policy,
        .doit   = ip_vs_genl_set_daemon,
    },
    {
        .cmd    = IPVS_CMD_DEL_DAEMON,
        .flags  = GENL_ADMIN_PERM,
        .policy = ip_vs_cmd_policy,
        .doit   = ip_vs_genl_set_daemon,
    },
}

处理函数ip_vs_genl_set_daemon,主要负责命令参数的合法判断工作。 IPVS_CMD_ATTR_DAEMON属性是必须的,如果确实返回错误。

static int ip_vs_genl_set_daemon(struct sk_buff *skb, struct genl_info *info)
{
    int ret = -EINVAL, cmd;
    struct net *net = sock_net(skb->sk);
    struct netns_ipvs *ipvs = net_ipvs(net);

    cmd = info->genlhdr->cmd;

    if (cmd == IPVS_CMD_NEW_DAEMON || cmd == IPVS_CMD_DEL_DAEMON) {
        struct nlattr *daemon_attrs[IPVS_DAEMON_ATTR_MAX + 1];

        if (!info->attrs[IPVS_CMD_ATTR_DAEMON] ||
            nla_parse_nested(daemon_attrs, IPVS_DAEMON_ATTR_MAX, info->attrs[IPVS_CMD_ATTR_DAEMON], ip_vs_daemon_policy, info->extack))
            goto out;

        if (cmd == IPVS_CMD_NEW_DAEMON)
            ret = ip_vs_genl_new_daemon(ipvs, daemon_attrs);
        else
            ret = ip_vs_genl_del_daemon(ipvs, daemon_attrs);
    }

out:
    return ret;
}

新增同步线程

函数ip_vs_genl_new_daemon主要负责netlink属性参数的解析工作,并将解析结构保存到内核的ipvs_sync_daemon_cfg结构中。 首先,确定三个必要参数是否存在,包括:IPVS_DAEMON_ATTR_STATE(master/backup)、IPVS_DAEMON_ATTR_MCAST_IFN接口名称和同步ID(IPVS_DAEMON_ATTR_SYNC_ID)。

static int ip_vs_genl_new_daemon(struct netns_ipvs *ipvs, struct nlattr **attrs)
{
    struct ipvs_sync_daemon_cfg c;
    struct nlattr *a;

    memset(&c, 0, sizeof(c));
    if (!(attrs[IPVS_DAEMON_ATTR_STATE] && attrs[IPVS_DAEMON_ATTR_MCAST_IFN] && attrs[IPVS_DAEMON_ATTR_SYNC_ID]))
        return -EINVAL;
    strlcpy(c.mcast_ifn, nla_data(attrs[IPVS_DAEMON_ATTR_MCAST_IFN]), sizeof(c.mcast_ifn));
    c.syncid = nla_get_u32(attrs[IPVS_DAEMON_ATTR_SYNC_ID]);

以下操作由netlink属性中分别解析出同步报文长度IPVS_DAEMON_ATTR_SYNC_MAXLEN、多播地址组IPVS_DAEMON_ATTR_MCAST_GROUP、多播端口IPVS_DAEMON_ATTR_MCAST_PORT、多播生存期IPVS_DAEMON_ATTR_MCAST_TTL和同步线程状态值IPVS_DAEMON_ATTR_STATE。在本文开头的ipvsadm命令配置中,除去最后的IPVS_DAEMON_ATTR_STATE属性,其它属性都没有配置,内核使用相应的默认值。

    a = attrs[IPVS_DAEMON_ATTR_SYNC_MAXLEN];
    if (a)
        c.sync_maxlen = nla_get_u16(a);

    a = attrs[IPVS_DAEMON_ATTR_MCAST_GROUP];
    if (a) {
        c.mcast_af = AF_INET;
        c.mcast_group.ip = nla_get_in_addr(a);
        if (!ipv4_is_multicast(c.mcast_group.ip))
            return -EINVAL;
    } else {
        a = attrs[IPVS_DAEMON_ATTR_MCAST_GROUP6];
        if (a) {
#ifdef CONFIG_IP_VS_IPV6
            int addr_type;

            c.mcast_af = AF_INET6;
            c.mcast_group.in6 = nla_get_in6_addr(a);
            addr_type = ipv6_addr_type(&c.mcast_group.in6);
            if (!(addr_type & IPV6_ADDR_MULTICAST))
                return -EINVAL;
#else
            return -EAFNOSUPPORT;
#endif
        }
    }

    a = attrs[IPVS_DAEMON_ATTR_MCAST_PORT];
    if (a)
        c.mcast_port = nla_get_u16(a);

    a = attrs[IPVS_DAEMON_ATTR_MCAST_TTL];
    if (a)
        c.mcast_ttl = nla_get_u8(a);

注意,当前的同步线程与混合有IPv4/IPv6地址的配置不兼容。函数的最后调用同步线程启动函数start_sync_thread。

/* The synchronization protocol is incompatible with mixed family services */
if (ipvs->mixed_address_family_dests > 0)
    return -EINVAL;

rtnl_lock();
mutex_lock(&ipvs->sync_mutex);
ret = start_sync_thread(ipvs, &c, nla_get_u32(attrs[IPVS_DAEMON_ATTR_STATE]));

}

同步线程启动准备

如果同步状态为空,即IP_VS_STATE_NONE,同步线程未启动,根据同步端口数量设置将要启动的线程数量,每个端口启动一个线程,但是线程数量控制在1到64(IPVS_SYNC_PORTS_MAX)之间。如果同步线程已经启动,不管其状态是master或slave,线程数量加1。

int start_sync_thread(struct netns_ipvs *ipvs, struct ipvs_sync_daemon_cfg *c, int state)
{
    struct ip_vs_sync_thread_data *tinfo;
    struct task_struct **array = NULL, *task;
    int (*threadfn)(void *data);

    if (!ipvs->sync_state) {
        count = clamp(sysctl_sync_ports(ipvs), 1, IPVS_SYNC_PORTS_MAX);
        ipvs->threads_mask = count - 1;
    } else
        count = ipvs->threads_mask + 1;

如果用户未指定同步使用的协议族、端口和TTL,内核使用默认的IP_VS_SYNC_GROUP(0xe0000051 - 224.0.0.81)、IP_VS_SYNC_PORT(8848)和1。如果同步线程的状态为备份backup,MTU值取指定接口设备的MTU值,但是范围锁定在1500到65535之间;对于状态为主master的情况,固定使用1500的MTU值。

    if (c->mcast_af == AF_UNSPEC) {
        c->mcast_af = AF_INET;
        c->mcast_group.ip = cpu_to_be32(IP_VS_SYNC_GROUP);
    }
    if (!c->mcast_port)
        c->mcast_port = IP_VS_SYNC_PORT;
    if (!c->mcast_ttl)
        c->mcast_ttl = 1;

    dev = __dev_get_by_name(ipvs->net, c->mcast_ifn);
    if (!dev) {
        pr_err("Unknown mcast interface: %s\n", c->mcast_ifn);
        return -ENODEV;
    }
    hlen = (AF_INET6 == c->mcast_af) ? sizeof(struct ipv6hdr) + sizeof(struct udphdr) : sizeof(struct iphdr) + sizeof(struct udphdr);
    mtu = (state == IP_VS_STATE_BACKUP) ? clamp(dev->mtu, 1500U, 65535U) : 1500U;
    min_mtu = (state == IP_VS_STATE_BACKUP) ? 1024 : 1;

如果没有指定同步报文的消息体长度,此处使用MTU值减去IP头部(IPv4/IPv6)和UDP头部的长度得到其值。

    if (c->sync_maxlen)
        c->sync_maxlen = clamp_t(unsigned int, c->sync_maxlen, min_mtu, 65535 - hlen);
    else
        c->sync_maxlen = mtu - hlen;

对于主master同步线程,其线程主函数为sync_thread_master;对于备份backup线程,其主函数为sync_thread_backup。

    if (state == IP_VS_STATE_MASTER) {
        if (ipvs->ms)
            return -EEXIST;

        ipvs->mcfg = *c;
        name = "ipvs-m:%d:%d";
        threadfn = sync_thread_master;
    } else if (state == IP_VS_STATE_BACKUP) {
        if (ipvs->backup_threads)
            return -EEXIST;

        ipvs->bcfg = *c;
        name = "ipvs-b:%d:%d";
        threadfn = sync_thread_backup;
    } else {
        return -EINVAL;
    }

对于主master状态的同步线程,按照端口数量分配同步状态结构,即为每一个端口分配一个此结构,用于保存同步信息。同时,为每个端口同步线程分配一个work处理函数master_wakeup_work_handler。而对于备份slave状态的同步线程,仅需分配与端口数量相等的线程结构task_struct。

    if (state == IP_VS_STATE_MASTER) {
        struct ipvs_master_sync_state *ms;

        ipvs->ms = kcalloc(count, sizeof(ipvs->ms[0]), GFP_KERNEL);

        ms = ipvs->ms;
        for (id = 0; id < count; id++, ms++) {
            INIT_LIST_HEAD(&ms->sync_queue);
            ms->sync_queue_len = 0;
            ms->sync_queue_delay = 0;
            INIT_DELAYED_WORK(&ms->master_wakeup_work, master_wakeup_work_handler);
            ms->ipvs = ipvs;
        }
    } else {
        array = kcalloc(count, sizeof(struct task_struct *), GFP_KERNEL);

以下的循环,将启动与端口数量相等的内核同步线程。首先,创建同步操作所使用的内核套接口,对于master主状态同步线程,创建客户端发送UDP套接口;对于slave状态同步线程,创建接收套接口线程。最后是使用kthread_run启动相应的内核线程。

    for (id = 0; id < count; id++) {
        if (state == IP_VS_STATE_MASTER)
            sock = make_send_sock(ipvs, id);
        else
            sock = make_receive_sock(ipvs, id, dev->ifindex);

        tinfo = kmalloc(sizeof(*tinfo), GFP_KERNEL);

        tinfo->ipvs = ipvs;
        tinfo->sock = sock;
        if (state == IP_VS_STATE_BACKUP) {
            tinfo->buf = kmalloc(ipvs->bcfg.sync_maxlen, GFP_KERNEL);
        } else {
            tinfo->buf = NULL;
        }
        tinfo->id = id;
        task = kthread_run(threadfn, tinfo, name, ipvs->gen, id);

        tinfo = NULL;
        if (state == IP_VS_STATE_MASTER)
            ipvs->ms[id].master_thread = task;
        else
            array[id] = task;
    }

    /* mark as active */

    if (state == IP_VS_STATE_BACKUP)
        ipvs->backup_threads = array;
    spin_lock_bh(&ipvs->sync_buff_lock);
    ipvs->sync_state |= state;

对于主master状态的同步线程,使用函数make_send_sock创建发送报文的套接口。 函数set_mcast_if用于绑定出接口,应用层可使用套接口的选项SO_BINDTODEVICE绑定出接口,对应于内核中sock套接口结构的成员sk_bound_dev_if,对于多播地址而言,函数set_mcast_if设置的是内核inet_sock结构体成员mc_index,其优先级低于sk_bound_dev_if,仅在后者未设置的情况下生效。

函数set_mcast_loop的功能相当于在用户层面设置套接口选项IP_MULTICAST_LOOP,其作用是将发出的多播报文送回本地的监听套接口。具体实现可参见多播函数ip_mc_output中对函数ip_mc_finish_output的调用,后者通过dev_loopback_xmit函数将多播报文送回协议栈。

默认情况下ipvs的多播TTL设置为1,禁止多播同步报文转发到其他网络中。 函数set_mcast_pmtudisc设置为不发送带有DF标志的报文,将超出MTU的报文进行分片处理。

函数set_sock_size的功能相当于在用户层设置套接口选项SO_SNDBUF,设置套接口发送缓存的大小,此值可通过PROC系统文件/proc/sys/net/ipv4/vs/sync_sock_size进行修改。

static struct socket *make_send_sock(struct netns_ipvs *ipvs, int id)
{
    union ipvs_sockaddr mcast_addr;
    struct socket *sock;

    /* First create a socket */
    result = sock_create_kern(ipvs->net, ipvs->mcfg.mcast_af, SOCK_DGRAM, IPPROTO_UDP, &sock);
    result = set_mcast_if(sock->sk, ipvs->mcfg.mcast_ifn);

    set_mcast_loop(sock->sk, 0);
    set_mcast_ttl(sock->sk, ipvs->mcfg.mcast_ttl);
    
    set_mcast_pmtudisc(sock->sk, IP_PMTUDISC_DONT); /* Allow fragmentation if MTU changes */
    result = sysctl_sync_sock_size(ipvs);
    if (result > 0)
        set_sock_size(sock->sk, 1, result);

    if (AF_INET == ipvs->mcfg.mcast_af)
        result = bind_mcastif_addr(sock, ipvs->mcfg.mcast_ifn);
    else
        result = 0;

    get_mcast_sockaddr(&mcast_addr, &salen, &ipvs->mcfg, id);
    result = sock->ops->connect(sock, (struct sockaddr *) &mcast_addr, salen, 0);

对于IPv4协议族,函数bind_mcastif_addr用于绑定(bind)用户指定的出接口上的IP地址。最后,对于状态为slave的同步线程,其接收套接口的初始化函数make_receive_sock,与以上的函数make_send_sock基本相同,区别在于前者需要调用join_mcast_group函数加入多播组。

static struct socket *make_receive_sock(struct netns_ipvs *ipvs, int id, int ifindex)
{
    /* join the multicast group */
#ifdef CONFIG_IP_VS_IPV6
    if (ipvs->bcfg.mcast_af == AF_INET6)
        result = join_mcast_group6(sock->sk, &mcast_addr.in6.sin6_addr, ipvs->bcfg.mcast_ifn);
    else
#endif
        result = join_mcast_group(sock->sk, &mcast_addr.in.sin_addr, ipvs->bcfg.mcast_ifn);
}

内核版本 4.15

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