码迷,mamicode.com
首页 > 其他好文 > 详细

LVS原理与实现 - 实现篇

时间:2021-06-28 17:42:21      阅读:0      评论:0      收藏:0      [点我收藏+]

标签:lse   ipvs   查询   协议栈   lazy   other   查找   none   配置   

LVS原理与实现 - 实现篇

在上一篇文章中,我们主要介绍了 LVS 的原理,接下来我们将会介绍 LVS 的代码实现。

本文使用的内核版本是:2.4.23,而 LVS 的代码在路径:/src/net/ipv4/ipvs 中。

Netfilter

在介绍 LVS 的实现前,我们需要了解以下 Netfilter 这个功能,因为 LVS 的实现使用了 Netfilter 的功能。

Netfilter:顾名思义就是网络过滤器(Network Filter),是 Linux 系统特有的网络子系统,用于过滤或修改进出内核协议栈的网络数据包。一般可以用来实现网络防火墙功能,其中 iptables 就是基于 Netfilter 实现的。

Linux 内核处理进出网络协议栈的数据包分为5个不同的阶段,Netfilter 通过这5个阶段注入钩子函数(Hooks Function)来实现对数据包的过滤和修改。如下图的蓝色方框所示:

技术图片

这5个阶段分为:

  • PER_ROUTING路由前阶段,发生在内核对数据包进行路由判决前。
  • LOCAL_IN本地上送阶段,发生在内核通过路由判决后。如果数据包是发送给本机的,那么就把数据包上送到上层协议栈。
  • FORWARD转发阶段,发生在内核通过路由判决后。如果数据包不是发送给本机的,那么就把数据包转发出去。
  • LOCAL_OUT本地发送阶段,发生在对发送数据包进行路由判决之前。
  • POST_ROUTING路由后阶段,发生在对发送数据包进行路由判决之后。

当向 Netfilter 的这5个阶段注册钩子函数后,内核会在处理数据包时,根据所在的不同阶段来调用这些钩子函数对数据包进行处理。向 Netfilter 注册钩子函数可以通过函数 nf_register_hook() 来进行,nf_register_hook() 函数的原型如下:

int nf_register_hook(struct nf_hook_ops *reg);

其中参数 reg 是类型为 struct nf_hook_ops 结构的指针,struct nf_hook_ops 结构的定义如下:

struct nf_hook_ops
{
    struct list_head list;
    nf_hookfn *hook;
    int pf;
    int hooknum;
    int priority;
};

struct nf_hook_ops 结构各个字段的作用如下:

  • list:用于连接同一阶段中所有相同的钩子函数列表。
  • hook:钩子函数指针。
  • pf:协议类型,因为 Netfilter 可以用于不同的协议,如 IPV4 和 IPV6 等。
  • hooknum:所处的阶段,也就是上面所说的5个不同的阶段。
  • priority:优先级,值越大优先级约小。

所以要使用 Netfilter 对网络数据包进行处理,只需要编写好处理数据包的钩子函数,然后通过调用 nf_register_hook() 函数向 Netfilter 注册即可。

另外,钩子函数 nf_hookfn 的原型如下:

typedef unsigned int nf_hookfn(unsigned int hooknum, struct sk_buff **skb, 
    const struct net_device *in, const struct net_device *out, int (*okfn)(struct sk_buff *));

其参数说明如下:

  • hooknum:所处的阶段,也就是上面所说的5个不同的阶段。
  • skb:要处理的数据包。
  • in:输入设备。
  • out:输出设备。
  • okfn:如果钩子函数执行成功,即调用这个函数完成对数据包的后续处理工作。

Netfilter 相关的知识点就介绍到这里,以后有机会会详解讲解 Netfilter 的原理和现实。

LVS 实现

前面我们主要简单介绍了 Netfilter 的使用,接下来我们将要分析 LVS 的代码实现。

1. 钩子函数注册

LVS 主要通过向 Netfilter 的3个阶段注册钩子函数来对数据包进行处理,如下图:

技术图片

  • LOCAL_IN 阶段注册了 ip_vs_in() 钩子函数。
  • FORWARD 阶段注册了 ip_vs_out() 钩子函数。
  • POST_ROUTING 阶段注册了 ip_vs_post_routing() 钩子函数。

我们在 LVS 的初始化函数 ip_vs_init() 可以找到这些钩子函数的注册代码,如下:

static struct nf_hook_ops ip_vs_in_ops = {
    { NULL, NULL },
    ip_vs_in, PF_INET, NF_IP_LOCAL_IN, 100
};

static struct nf_hook_ops ip_vs_out_ops = {
    { NULL, NULL },
    ip_vs_out, PF_INET, NF_IP_FORWARD, 100
};

static struct nf_hook_ops ip_vs_post_routing_ops = {
    { NULL, NULL },
    ip_vs_post_routing, PF_INET, NF_IP_POST_ROUTING, NF_IP_PRI_NAT_SRC-1
};

static int __init ip_vs_init(void)
{
    int ret;
    ...
    ret = nf_register_hook(&ip_vs_in_ops);
    ...
    ret = nf_register_hook(&ip_vs_out_ops);
    ...
    ret = nf_register_hook(&ip_vs_post_routing_ops);
    ...
    return ret;
}
  • LOCAL_IN 阶段:在路由判决之后,如果发现数据包是发送给本机的,那么就调用 ip_vs_in() 函数对数据包进行处理。

  • FORWARD 阶段:在路由判决之后,如果发现数据包不是发送给本机的,调用 ip_vs_out() 函数对数据包进行处理。

  • POST_ROUTING 阶段:在发送数据前,需要调用 ip_vs_post_routing() 函数对数据包进行处理。

2. LVS 角色介绍

在介绍这些钩子函数之前,我们先来了解一下 LVS 中的四个角色。如下:

  • ip_vs_service:服务配置对象,主要用于保存 LVS 的配置信息,如 支持的 传输层协议虚拟IP端口 等。

  • ip_vs_dest:真实服务器对象,主要用于保存真实服务器 (Real-Server) 的配置,如 真实IP端口权重 等。

  • ip_vs_scheduler:调度器对象,主要通过使用不同的调度算法来选择合适的真实服务器对象。

  • ip_vs_conn:连接对象,主要为了维护相同的客户端与真实服务器之间的连接关系。这是由于 TCP 协议是面向连接的,所以同一个的客户端每次选择真实服务器的时候必须保存一致,否则会出现连接中断的情况,而连接对象就是为了维护这种关系。

各个角色之间的关系如下图所示:

技术图片

从上图可以看出,ip_vs_service 对象的 destinations 字段用于保存 ip_vs_dest 对象的列表,而 scheduler 字段指向了一个 ip_vs_scheduler 对象。

ip_vs_scheduler 对象的 schedule 字段指向了一个调度算法函数,通过这个调度函数可以从 ip_vs_service 对象的 ip_vs_dest 对象列表中选择一个合适的真实服务器。

那么,ip_vs_service 对象和 ip_vs_dest 对象的信息怎么来的呢?答案是通过用户配置创建。例如可以通过下面的命令来创建 ip_vs_service 对象和 ip_vs_dest 对象:

node1 ]# ipvsadm -A -t node1:80 -s wrr
node1 ]# ipvsadm -a -t node1:80 -r node2 -m -w 3
node1 ]# ipvsadm -a -t node1:80 -r node3 -m -w 5

第一行用于创建一个 ip_vs_service 对象,而第二和第三行用于向 ip_vs_service 对象添加 ip_vs_dest 对象到 destinations 列表中。关于 LVS 的配置这里不作详细介绍,读者可以参考其他关于 LVS 配置的资料。

ip_vs_service 对象创建

我们来看看 LVS 源码是怎么创建一个 ip_vs_service 对象的,创建 ip_vs_service 对象通过 ip_vs_add_service() 函数完成,如下:

static int
ip_vs_add_service(struct ip_vs_rule_user *ur, struct ip_vs_service **svc_p)
{
    int ret = 0;
    struct ip_vs_scheduler *sched;
    struct ip_vs_service *svc = NULL;

    sched = ip_vs_scheduler_get(ur->sched_name); // 根据调度器名称获取调度策略对象
    ...
    // 申请一个 ip_vs_service 对象
    svc = (struct ip_vs_service *)kmalloc(sizeof(struct ip_vs_service), GFP_ATOMIC);
    ...
    memset(svc, 0, sizeof(struct ip_vs_service));
    // 设置 ip_vs_service 对象的各个字段
    svc->protocol = ur->protocol;       // 协议
    svc->addr = ur->vaddr;              // 虚拟IP
    svc->port = ur->vport;              // 虚拟端口
    svc->fwmark = ur->vfwmark;          // 防火墙标记
    svc->flags = ur->vs_flags;          // 标志位
    svc->timeout = ur->timeout * HZ;    // 超时时间
    svc->netmask = ur->netmask;         // 网络掩码

    INIT_LIST_HEAD(&svc->destinations);
    svc->sched_lock = RW_LOCK_UNLOCKED;
    svc->stats.lock = SPIN_LOCK_UNLOCKED;

    ret = ip_vs_bind_scheduler(svc, sched); // 绑定调度器
    ...
    ip_vs_svc_hash(svc); // 添加到ip_vs_service对象的hash表中
    ...
    *svc_p = svc;
    return 0;
}

先说明一下,参数 ur 是用户通过命令行配置的规则信息。上面的代码主要完成以下几个工作:

  • 通过调用 ip_vs_scheduler_get() 函数来获取一个 ip_vs_scheduler (调度器) 对象。

  • 然后申请一个 ip_vs_service 对象并且根据用户的配置设置其各个参数,并且把调度器对象绑定这个 ip_vs_service 对象。

  • 最后把 ip_vs_service 对象添加到 ip_vs_service 对象的全局哈希表中(这是由于可以创建多个 ip_vs_service 对象,这些对象通过一个全局哈希表来存储)。

ip_vs_dest 对象创建

创建 ip_vs_dest 对象通过 ip_vs_add_dest() 函数完成,代码如下:

static int ip_vs_add_dest(struct ip_vs_service *svc, struct ip_vs_rule_user *ur)
{
    struct ip_vs_dest *dest;
    __u32 daddr = ur->daddr; // 目的IP
    __u16 dport = ur->dport; // 目的端口
    int ret;
    ...
    // 调用 ip_vs_new_dest() 函数创建一个 ip_vs_dest 对象
    ret = ip_vs_new_dest(svc, ur, &dest);
    ...
    // 把 ip_vs_dest 对象添加到 ip_vs_service 对象的 destinations 列表中
    list_add(&dest->n_list, &svc->destinations); 
    svc->num_dests++;

    /* 调用调度器的 update_service() 方法更新 ip_vs_service 对象 */
    svc->scheduler->update_service(svc);
    ...
    return 0;
}

ip_vs_add_dest() 函数主要通过调用 ip_vs_new_dest() 创建一个 ip_vs_dest 对象,然后将其添加到 ip_vs_service 对象的 destinations 列表中。我们来看看 ip_vs_new_dest() 函数的实现:

static int
ip_vs_new_dest(struct ip_vs_service *svc,
               struct ip_vs_rule_user *ur,
               struct ip_vs_dest **destp)
{
    struct ip_vs_dest *dest;
    ...
    *destp = dest = (struct ip_vs_dest*)kmalloc(sizeof(struct ip_vs_dest), GFP_ATOMIC);
    ...
    memset(dest, 0, sizeof(struct ip_vs_dest));
    // 设置 ip_vs_dest 对象的各个字段
    dest->protocol = svc->protocol; // 协议
    dest->vaddr = svc->addr;        // 虚拟IP
    dest->vport = svc->port;        // 虚拟端口
    dest->vfwmark = svc->fwmark;    // 虚拟网络掩码
    dest->addr = ur->daddr;         // 真实IP
    dest->port = ur->dport;         // 真实端口

    atomic_set(&dest->activeconns, 0);
    atomic_set(&dest->inactconns, 0);
    atomic_set(&dest->refcnt, 0);

    INIT_LIST_HEAD(&dest->d_list);
    dest->dst_lock = SPIN_LOCK_UNLOCKED;
    dest->stats.lock = SPIN_LOCK_UNLOCKED;
    __ip_vs_update_dest(svc, dest, ur);
    ...
    return 0;
}

ip_vs_new_dest() 函数的实现也比较简单,首先通过调用 kmalloc() 函数申请一个 ip_vs_dest 对象,然后根据用户配置的规则信息来初始化 ip_vs_dest 对象的各个字段。

ip_vs_scheduler 对象

ip_vs_scheduler (调度器) 对象用于从 ip_vs_service 对象的 destinations 列表中选择一个合适的 ip_vs_dest 对象,其定义如下:

struct ip_vs_scheduler {
    struct list_head    n_list;     // 连接所有调度策略
    char                *name;      // 调度策略名称
    atomic_t            refcnt;     // 应用计数器
    struct module       *module;    // 模块对象(如果是通过模块引入的)

    int (*init_service)(struct ip_vs_service *svc);   // 用于初始化服务
    int (*done_service)(struct ip_vs_service *svc);   // 用于停止服务
    int (*update_service)(struct ip_vs_service *svc); // 用于更新服务

    // 用于获取一个真实服务器对象 (Real-Server)
    struct ip_vs_dest *(*schedule)(struct ip_vs_service *svc, struct iphdr *iph);
};

ip_vs_scheduler 对象的各个字段都在注释说明了,其中 schedule 字段是一个函数的指针,其指向一个调度函数,用于从 ip_vs_service 对象的 destinations 列表中选择一个合适的 ip_vs_dest 对象。

我们可以通过一个最简单的调度模块(轮询调度模块)来分析 ip_vs_scheduler 对象的工作原理(文件路径:/net/ipv4/ipvs/ip_vs_rr.c):

static struct ip_vs_scheduler ip_vs_rr_scheduler = {
    {0},                /* n_list */
    "rr",               /* name */
    ATOMIC_INIT(0),     /* refcnt */
    THIS_MODULE,        /* this module */
    ip_vs_rr_init_svc,  /* service initializer */
    ip_vs_rr_done_svc,  /* service done */
    ip_vs_rr_update_svc,/* service updater */
    ip_vs_rr_schedule,  /* select a server from the destination list */
};

首先轮询调度模块定义了一个 ip_vs_scheduler 对象,其中 schedule 字段设置为 ip_vs_rr_schedule() 函数。我们来看看 ip_vs_rr_schedule() 函数的实现:

static struct ip_vs_dest *
ip_vs_rr_schedule(struct ip_vs_service *svc, struct iphdr *iph)
{
    register struct list_head *p, *q;
    struct ip_vs_dest *dest;

    write_lock(&svc->sched_lock);
    p = (struct list_head *)svc->sched_data; // 最后一次被调度的位置
    p = p->next;
    q = p;
    // 遍历 destinations 列表
    do {
        if (q == &svc->destinations) {
            q = q->next;
            continue;
        }
        dest = list_entry(q, struct ip_vs_dest, n_list);
        // 找到一个权限值大于 0 的 ip_vs_dest 对象
        if (atomic_read(&dest->weight) > 0)
            goto out;
        q = q->next;
    } while (q != p);
    write_unlock(&svc->sched_lock);

    return NULL;

  out:
    svc->sched_data = q; // 设置最后一次被调度的位置
    ...
    return dest;
}

ip_vs_rr_schedule() 函数是轮询调度算法的实现,其实现原理如下:

  • ip_vs_service 对象的 sched_data 字段保存了最后一次调度的位置,所以每次调度时都是从这个字段读取到最后一次调度的位置。

  • 从最后一次调度的位置开始遍历,找到一个权限值(weight)大于 0 的 ip_vs_dest 对象。

  • 如果找到就把 ip_vs_service 对象的 sched_data 字段设置为最后被选择的 ip_vs_dest 对象的位置。

其原理可以通过以下图片说明:

技术图片

上图描述的原理还是比较简单,首先从 sched_data 处开始遍历,查找一个合适的 ip_vs_dest 对象,然后更新 sched_data 的位置。

另外,由于 LVS 可以存在多种不同的调度对象(提供不同的调度算法),所以 LVS 把这些调度对象通过一个链表(ip_vs_schedulers)存储起来,而这些调度对象可以通过调度对象的名字(name 字段)来查询。

可以通过调用 register_ip_vs_scheduler() 函数向 LVS 注册调度对象,而通过调用 ip_vs_scheduler_get() 函数来获取指定名字的调度对象,这两个函数的实现比较简单,这里就不作详细介绍了。

ip_vs_conn 对象

ip_vs_conn 对象用于维护 客户端真实服务器 之间的关系,为什么需要维护它们之间的关系?原因是 TCP协议 面向连接的协议,所以每次调度都必须选择相同的真实服务器,否则连接就会失效。

技术图片

如上图所示,刚开始时调度器选择了 Real-Server(1) 服务器进行处理客户端请求,但第二次调度时却选择了 Real-Server(2) 来处理客户端请求。

由于 TCP协议 需要客户端与服务器进行连接,但第二次请求的服务器发生了变化,所以连接状态就失效了,这就为什么 LVS 需要维持客户端与真实服务器连接关系的原因。

LVS 通过 ip_vs_conn 对象来维护客户端与真实服务器之间的连接关系,其定义如下:

struct ip_vs_conn {
    struct list_head    c_list;     /* 用于连接到哈希表 */

    __u32               caddr;      /* 客户端IP地址 */
    __u32               vaddr;      /* 虚拟IP地址 */
    __u32               daddr;      /* 真实服务器IP地址 */
    __u16               cport;      /* 客户端端口 */
    __u16               vport;      /* 虚拟端口 */
    __u16               dport;      /* 真实服务器端口 */
    __u16               protocol;   /* 协议类型(UPD/TCP) */
    ...
    /* 用于发送数据包的接口 */
    int (*packet_xmit)(struct sk_buff *skb, struct ip_vs_conn *cp);
    ...
};

ip_vs_conn 对象各个字段的作用都在注释中进行说明了,客户端与真实服务器的连接关系就是通过 协议类型客户端IP客户端端口虚拟IP虚拟端口 来进行关联的,也就是说根据这五元组能够确定一个 ip_vs_conn 对象。

另外,在《原理篇》我们说过,LVS 有3中运行模式:NAT模式DR模式TUN模式。而对于不同的运行模式,发送数据包的接口是不一样的,所以 ip_vs_conn 对象的 packet_xmit 字段会根据不同的运行模式来选择不同的发送数据包接口,绑定发送数据包接口是通过 ip_vs_bind_xmit() 函数完成,如下:

static inline void ip_vs_bind_xmit(struct ip_vs_conn *cp)
{
    switch (IP_VS_FWD_METHOD(cp)) {
    case IP_VS_CONN_F_MASQ:                     // NAT模式
        cp->packet_xmit = ip_vs_nat_xmit;
        break;
    case IP_VS_CONN_F_TUNNEL:                   // TUN模式
        cp->packet_xmit = ip_vs_tunnel_xmit;
        break;
    case IP_VS_CONN_F_DROUTE:                   // DR模式
        cp->packet_xmit = ip_vs_dr_xmit;
        break;
    ...
    }
}

一个客户端请求到达 LVS 后,Director服务器 首先会查找客户端是否已经与真实服务器建立了连接关系,如果已经建立了连接,那么直接使用这个连接关系。否则,通过调度器对象选择一台合适的真实服务器,然后创建客户端与真实服务器的连接关系,并且保存到全局哈希表 ip_vs_conn_tab 中。流程图如下:

技术图片

上面对 LVS 各个角色都进行了介绍,下面开始讲解 LVS 对数据包的转发过程。

3. 数据转发

因为 LVS 是一个负载均衡工具,所以其最重要的功能就是对数据的调度与转发, 而对数据的转发是在前面介绍的 Netfilter 钩子函数进行的。

对数据的转发主要是通过 ip_vs_in()ip_vs_out() 这两个钩子函数:

  • ip_vs_in() 运行在 NetfilterLOCAL_IN 阶段。

  • ip_vs_out() 运行在 NetfilterFORWARD 阶段。

FORWARD 阶段发送在数据包不是发送给本机的情况,但是一般来说数据包都是发送给本机的,所以对于 ip_vs_out() 这个函数的实现就不作介绍,我们主要重点分析 ip_vs_in() 这个函数。

ip_vs_in() 钩子函数

有了前面的知识点,我们对 ip_vs_in() 函数的分析就不那么困难了。下面我们分段对 ip_vs_in() 函数进行分析:

static unsigned int
ip_vs_in(unsigned int hooknum,
         struct sk_buff **skb_p,
         const struct net_device *in,
         const struct net_device *out,
         int (*okfn)(struct sk_buff *))
{
    struct sk_buff *skb = *skb_p;
    struct iphdr *iph = skb->nh.iph; // IP头部
    union ip_vs_tphdr h;
    struct ip_vs_conn *cp;
    struct ip_vs_service *svc;
    int ihl;
    int ret;
    ...
    // 因为LVS只支持TCP和UDP
    if (iph->protocol != IPPROTO_TCP && iph->protocol != IPPROTO_UDP)
        return NF_ACCEPT;

    ihl = iph->ihl << 2; // IP头部长度

    // IP头部是否正确
    if (ip_vs_header_check(skb, iph->protocol, ihl) == -1)
        return NF_DROP;

    iph = skb->nh.iph;         // IP头部指针
    h.raw = (char*)iph + ihl;  // TCP/UDP头部指针

上面的代码主要对数据包的 IP头部 进行正确性验证,并且将 iph 变量指向 IP头部,而 h 变量指向 TCP/UDP 头部。

    // 根据 "协议类型", "客户端IP", "客户端端口", "虚拟IP", "虚拟端口" 五元组获取连接对象
    cp = ip_vs_conn_in_get(iph->protocol, iph->saddr,
                           h.portp[0], iph->daddr, h.portp[1]);

    // 1. 如果连接还没建立
    // 2. 如果是TCP协议的话, 第一个包必须是syn包, 或者UDP协议。
    // 3. 根据协议、虚拟IP和虚拟端口查找服务对象
    if (!cp && 
        (h.th->syn || (iph->protocol != IPPROTO_TCP)) &&
        (svc = ip_vs_service_get(skb->nfmark, iph->protocol, iph->daddr, h.portp[1])))
    {
        ...
        // 通过调度器选择一个真实服务器
        // 并且创建一个新的连接对象, 建立真实服务器与客户端连接关系
        cp = ip_vs_schedule(svc, iph); 
        ...
    }

上面的代码主要完成以下几个功能:

  • 根据 协议类型客户端IP客户端端口虚拟IP虚拟端口 五元组,然后调用 ip_vs_conn_in_get() 函数获取连接对象。

  • 如果连接还没建立,那么就调用 ip_vs_schedule() 函数调度一台合适的真实服务器,然后创建一个连接对象,并且建立真实服务器与客户端之间的连接关系。

我们来分析一下 ip_vs_schedule() 函数的实现:

static struct ip_vs_conn *
ip_vs_schedule(struct ip_vs_service *svc, struct iphdr *iph)
{
    struct ip_vs_conn *cp = NULL;
    struct ip_vs_dest *dest;
    const __u16 *portp;
    ...
    portp = (__u16 *)&(((char *)iph)[iph->ihl*4]); // 指向TCP或者UDP头部
    ...
    dest = svc->scheduler->schedule(svc, iph); // 通过调度器选择一台合适的真实服务器
    ...
    cp = ip_vs_conn_new(iph->protocol,                      // 协议类型
                        iph->saddr,                         // 客户端IP
                        portp[0],                           // 客户端端口
                        iph->daddr,                         // 虚拟IP
                        portp[1],                           // 虚拟端口
                        dest->addr,                         // 真实服务器的IP
                        dest->port ? dest->port : portp[1], // 真实服务器的端口
                        0,                                  // flags
                        dest);
    ...
    return cp;
}

ip_vs_schedule() 函数的主要工作如下:

  • 首先通过调用调度器(ip_vs_scheduler 对象)的 schedule() 方法从 ip_vs_service 对象的 destinations 链表中选择一台真实服务器(ip_vs_dest 对象)

  • 然后调用 ip_vs_conn_new() 函数创建一个新的 ip_vs_conn 对象。

ip_vs_conn_new() 主要用于创建 ip_vs_conn 对象,并且根据 LVS 的运行模式为其选择正确的数据发送接口,其实现如下:

struct ip_vs_conn *
ip_vs_conn_new(int proto,                   // 协议类型
               __u32 caddr, __u16 cport,    // 客户端IP和端口
               __u32 vaddr, __u16 vport,    // 虚拟IP和端口
               __u32 daddr, __u16 dport,    // 真实服务器IP和端口
               unsigned flags, struct ip_vs_dest *dest)
{
    struct ip_vs_conn *cp;

    // 创建一个 ip_vs_conn 对象
    cp = kmem_cache_alloc(ip_vs_conn_cachep, GFP_ATOMIC); 
    ...
    // 设置 ip_vs_conn 对象的各个字段
    cp->protocol = proto;
    cp->caddr = caddr;
    cp->cport = cport;
    cp->vaddr = vaddr;
    cp->vport = vport;
    cp->daddr = daddr;
    cp->dport = dport;
    cp->flags = flags;
    ...
    ip_vs_bind_dest(cp, dest); // 将 ip_vs_conn 与真实服务器对象进行绑定
    ...
    ip_vs_bind_xmit(cp); // 绑定一个发送数据的接口
    ...
    ip_vs_conn_hash(cp); // 把 ip_vs_conn 对象添加到连接信息表中

    return cp;
}

ip_vs_conn_new() 函数的主要工作如下:

  • 创建一个新的 ip_vs_conn 对象,并且设置其各个字段的值。

  • 调用 ip_vs_bind_dest() 函数将 ip_vs_conn 对象与真实服务器对象(ip_vs_dest 对象)进行绑定。

  • 根据 LVS 的运行模式,调用 ip_vs_bind_xmit() 函数为连接对象选择一个正确的数据发送接口,ip_vs_bind_xmit() 函数在前面已经介绍过。

  • 调用 ip_vs_conn_hash() 函数把新创建的 ip_vs_conn 对象添加到全局连接信息哈希表中。

我们接着分析 ip_vs_in() 函数:

    if (cp->packet_xmit)
        ret = cp->packet_xmit(skb, cp); // 把数据包转发出去
    else {
        ret = NF_ACCEPT;
    }
    ...
    return ret;
}

ip_vs_in() 函数的最后部分就是通过调用数据发送接口把数据包转发出去,对于 NAT模式 来说,数据发送接口就是 ip_vs_nat_xmit()

数据发送接口:ip_vs_nat_xmit()

接下来,我们对 NAT模式 的数据发送接口 ip_vs_nat_xmit() 进行分析。由于 ip_vs_nat_xmit() 函数的实现比较复杂,所以我们通过分段来分析:

static int ip_vs_nat_xmit(struct sk_buff *skb, struct ip_vs_conn *cp)
{
    struct rtable *rt;      /* Route to the other host */
    struct iphdr  *iph;
    union ip_vs_tphdr h;
    int ihl;
    unsigned short size;
    int mtu;
    ...
    iph = skb->nh.iph;                // IP头部
    ihl = iph->ihl << 2;              // IP头部长度
    h.raw = (char*) iph + ihl;        // 传输层头部(TCP/UDP)
    size = ntohs(iph->tot_len) - ihl; // 数据长度
    ...
    // 找到真实服务器IP的路由信息
    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_get_out_rt() 函数查找真实服务器 IP 对应的路由信息对象。

  • 把数据包的旧路由信息替换成新的路由信息。

我们接着分析:

    iph->daddr = cp->daddr; // 修改目标IP地址为真实服务器IP地址
    h.portp[1] = cp->dport; // 修改目标端口为真实服务器端口
    ...
    // 更新UDP/TCP头部的校验和
    if (!cp->app && (iph->protocol != IPPROTO_UDP || h.uh->check != 0)) {
        ip_vs_fast_check_update(&h, cp->vaddr, cp->daddr, cp->vport,
                                cp->dport, iph->protocol);

        if (skb->ip_summed == CHECKSUM_HW)
            skb->ip_summed = CHECKSUM_NONE;

    } else {
        switch (iph->protocol) {
        case IPPROTO_TCP:
            h.th->check = 0;
            h.th->check = csum_tcpudp_magic(iph->saddr, iph->daddr,
                                            size, iph->protocol,
                                            csum_partial(h.raw, size, 0));
            break;

        case IPPROTO_UDP:
            h.uh->check = 0;
            h.uh->check = csum_tcpudp_magic(iph->saddr, iph->daddr,
                                            size, iph->protocol,
                                            csum_partial(h.raw, size, 0));
            if (h.uh->check == 0)
                h.uh->check = 0xFFFF;
            break;
        }

        skb->ip_summed = CHECKSUM_UNNECESSARY;
    }

上面的代码完成两个工作:

  • 修改目标IP地址和端口为真实服务器IP地址和端口。

  • 更新 UDP/TCP 头部 的校验和(checksum)。

我们接着分析:

    ip_send_check(iph); // 计算IP头部的校验和
    ...
    skb->nfcache |= NFC_IPVS_PROPERTY;

    ip_send(skb); // 把包发送出去
    ...
    return NF_STOLEN; // 让其他 Netfilter 的钩子函数放弃处理该包
}

上面的代码完成两个工作:

  • 调用 ip_send_check() 函数重新计算数据包的 IP头部 校验和。

  • 调用 ip_send() 函数把数据包发送出去。

这样,数据包的目标IP地址和端口被替换成真实服务器的IP地址和端口,然后被发送到真实服务器处。至此,NAT模式 的分析已经完毕。下面我们来总结一下整个流程:

  • 当数据包进入到 Director服务器 后,会被 LOCAL_IN阶段ip_vs_in() 钩子函数进行处理。

  • ip_vs_in() 函数首先查找客户端与真实服务器的连接是否存在,如果存在就使用这个真实服务器。否则通过调度算法对象选择一台最合适的真实服务器,然后建立客户端与真实服务器的连接关系。

  • 根据运行模式来选择发送数据的接口(如 NAT模式 对应的是 ip_vs_nat_xmit() 函数),然后把数据转发出去。

  • 转发数据时,首先会根据真实服务器的IP地址更新数据包的路由信息,然后再更新各个协议头部的信息(如IP地址、端口和校验和等),然后把数据发送出去。

总结

本文主要分析 LVS 的实现原理,但由于本人能力有限,并且很多细节没有分析,所以有问题可以通过评论指出。另外,本文只介绍了 NAT模式 的原理,还有 DR模式TUN模式 的没有分析,有兴趣可以自行阅读源码。

LVS原理与实现 - 实现篇

标签:lse   ipvs   查询   协议栈   lazy   other   查找   none   配置   

原文地址:https://www.cnblogs.com/still-smile/p/14932119.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!