运行中的ipvs
ipvs 的规则实现原理
ipvs的规则是如何生效的,先来看看他实现的原理
简单的来讲,ipvs无非就是修改了数据报头信息来完成client -> virus server -> real server的调度.调度的目的是使realservers之间的负载接近于平衡状态.这里牵扯到2个问题,修改数据报的方式和调度的策略.
我们先来看看修改数据报的具体方式,现在2.6内核中ipvs实现的方式和原来有点不一样.引用一下ipvs的作者张文嵩先生的一段话
我们分别在Linux 内核2.0和内核2.2中修改了TCP/IP协议栈,在IP层截取和改写/转发IP报文,实现了三种IP负载均衡技术,并提供了一个ipvsadm程序进行虚拟服务器的配置和管理。在Linux内核2.4和2.6中,我们把它实现为NetFilter的一个模块,很多代码作了改写和进一步优化,目前版本已在网上发布,根据反馈信息该版本已经较稳定。
好吧,说得很清楚了,ipvs就是借用netfilter来修改数据报的.那么简单了解一下netfilter的工作原理还是很有必要的,如图
netfilter一共有5个规则链,每个规则链都能存放若干条规则,规则之间都顺序(也就是优先级),一旦有规则被匹配到,完成相应动作后,跳出该规则链.这5个规则链分别是PREROUTING,INPUT,FORWARD,OUTPUT,POSTROUTING.我们可以将机器中的连接分成3中状态
从外部进入主机的连接,经过 PREROUTING -> INPUT 从主机出去的连接,将经过 OUPUT -> POSTROUTING 由主机转发的连接,经过PREROUTING -> FORWARD -> POSTROUTING
每个规则链里的规则会在数据经过该规则链的时候起作用(也就是调用相应的函数进行处理).看上去很简单吧,比如ipvs作为netfilter的一个模块,往这些规则链里写入规则就好可以了
等等.如果netfilter有很多模块,都往一个规则链里写入规则,会不会很乱呢?优先级如何控制呢?所以规则链里的规则我们会根据不同的作用将其分类进行管理,每一类的规则用一个整数来表示他的优先级,越小,优先级越高.如果是同一类型的规则,则根据规则的先后顺序来决定(链表结构,越靠前,优先级越高)
netfilter本身有3个作用,所以他的规则分为3种类型,用3个表来表示,分别为filter表(过滤),nat表(修改数据报头),mangle表(修改数据).而ipvs模块就相当于在netfilter里添加了一张新的ipvs表一样.关于netfilter的更多信息,请参考
文献一ipvs 的规则实现过程
每当有新的连接(数据报)经过netfilter的规则链时,就会调用NF_HOOK()函数.此函数会访问一个全部变量nf_hooks.这个变量里存放了netfilter的所有表(包括filter,nat,mangle和ipvs附加表等),以及每个表的规则链,规则链里的函数调用.然后遍历nf_hooks变量里相应规则链里的所有信息,根据优先级进行相应的函数调用,每个规则链里的函数都会根据该规则链里的规则对数据报进行匹配和处理
还记得在前一部分的最后,讲到的nf_register_hook()部分吗?正是ipvs使用ret = nf_register_hooks(ip_vs_ops, ARRAY_SIZE(ip_vs_ops)); 往nf_hooks变量里加入了一些数据,才使得ipvs的规则能被netfilter执行.接下来我们来看看加入的都是些什么数据
ip_vs_ops的数据内容是
net/ipv4/ipvs/ip_vs_core.c
static struct nf_hook_ops ip_vs_ops[] __read_mostly = { /* After packet filtering, forward packet through VS/DR, VS/TUN, * or VS/NAT(change destination), so that filtering rules can be * applied to IPVS. */ { .hook = ip_vs_in, //调用的函数名称,也就是说只要有数据经过INPUT规则链,就会调用ip_vs_in()对数据进行匹配和处理 .owner = THIS_MODULE, //模块的名称 .pf = PF_INET, //协议族的名称,一般都是ip(PF_INET)协议 .hooknum = NF_INET_LOCAL_IN, //规则链的代号,为INPUT .priority = 100, //优先级 }, /* After packet filtering, change source only for VS/NAT */ { .hook = ip_vs_out, //对经过FORWARD的数据调用ip_vs_out()进行处理 .owner = THIS_MODULE, .pf = PF_INET, .hooknum = NF_INET_FORWARD, .priority = 100, }, /* After packet filtering (but before ip_vs_out_icmp), catch icmp * destined for 0.0.0.0/0, which is for incoming IPVS connections */ { .hook = ip_vs_forward_icmp, //对经过FORWARD的数据调用ip_vs_forward_icmp()进行处理 .owner = THIS_MODULE, .pf = PF_INET, .hooknum = NF_INET_FORWARD, .priority = 99, }, /* Before the netfilter connection tracking, exit from POST_ROUTING */ { .hook = ip_vs_post_routing, //对经过POSTROUTING的数据调用ip_vs_post_routing()进行处理 .owner = THIS_MODULE, .pf = PF_INET, .hooknum = NF_INET_POST_ROUTING, .priority = NF_IP_PRI_NAT_SRC-1, }, };
可以看到,ipvs一共在INPUT,FORWARD,POSTROUTING这3个规则链里一共添加了4个处理的函数.接下来一个一个来分析
ip_vs_in()ip_vs_out()ip_vs_forward_icmp()ip_vs_post_routing()ip_vs_in()
ip_vs_in()被放置在INPUT规则链里,会检查进入本机的所有数据报.作用是将访问vs(虚拟服务器)的连接转给rs(真实服务器),达到负载均衡的目的,如何调度与配置时的调度算法相关.如何修改数据报头部与VS的类型相关,VS有3种类型
VS/NAT会修改s_addr, d_addr, d_port(可能) VS/DR会修改d_addr, d_port(可能) VS/TUN直接在原来数据报的基础上加一个新的包头,也叫封装
在这个函数中,对所有目的地址为本机(调度服务器)的数据进行了处理,从skb(sk_buff)中提出连接的协议结构pp(ip_vs_protocol),找出哪些skb(sk_buff)符合虚拟服务的规则svc(ip_vs_service),并找到与之对应的cp(ip_vs_conn),如果没有找到就new一个cp,并将其加入到ip_vs_conn_tab列表中).最后根据cp->packet_xmit()的方法对数据进行传送.当然,有很多的参数需要更新,比如连接的状态,pp,cp,skb的计数器等等...
net/ipv4/ipvs/ip_vs_core.c
/* * Check if it's for virtual services, look it up, * and send it on its way... */ //这里翻译一下,检查数据报是否是发往vs(虚拟服务器)的,如果是,将其转发到它该去的地方... static unsigned int ip_vs_in(unsigned int hooknum, struct sk_buff *skb, const struct net_device *in, const struct net_device *out, int (*okfn)(struct sk_buff *)) //hooknum是规则链代号;*skb是数据报头部;*in记录了数据报从哪个网络设备进来;*out记录了数据报将会从哪个网络设备出去(如果知道的话); *okfn()是一个处理sk_buff指针的函数指针,基本上没用到 { struct iphdr *iph; struct ip_vs_protocol *pp; struct ip_vs_conn *cp; int ret, restart; int ihl; /* * Big tappo: only PACKET_HOST (neither loopback nor mcasts) * ... don't know why 1st test DOES NOT include 2nd (?) */ if (unlikely(skb->pkt_type != PACKET_HOST //如果数据不是给本地网络(我们/PACKET_HOST)的 || skb->dev->flags & IFF_LOOPBACK || skb->sk)) { //或者是给lo设备的,或者是一个sock已经建立好的连接(应该是指本机已存在的真实连接吧) IP_VS_DBG(12, "packet type=%d proto=%d daddr=%d.%d.%d.%d ignored\n", skb->pkt_type, ip_hdr(skb)->protocol, NIPQUAD(ip_hdr(skb)->daddr)); //调用IP_VS_DBG做下记录 return NF_ACCEPT; //立刻返回NF_ACCEPT(意味着继续下一个hook函数) } //而作为一个vs机器,以上情况是很少发生的,所以用到了unlikely这样的gcc预编译函数.以加快执行速度 iph = ip_hdr(skb); //得到ip层头部信息 if (unlikely(iph->protocol == IPPROTO_ICMP)) { //如果数据报是icmp协议 int related, verdict = ip_vs_in_icmp(skb, &related, hooknum); //用ip_vs_in_icmp()进行处理 if (related) //如果是相关联的连接 return verdict; //用ip_vs_in_icmp()返回的值退出 iph = ip_hdr(skb); //否则得到skb的网络层头部指针(ip_hdr()使用的是偏移量得到的指针位置) } /* Protocol supported? */ pp = ip_vs_proto_get(iph->protocol); //如果是ipvs不认识的协议,pass掉 if (unlikely(!pp)) return NF_ACCEPT; ihl = iph->ihl << 2; //iph->ihl是以4byte为一个单位,所以要做一个转换 /* * Check if the packet belongs to an existing connection entry */ cp = pp->conn_in_get(skb, pp, iph, ihl, 0); //该连接是否已存在,cp为连接状态 if (unlikely(!cp)) { //如果在ip_vs_conn_tab中找不到该连接(也就是该连接是第一次访问vs的话) int v; if (!pp->conn_schedule(skb, pp, &v, &cp)) //利用该协议定义的conn_schedule函数为skb选择合适的rs,并根据skb,pp生成一个新的cp.并将cp添加到ip_vs_conn_tab中.rs的选择请查看相应协议的conn_schedule函数,比如tcp_conn_schedule() return v; //添加失败时,返回错误码 } if (unlikely(!cp)) { //不可知的异常,输出debug信息后,退出 /* sorry, all this trouble for a no-hit :) */ IP_VS_DBG_PKT(12, pp, skb, 0, "packet continues traversal as normal"); return NF_ACCEPT; } IP_VS_DBG_PKT(11, pp, skb, 0, "Incoming packet"); /* Check the server status */ if (cp->dest && !(cp->dest->flags & IP_VS_DEST_F_AVAILABLE)) { //如果目标地址不可用 /* the destination server is not available */ if (sysctl_ip_vs_expire_nodest_conn) { //让cp立刻超时 /* try to expire the connection immediately */ ip_vs_conn_expire_now(cp); } /* don't restart its timer, and silently drop the packet. */ __ip_vs_conn_put(cp); //cp计数器-1 return NF_DROP; } ip_vs_in_stats(cp, skb); //更新cp,skb的计数器(连接数和数据量) restart = ip_vs_set_state(cp, IP_VS_DIR_INPUT, skb, pp); //更新skb连接在IP_VS_DIR_INPUT位置的状态 if (cp->packet_xmit) //调用cp的packet_xmit()将数据传送出去,函数是在建立cp的时候,由ip_vs_bind_xmit(cp),根据dest->flags(真实服务器的标记)来决定的,有5种方法ip_vs_nat_xmit,ip_vs_tunnel_xmit,ip_vs_dr_xmit,ip_vs_null_xmit,ip_vs_bypass_xmit ret = cp->packet_xmit(skb, cp, pp); /* do not touch skb anymore */ else { IP_VS_DBG_RL("warning: packet_xmit is null"); ret = NF_ACCEPT; } /* Increase its packet counter and check if it is needed * to be synchronized * * Sync connection if it is about to close to * encorage the standby servers to update the connections timeout */ atomic_inc(&cp->in_pkts); //计数器 if ((ip_vs_sync_state & IP_VS_STATE_MASTER) && (((cp->protocol != IPPROTO_TCP || cp->state == IP_VS_TCP_S_ESTABLISHED) && (atomic_read(&cp->in_pkts) % sysctl_ip_vs_sync_threshold[1] == sysctl_ip_vs_sync_threshold[0])) || ((cp->protocol == IPPROTO_TCP) && (cp->old_state != cp->state) && ((cp->state == IP_VS_TCP_S_FIN_WAIT) || (cp->state == IP_VS_TCP_S_CLOSE))))) ip_vs_sync_conn(cp); //将ip_vs_conn的信息添加到sync_buff,可用于vs(调度服务器)之间的信息同步 cp->old_state = cp->state; ip_vs_conn_put(cp); //释放cp return ret; }
ip_vs_out()
此函数放在FORWARD规则链上,经过本机进行转发的skb都会被该函数处理.在vs/nat模式下,内网的rs返回给client的数据会经网关(本机)转发,这个时候需要修改数据报的源地址,将其修改为网关的公网ip地址,这样才能使连接持续下去,否则client将无法访问到rs(内网地址)
net/ipv4/ipvs/ip_vs_core.c
/* * It is hooked at the NF_INET_FORWARD chain, used only for VS/NAT. * Check if outgoing packet belongs to the established ip_vs_conn, * rewrite addresses of the packet and send it on its way... */ static unsigned int ip_vs_out(unsigned int hooknum, struct sk_buff *skb, const struct net_device *in, const struct net_device *out, int (*okfn)(struct sk_buff *)) { struct iphdr *iph; struct ip_vs_protocol *pp; struct ip_vs_conn *cp; int ihl; EnterFunction(11); //debug if (skb->ipvs_property) //如果已经被ipvs修改过,直接pass return NF_ACCEPT; iph = ip_hdr(skb); //得到skb的网络层头部信息起始指针 if (unlikely(iph->protocol == IPPROTO_ICMP)) { //如果是icmp协议的数据 int related, verdict = ip_vs_out_icmp(skb, &related); //用ip_vs_out_icmp处理 if (related) //如果是相关联的连接 return verdict; //返回verdict iph = ip_hdr(skb); //否则再次得到iph(ip层头部指针)***为什么又运行一次呢? } pp = ip_vs_proto_get(iph->protocol); //得到ipvs的ip_vs_proto结构pp if (unlikely(!pp)) //如果是ipvs不支持的协议,pass掉 return NF_ACCEPT; /* reassemble IP fragments */ if (unlikely(iph->frag_off & htons(IP_MF|IP_OFFSET) && //如果skb是一个分片 !pp->dont_defrag)) { if (ip_vs_gather_frags(skb, IP_DEFRAG_VS_OUT)) //则重组以后,标记为NF_STOLEN返回,防止netfilter对其再次操作 return NF_STOLEN; iph = ip_hdr(skb); //如果重组失败,再次得到iph.***重复3次了 } ihl = iph->ihl << 2; //转成byte为长度单位,默认为4byte /* * Check if the packet belongs to an existing entry */ cp = pp->conn_out_get(skb, pp, iph, ihl, 0); //检查skb是否是ip_vs_conn_tab中某个连接(client -> rs)的相关连接(rs -> client),如果是,则返回cp(ip_vs_conn),如果不是,cp为NULL if (unlikely(!cp)) { //如果cp不存在 if (sysctl_ip_vs_nat_icmp_send && //sysctl_ip_vs_nat_icmp_send值为0,后面的代码貌似不会继续执行了,这部分代码估计是debug用的 (pp->protocol == IPPROTO_TCP || //skb为tcp协议或者udp协议 pp->protocol == IPPROTO_UDP)) { __be16 _ports[2], *pptr; pptr = skb_header_pointer(skb, ihl, //得到skb端口信息 sizeof(_ports), _ports); if (pptr == NULL) //如果没端口,pass return NF_ACCEPT; /* Not for me */ if (ip_vs_lookup_real_service(iph->protocol, //通过协议/源地址/源端口去寻找是否是内网的某个rs发出的tcp/udp数据报 iph->saddr, pptr[0])) { /* * Notify the real server: there is no * existing entry if it is not RST * packet or not TCP packet. */ if (iph->protocol != IPPROTO_TCP //考虑到由内网(rs)通过本机转发到外网(client)的数据,不可能是不是tcp或者不是rst包,否则发出一个icmp出错报文,目的地址不可达.然后丢弃skb || !is_tcp_reset(skb)) { icmp_send(skb,ICMP_DEST_UNREACH, ICMP_PORT_UNREACH, 0); return NF_DROP; } } } IP_VS_DBG_PKT(12, pp, skb, 0, "packet continues traversal as normal"); return NF_ACCEPT; //pass掉从内网(realserver)发出的到外网的新连接(因为不与ip_vs_conn_tab中的连接相关联) } IP_VS_DBG_PKT(11, pp, skb, 0, "Outgoing packet"); //debug if (!skb_make_writable(skb, ihl)) //如果skb的头部不可写入,跳到drop处 goto drop; /* mangle the packet */ if (pp->snat_handler && !pp->snat_handler(skb, pp, cp)) //到这里的数据就是需要修改源地址的(rs -> client)从内网到外网的数据报了 goto drop; //如果定义了snat_handler,但是snat_handler()失败,跳到drop处 ip_hdr(skb)->saddr = cp->vaddr; //将源地址转化为虚拟服务器的地址,让这个到外网的数据报看上去就像是从vs发出的一样 ip_send_check(ip_hdr(skb)); //改动了源地址,就要重新计算校验和 /* For policy routing, packets originating from this * machine itself may be routed differently to packets * passing through. We want this packet to be routed as * if it came from this machine itself. So re-compute * the routing information. */ if (ip_route_me_harder(skb, RTN_LOCAL) != 0) //为了让skb看上去就像是本机发出的,还需要刷新路由信息 goto drop; IP_VS_DBG_PKT(10, pp, skb, 0, "After SNAT"); //debug ip_vs_out_stats(cp, skb); //更新cp,skb的计数器(连接数,通讯量) ip_vs_set_state(cp, IP_VS_DIR_OUTPUT, skb, pp); //更新cp,skb,pp的状态参数,标记等 ip_vs_conn_put(cp); //释放cp计数 skb->ipvs_property = 1; //打上标记,以免再被ipvs修改 LeaveFunction(11); //debug return NF_ACCEPT; //pass drop: ip_vs_conn_put(cp); //释放cp计数 kfree_skb(skb); //释放skb空间 return NF_STOLEN; //返回NF_STOLEN,避免netfilter再次修改 }
ip_vs_forward_icmp()
该函数和前面讲到的ip_vs_out()在同一个FORWARD规则链上,但是的优先级为99,比ip_vs_out()的100要小(高),所以优先执行.
函数非常简单,就是将经过FORWARD规则链的所有icmp数据报交给ip_vs_in_icmp()处理.为什么进入本机的数据会到FORWARD规则链上呢,原因在于local配置成透明设备时,tcp/udp协议是比较容易将forward的数据让它input的,而icmp则没有那么简单了,所以有一些发往本机的icmp报文会跑到forward规则链上来(具体原因不明),所以在这里把漏掉的进入vs的icmp交给ip_vs_forward_icmp()处理
net/ipv4/ipvs/ip_vs_core.c
/* * It is hooked at the NF_INET_FORWARD chain, in order to catch ICMP * related packets destined for 0.0.0.0/0. * When fwmark-based virtual service is used, such as transparent * cache cluster, TCP packets can be marked and routed to ip_vs_in, * but ICMP destined for 0.0.0.0/0 cannot not be easily marked and * sent to ip_vs_in_icmp. So, catch them at the NF_INET_FORWARD chain * and send them to ip_vs_in_icmp. */ static unsigned int ip_vs_forward_icmp(unsigned int hooknum, struct sk_buff *skb, const struct net_device *in, const struct net_device *out, int (*okfn)(struct sk_buff *)) { int r; if (ip_hdr(skb)->protocol != IPPROTO_ICMP) //如果不是icmp,直接pass return NF_ACCEPT; return ip_vs_in_icmp(skb, &r, hooknum); //如果是.处理之 }
ip_vs_post_routing()
此函数的优先级为NF_IP_PRI_NAT_SRC-1,比POSTROUTING上的nat,mangle的优先级都高,保证了早于他们执行,目的就是防止被ipvs修改过的数据报再次被netfilter修改.具体做法如下
net/ipv4/ipvs/ip_vs_core.c
/* * It is hooked before NF_IP_PRI_NAT_SRC at the NF_INET_POST_ROUTING * chain, and is used for VS/NAT. * It detects packets for VS/NAT connections and sends the packets * immediately. This can avoid that iptable_nat mangles the packets * for VS/NAT. */ static unsigned int ip_vs_post_routing(unsigned int hooknum, struct sk_buff *skb, const struct net_device *in, const struct net_device *out, int (*okfn)(struct sk_buff *)) { if (!skb->ipvs_property) //如果skb没有ipvs修改过的记号,则pass,让netfilter继续处理去 return NF_ACCEPT; /* The packet was sent from IPVS, exit this chain */ return NF_STOP; //否则,用NF_STOP返回,netfilter受到这个信号以后,直接退出该规则链,不再做任何处理 }