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