ipvs负载均衡模块的内核实现

本文涉及的产品
公网NAT网关,每月750个小时 15CU
简介:
传输模式:
[直接路由方式]:直接查找路由表,以原始数据包的目的地址为查找键。本地配置的ip地址就是数据包的目的地址,数据既然已经到了本地为何还要查找,为何还要继续路由?这是因为本地的目的地到达情景仅仅是一个假象,真正提供服务的机器还在后面,也就是说服务被负载均衡了。此时问题是,既然本地配置了一个目的地ip地址,其它机器还能配置这个ip地址吗?那样的话岂不ip冲突了吗?
     在直接路由模式中,负载均衡器和“后面”真正提供服务的机器都配置有同一个ip地址,在负载均衡器中,该ip配置在一个物理的真实网卡上,用来接收客户端的数据包,很显然,这些数据包最终肯定走到了ip_local_deliver这个函数中,接下来数据包要通过NF_IP_LOCAL_IN这个hook,恰好ipvs等在这里,接着调用ip_vs_in这个hook函数,经过判断发现数据包需要进行负载均衡后,会调用已经建立的到真实机器的连接ip_vs_conn这个数据结构的packet_xmit回调函数,在该函数中会以ip_vs_conn中的信息来查找路由,ip_vs_conn中有三个字段很重要:caddr-客户端的ip;vaddr-虚拟ip,也就是在负载均衡器物理网卡上配置的ip,同时该ip将绑定在所有提供真实服务的所谓均衡机器的loopback网卡上;daddr-这是均衡机器的物理网卡的ip,简单点理解为负载均衡器直连。
     数据从查找到的daddr路由的出口设备发送了出去,最终数据到达了这个daddr,然后进入路由查找,看是local-in还是forward,在查找的时候会调用fib_lookup函数,它会查找各个路由表,或者它会遍历各个路由规则表--在配置了MULTIPLE_TABLES的情况下,最终它发现目的ip地址就是本机--绑定在loopback上的地址,也就是vaddr,然后数据被交由上层真正地被处理。之所以可以做到从一个网口进入的本地接收数据包的目的地址并没有配置在该网口上是因为路由查找的默认策略是不检查入口网卡的ip和目的地ip的关系的,这么检查也没有多大意义,因为大多数的过路包的目的ip和本机网卡ip本来就没有什么关系,而在检查路由的伊始还区分不出这是过路包还是本地接收包。不过想实现入口网口和路由的关系绑定也不是不能实现,办法就是配置一条策略路由或者在MULTIPLE_TABLES的情况下添加一个新的规则表,将fib_rule的r_ifindex字段进行硬绑定即可,这样的话fib_lookup中会对其进行检查:(r->r_ifindex && r->r_ifindex != flp->iif)
     为了将这些真实的处理机器彻底隐藏,需要隐藏它们的虚拟ip地址,由于所有的机器都配置了同样的vaddr,那么免费arp的发送会导致大量的ip冲突,因此需要做的就是将这些ip隐藏,仅仅开放给进入包的协议栈路由查找。所谓的隐藏就是不让本机协议栈以外的别人知道,由于任何的ip在以太网中都是通过arp来使别人知道自己存在的,你要ping一台局域网的机器,首先要arp一下,得到回复后方知目的地的mac地址,这样才能实际发送,所以只要能够抑制这个虚拟网址发送arp回应即可,它本身配置在loopback上,为了不使它响应外部的任何arp请求,只需要配置一个内核参数即可--arp_ignore,这个参数控制对ip没有配置在收到arp请求的网卡上的请求的回复策略,vaddr配置在loopback上,而arp请求肯定是由ethX进入的,因此这样可以不回应任何arp请求,同时它自己也不会广播免费arp,因此这个vaddr实际上是一个“无用”的ip,无用的意义在于它无法被寻址。
     真实的服务器监听哪个ip地址呢?它监听的就是vaddr,就是配置在loopback上的vaddr,这个地址除了负载均衡器可以连到,对于其它主机是不可见的,因为它无法响应arp请求(arp_ignoe),然而你还是可以用vaddr去连接真实服务器上的服务,前提是设置一条静态路由指向特定的真实服务器,实际上如果设置了静态路由,ping之也是可以通的,之所以设置arp_ignore是怕真实服务器回应arp请求和频发arp广播,很多情况下是没有必要设置它的。
[NAT方式]:这种方式配置起来比较简单,不需要配置虚拟ip以及arp内核参数之类的,但是性能较直接路由模式就有点不佳了,毕竟要做的事情多了。NAT模式很简单,就是将ip地址和端口信息修改成真实均衡机器们的ip和端口,这样真实的服务机器就被隐藏在负载均衡器后面了,思想和普通的nat是一样的。
[隧道模式]:隧道模式就是将数据重新打包成一个新的ip数据包,然后可以通过修改代码实现发送到虚拟网卡,由应用程序来做负载均衡,也可以直接发送到一个ipip隧道中去。
调度算法:
[轮转算法]:一个接着一个地提供服务...
[加权...]:主要是根据配置让内核了解到各个服务器的“能力”,不再平等的对待所有服务器,而是尽可能让处理能力强的服务器尽可能多的处理多的请求
[...算法]:(调度本质上和进程调度是一致的,略)
关键数据结构:
[struct ip_vs_app]:代表一个应用类型,也就是需要负载均衡的服务,其中port和protocol描述了其应用层信息,另外该结构体包含了大量回调函数,这些函数和具体的应用相关联。
[struct ip_vs_conn]:一个连接,这是一个负载均衡器和真实提供服务机器之间的连接,一个负载均衡器对于同一个ip_vs_app同时保持着多个连接,这就是负载均衡的意义。其中caddr代表需要服务的客户端的ip地址,vaddr为负载均衡器的ip地址,在直接路由模式中 它还是真实机器的绑定于loopback的虚拟ip地址,daddr为目的ip地址,也就是真实提供服务机器的可路由可被寻址的ip地址,app为该连接绑定的ip_vs_app,packet_xmit为发送回调函数,对于不同的模式其实现不同。
[struct ip_vs_service]:一种服务的描述,上述的ip_vs_conn就是该ip_vs_service的一个连接。其addr代表一个虚拟的ip地址,也就是对外公开的服务ip地址,而实际上服务并不由该ip提供,而需要路由到另外的ip上,protocol和port的含义和addr相同,只是它们是第四层的信息。
[struct ip_vs_protocol]:四层协议标示。conn_schedule为其调度回调函数。注意,负载均衡调度是基于四层协议的,而发送是基于连接的。
代码:
[在netfilter体系中注册几个钩子]:
static struct nf_hook_ops ip_vs_in_ops = {
    .hook        = ip_vs_in,
    .owner        = THIS_MODULE,
    .pf        = PF_INET,
    .hooknum        = NF_IP_LOCAL_IN,
    .priority       = 100,
};
NF_IP_LOCAL_IN表明数据的目的地是该虚拟服务器,ip_local_deliver中会调用该钩子点,ip_vs_in是其处理函数:
static unsigned int ip_vs_in(...)
{
    struct sk_buff    *skb = *pskb;
    struct iphdr    *iph;
    struct ip_vs_protocol *pp;
    struct ip_vs_conn *cp;
    ...
    pp = ip_vs_proto_get(iph->protocol);  //得到注册的可被负载均衡的四层协议
    if (unlikely(!pp)) //如果没有的话就按照常规方式被接收
        return NF_ACCEPT;
    ihl = iph->ihl << 2;
    cp = pp->conn_in_get(skb, pp, iph, ihl, 0); //检查该包是否属于一个已经建立的ip_vs_conn
    if (unlikely(!cp)) {
        int v;  //如果没有找到这个ip_vs_conn,则初始化一个新的
        if (!pp->conn_schedule(skb, pp, &v, &cp)) //调度,就是初始化一个cp
            return v;
    }
    ...
    restart = ip_vs_set_state(cp, IP_VS_DIR_INPUT, skb, pp);
    if (cp->packet_xmit)  //在新建的连接或者旧的连接上发送数据
        ret = cp->packet_xmit(skb, cp, pp);
    else {
        IP_VS_DBG_RL("warning: packet_xmit is null");
        ret = NF_ACCEPT;
    }
...
    return ret;
}
既然数据发了出去,回复包肯定是要回来的,ipvs的实现中有一个不对称性,就是说顺向的包会被导入本地后作抉择,可是真实服务器的回应包却直接被forward了,回应包并没有被导入本地,因为在顺向的包发送给真实服务期的时候并没有做snat操作,顶多在NAT模式下做一下dnat操作,因此回应包的目的ip和端口仍然是原始客户端的ip和端口,因此数据回到负载均衡器的时候,负载均衡器发现目的地址并不是自己,于是就forward了,这种没有snat的不对称实现对于效率是有提高的,但是要慎重自行设计此类框架,因为它有一个危险,那就是负载均衡器被绕过的情况,真实的服务器到达客户端并不一定非要经过负载均衡器,如果端口在ip_vs_in中被dnat改变了,那么数据由真实服务器不经负载均衡器回到客户端时就会出现混乱(ipvs中是不会出现这类情况的,因为直接路由模式是不更改ip和端口信息的),但是一般情况下通过配置可以避免这种情况。下面看反向包的处理:
static struct nf_hook_ops ip_vs_out_ops = {
    .hook        = ip_vs_out,
    .owner        = THIS_MODULE,
    .pf        = PF_INET,
    .hooknum        = NF_IP_FORWARD,
    .priority       = 100,
};
static unsigned int ip_vs_out(...)
{
    struct sk_buff  *skb = *pskb;
    struct iphdr    *iph;
    struct ip_vs_protocol *pp;
    struct ip_vs_conn *cp;
    int ihl;
    if (skb->nfcache & NFC_IPVS_PROPERTY)
        return NF_ACCEPT;
    ...
    pp = ip_vs_proto_get(iph->protocol);
    if (unlikely(!pp))  //正常包--非负载均衡反向包
        return NF_ACCEPT;
    cp = pp->conn_out_get(skb, pp, iph, ihl, 0);

    if (unlikely(!cp)) {
        ...//正常包--非负载均衡反向包
        return NF_ACCEPT;
    }
    ...//下面无论如何都要调用snat_handler,统一处理了直接路由模式和NAT模式,这是不对称设计的结果
    if (pp->snat_handler && !pp->snat_handler(pskb, pp, cp))
        goto drop;
    skb = *pskb;
    skb->nh.iph->saddr = cp->vaddr; //无论如何更新源ip,虽然对于直接路由模式这是没有必要的,这也是不对称设计的结果
...
}
对于tcp协议,tcp_conn_schedule是ip_vs_protocol的调度函数:
static int tcp_conn_schedule(struct sk_buff *skb,
          struct ip_vs_protocol *pp,
          int *verdict, struct ip_vs_conn **cpp)
{
    struct ip_vs_service *svc;
    struct tcphdr _tcph, *th;
    ... //找到tcp的一个ip_vs_service,根据是虚拟服务的ip-vaddr和port
    if (th->syn && (svc = ip_vs_service_get(skb->nfmark, skb->nh.iph->protocol,
                     skb->nh.iph->daddr, th->dest))) {
        ...
        *cpp = ip_vs_schedule(svc, skb);  //着手进行调度
        ...
    }
    return 1;
}
struct ip_vs_conn *ip_vs_schedule(struct ip_vs_service *svc, const struct sk_buff *skb)
{
    struct ip_vs_conn *cp = NULL;
    struct iphdr *iph = skb->nh.iph;
    struct ip_vs_dest *dest;
    __u16 _ports[2], *pptr;
    ...
    if (svc->flags & IP_VS_SVC_F_PERSISTENT)
        return ip_vs_sched_persist(svc, skb, pptr);
    if (!svc->fwmark && pptr[1] != svc->port) {
        ...
    }
    dest = svc->scheduler->schedule(svc, skb);  //根据调度算法选择一个可用的真实服务器
    ...
    cp = ip_vs_conn_new(iph->protocol,   //设置一个到真实服务器的连接
                iph->saddr, pptr[0], 
                iph->daddr, pptr[1],
                dest->addr, dest->port?dest->port:pptr[1],
                0,
                dest); //其中的ip_vs_bind_dest更新了被选择服务器的负载情况
    if (cp == NULL)
        return NULL;
    ip_vs_conn_stats(cp, svc);  //更新计数
    return cp;
}
对于直接路由模式,一个连接的packet_xmit是ip_vs_dr_xmit:
#define IP_VS_XMIT(skb, rt)                /
do {                            /
    (skb)->ipvs_property = 1;            /
    (skb)->ip_summed = CHECKSUM_NONE;        /
    NF_HOOK(PF_INET, NF_IP_LOCAL_OUT, (skb), NULL,    /
        (rt)->u.dst.dev, dst_output);        /
} while (0)
int ip_vs_dr_xmit(struct sk_buff *skb, struct ip_vs_conn *cp,
          struct ip_vs_protocol *pp)
{
    ...
    if (!(rt = __ip_vs_get_out_rt(cp, RT_TOS(iph->tos))))
        goto tx_error_icmp;
    ...
    dst_release(skb->dst);
    skb->dst = &rt->u.dst;  //设置新的出口路由
    ...
    IP_VS_XMIT(skb, rt);   //将数据发送出去
...
}
[调度算法过程]:无非就是按照特定策略选择一个真实的服务器,然后将数据扔过去而已。可以考虑修改代码实现真实服务器定期向负载均衡器汇报自己的负载情况,这样最好让用户态进程配合实现
[总的过程]:
1.数据包进入-
2.进入虚拟服务-
3.查找已有的到真实服务器的连接-
4.若查到,到5-
5.发送-结束
6.若查不到-
7.挑选一个真实服务器并初始化一个到该服务器的连接,到5-
8.等待反向数据,找到已有连接,处理-
5.1.nat模式--修改目的地址到真实服务器的地址,数据包变化
5.2.直接路由模式--直接以真实服务器的一个物理网卡的ip为键值查找路由,数据包不变化

[总结]:ipvs实现的是一个一对多的映射,这个机制用一对一的nat技术是无法实现的,但是ipvs也是“几乎”基于连接的负载均衡,但是可以通过设置超时时间为很短的值来变通地实现基于多个包的负载均衡,不管怎样,这仍然不是一个完全基于包的负载均衡方案。



 本文转自 dog250 51CTO博客,原文链接:http://blog.51cto.com/dog250/1271757

相关实践学习
部署高可用架构
本场景主要介绍如何使用云服务器ECS、负载均衡SLB、云数据库RDS和数据传输服务产品来部署多可用区高可用架构。
负载均衡入门与产品使用指南
负载均衡(Server Load Balancer)是对多台云服务器进行流量分发的负载均衡服务,可以通过流量分发扩展应用系统对外的服务能力,通过消除单点故障提升应用系统的可用性。 本课程主要介绍负载均衡的相关技术以及阿里云负载均衡产品的使用方法。
相关文章
|
消息中间件 存储 负载均衡
RabbitMQ使用docker搭建集群并使用Haproxy实现负载均衡(多机镜像模式)
RabbitMQ使用docker搭建集群并使用Haproxy实现负载均衡(多机镜像模式)
464 0
RabbitMQ使用docker搭建集群并使用Haproxy实现负载均衡(多机镜像模式)
|
2月前
|
存储 缓存 负载均衡
【Apache ShenYu源码】如何实现负载均衡模块设计
整个模块为ShenYu提供了什么功能。我们可以看下上文我们提到的工厂对象。/***/核心方法很清晰,我们传入Upsteam列表,通过这个模块的负载均衡算法,负载均衡地返回其中一个对象。这也就是这个模块提供的功能。
27 1
|
3月前
|
消息中间件 关系型数据库 MySQL
使用Nginx的stream模块实现MySQL反向代理与RabbitMQ负载均衡
使用Nginx的stream模块实现MySQL反向代理与RabbitMQ负载均衡
70 0
|
存储 负载均衡 NoSQL
docker swam 集群实现负载均衡
docker swam 集群实现负载均衡
|
负载均衡 网络协议 应用服务中间件
nginx实现负载均衡
nginx实现负载均衡
319 0
nginx实现负载均衡
|
负载均衡 Linux 调度
使用keepalived(HA)+LVS实现高可用负载均衡群集,调度器的双机热备
使用keepalived(HA)+LVS实现高可用负载均衡群集,调度器的双机热备
160 1
使用keepalived(HA)+LVS实现高可用负载均衡群集,调度器的双机热备
|
缓存 负载均衡 算法
Nginx实现负载均衡(整合SpringBoot小demo)
Nginx实现负载均衡(整合SpringBoot小demo)
303 4
Nginx实现负载均衡(整合SpringBoot小demo)
|
弹性计算 负载均衡 Kubernetes
【视频】第四讲-负载均衡ALB+实验三-使用ALB实现灰度发布|学习笔记
快速学习【视频】第四讲-负载均衡ALB+实验三-使用ALB实现灰度发布。
686 0
【视频】第四讲-负载均衡ALB+实验三-使用ALB实现灰度发布|学习笔记
|
域名解析 tengine 负载均衡
使用nginx的负载均衡机制实现用户无感更新服务
用户请求的转发是接口服务在部署时必须要做的一步。
|
负载均衡 Java 开发者
自定义实现负载均衡|学习笔记
快速学习自定义实现负载均衡
106 0
自定义实现负载均衡|学习笔记