以下命令启动和停止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