IPVS基于应用层任意偏移字段HASH值的负载均衡算法

在比较早的那些年,我曾经写了一个负载均衡调度算法模块,是基于应用层协议包任意偏移量开始的一段固定长度的数据计算一个值,然后将这个值hash到不同的服务器。那时觉得没啥用,就没有再继续,直到前一段时间的一段思考以及前几天的一次预研。我决定作文以记之,以后说不定能用得着。

1.UDP服务的负载均衡

以前使用UDP的服务很少,虽然HTTP并没有说一定要是TCP,但事实上几乎没有UDP上的HTTP。但是随着网络可靠性的增加,网络集中控制机制与分布式优化技术的日益成熟,使用UDP的场合越来越多。
       使用UDP就意味着你必须在应用层做传输控制,其实这还不是主要的,主要的问题是现在没有什么熟知的UDP服务,比如你不能指望在负载均衡器上内置一个关于OpenVPN服务的负载均衡,但是对于基于TCP的HTTP服务几乎总是被内置于任何网关内部。因为作为一个著名的应用层协议,HTTP在各个层面都拥有自己的一套成熟的标准,大家均认可这些标准。使用UDP你必须实现一个符合常理的连接过期机制,由于在UDP层面根本就不可能识别一个"连接"的断开,这就意味着要么在应用层识别,比如发送一个特殊的UDP包表示要“断开”了,要么就是对一个UDP“连接”设置一个超时。
       虽然存在这么多的问题,但是在移动时代,有些问题还真必须使用UDP作为传输协议才能解决。

2.移动网络的问题

如果使用手机或PAD访问服务,由于这些移动终端时刻处于移动中,其IP地址也会不断变化(请不要考虑LISP,这只是个理想),如果使用TCP作为服务的承载协议,那就意味着TCP会不断地断开再重连-TCP和IP是相关的,如果使用UDP,就没有这个问题,代价只是在应用层记录连接信息。这是一个会话层缺失的问题,虽然有人不太认同,但是毕竟键盘党喷子说再多也没有用,实现一个这样的机制跑出来效果才是王道。鉴于此,我给OpenVPN做了手术。
       OpenVPN也是用5元组来识别一个特定客户端的,但是由于存在终端移动IP地址变化的问题,这会导致OpenVPN服务端频繁断开和客户端的连接然后等待重连,虽然这不是由于TCP导致的,但是却道出了一个问题的本质,只要是用5元组来识别连接,IP地址的变化都会导致连接断开。因此我在OpenVPN协议的头里面加了一个服务器内部唯一的4个字节的所谓sessionID用以补充缺失的会话层。以后OpenVPN服务端不再用5元组来识别到一个客户端的连接了,而是使用这个唯一的sessionID来识别,这样对于UDP的情况,即便是客户端的IP地址发生变化,服务端也不会断开连接,因为sessionID没有变化。注意,这个对于TCP模式的服务是没有用的,因为TCP处在传输层,在OpenVPN识别到sessionID以前,TCP本身就先断开了,除非在accept调用之上再封装一层,做到虽然TCP连接(TCP连接)在不断的断开/重连,但是OpenVPN连接(会话层连接)始终不会断。但是由于工作量比较大,作罢。
       在强大的功能展现的效果面前,任何的唧唧歪歪都是苍白的。通过引入一个很小的字段(4字节或者2字节),完美解决了“UDP长连接”(还真不能用TCP,除非引入LISP)时IP地址切换的问题,这就是UDP的力量。OpenVPN如此,为何别的就不行。事实上,任何的应用层协议都可以用UDP来封装,将连接控制(连接,排序,重传,断开等)等操纵进行标准化置于上层即可。然而,如果客户端的IP地址不断变化,负载均衡器还能基于源IP做负载均衡吗?
       很显然是可以的,但是却是有问题的。因为有可能在同一客户端变化了IP地址之后,负载均衡器会将其分发到不同的服务器上,然而实际上,它们的sessionID并没有变化,因为将不能再根据源IP地址做负载均衡了。那怎么办?答案就是基于sessionID做负载均衡。

3.基于UDP协议应用层的sessionID做负载均衡

一步一步地,我们就走到了这里,现在必须回答的问题是如何做。sessionID是什么?它并非标准协议的一部分。首先你必须保证数据包中一定要有这个字段,这个一般可以保证,我肯定知道我在配置什么东西,其次,问题是这个sessionID在什么地方?这决不能强行规定。事实上,所谓的sessionID就是在一次连接中,数据包中不会变化的那个部分,仅此。因此,最好的办法就是让配置者自己决定它在什么地方以及它的长度是多少。
       有了相对应用层开始的偏移和长度,取字段和算HASH就犹如探囊取物了,几乎和取源IP一样,只是多了几个计算而已,IPVS的代码如下:

net/netfilter/ipvs/ip_vs_offh.c:

/*
 * IPVS:        Layer7 payload Hashing scheduling module
 *
 * Authors:     ZHAOYA
 *              基于ip_vs_sh/dh修改而来,详细注释请参见:
 *              net/netfilter/ipvs/ip_vs_sh.c
 *              net/netfilter/ipvs/ip_vs_dh.c
 */

#include <linux/ip.h>
#include <linux/tcp.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/skbuff.h>
#include <linux/ctype.h>

#include <net/ip.h>
#include <net/ip_vs.h>

struct ip_vs_offh_bucket {
    struct ip_vs_dest       *dest;
};

struct ip_vs_offh_data {
    struct ip_vs_offh_bucket *tbl;
    u32 offset;
    u32 offlen;
};

#define IP_VS_OFFH_TAB_BITS               8
#define IP_VS_OFFH_TAB_SIZE               (1 << IP_VS_OFFH_TAB_BITS)
#define IP_VS_OFFH_TAB_MASK               (IP_VS_OFFH_TAB_SIZE - 1)

/*
 * 全局变量
 * offset:Layer7计算hash值的payload偏移量(相对于Layer7头)
 * offlen:Layer7计算hash值的payload长度
 */
static u32 offset, offlen;

static int skip_atoi(char **s)
{
    int i=0;

    while (isdigit(**s))
        i = i*10 + *((*s)++) - ‘0‘;
    return i;
}

static inline struct ip_vs_dest *
ip_vs_offh_get(struct ip_vs_offh_bucket *tbl, const char *payload, u32 length)
{
    __be32 v_fold = 0;
    /* 算法有待优化 */
    v_fold = (payload[0]^payload[length>>2]^payload[length])*2654435761UL;

    return (tbl[v_fold & IP_VS_OFFH_TAB_MASK]).dest;
}

static int
ip_vs_offh_assign(struct ip_vs_offh_bucket *tbl, struct ip_vs_service *svc)
{
    int i;
    struct ip_vs_offh_bucket *b;
    struct list_head *p;
    struct ip_vs_dest *dest;

    b = tbl;
    p = &svc->destinations;
    for (i=0; i<IP_VS_OFFH_TAB_SIZE; i++) {
        if (list_empty(p)) {
            b->dest = NULL;
        } else {
            if (p == &svc->destinations)
                p = p->next;

            dest = list_entry(p, struct ip_vs_dest, n_list);
            atomic_inc(&dest->refcnt);
            b->dest = dest;

            p = p->next;
        }
        b++;
    }
    return 0;
}

static void ip_vs_offh_flush(struct ip_vs_offh_bucket *tbl)
{
    int i;
    struct ip_vs_offh_bucket *b;

    b = tbl;
    for (i=0; i<IP_VS_OFFH_TAB_SIZE; i++) {
        if (b->dest) {
            atomic_dec(&b->dest->refcnt);
            b->dest = NULL;
        }
        b++;
    }
}

static int ip_vs_offh_init_svc(struct ip_vs_service *svc)
{
    struct ip_vs_offh_data *pdata;
    struct ip_vs_offh_bucket *tbl;

    pdata = kmalloc(sizeof(struct ip_vs_offh_data), GFP_ATOMIC);
    if (pdata == NULL) {
        pr_err("%s(): no memory\n", __func__);
        return -ENOMEM;
    }

    tbl = kmalloc(sizeof(struct ip_vs_offh_bucket)*IP_VS_OFFH_TAB_SIZE,
              GFP_ATOMIC);
    if (tbl == NULL) {
        kfree(pdata);
        pr_err("%s(): no memory\n", __func__);
        return -ENOMEM;
    }

    pdata->tbl = tbl;
    pdata->offset = 0;
    pdata->offlen = 0;

    svc->sched_data = pdata;

    ip_vs_offh_assign(tbl, svc);

    return 0;
}

static int ip_vs_offh_done_svc(struct ip_vs_service *svc)
{
    struct ip_vs_offh_data *pdata = svc->sched_data;
    struct ip_vs_offh_bucket *tbl = pdata->tbl;

    ip_vs_offh_flush(tbl);

    kfree(tbl);
    kfree(pdata);

    return 0;
}

static int ip_vs_offh_update_svc(struct ip_vs_service *svc)
{
    struct ip_vs_offh_bucket *tbl = svc->sched_data;

    ip_vs_offh_flush(tbl);
    ip_vs_offh_assign(tbl, svc);

    return 0;
}

static inline int is_overloaded(struct ip_vs_dest *dest)
{
    return dest->flags & IP_VS_DEST_F_OVERLOAD;
}

static struct ip_vs_dest *
ip_vs_offh_schedule(struct ip_vs_service *svc, const struct sk_buff *skb)
{
    struct ip_vs_dest *dest;
    struct ip_vs_offh_data *pdata;
    struct ip_vs_offh_bucket *tbl;
    struct iphdr *iph;
    void *transport_hdr;
    char *payload;
    u32 hdrlen = 0;
    u32 _offset = 0;
    u32 _offlen = 0;

    iph = ip_hdr(skb);
    hdrlen = iph->ihl*4;
    if (hdrlen > skb->len) {
        return NULL;
    }
    transport_hdr = (void *)iph + hdrlen;

    switch (iph->protocol) {
        case IPPROTO_TCP:
            hdrlen += (((struct tcphdr*)transport_hdr)->doff)*4;
            break;
        case IPPROTO_UDP:
            hdrlen += sizeof(struct udphdr);
            break;
        default:
            return NULL;
    }
#if 0
    {
        int i = 0;
        _offset = offset;
        _offlen = offlen;
        payload = (char *)iph + hdrlen + _offset;
        printk("begin:iplen:%d   \n", hdrlen);
        for (i = 0; i < _offlen; i++) {
            printk("%02X ", payload[i]);
        }
        printk("\nend\n");
        return NULL;

    }
#endif

    pdata = (struct ip_vs_offh_datai *)svc->sched_data;

    tbl = pdata->tbl;
    _offset = offset;//pdata->offset;
    _offlen = offlen;//pdata->offlen;
    if (_offlen + _offset > skb->len - hdrlen) {
        IP_VS_ERR_RL("OFFH: exceed\n");
        return NULL;
    }

    payload = (char *)iph + hdrlen + _offset;

    dest = ip_vs_offh_get(tbl, payload, _offlen);
    if (!dest
        || !(dest->flags & IP_VS_DEST_F_AVAILABLE)
        || atomic_read(&dest->weight) <= 0
        || is_overloaded(dest)) {
        IP_VS_ERR_RL("OFFH: no destination available\n");
        return NULL;
    }

    return dest;
}

static struct ip_vs_scheduler ip_vs_offh_scheduler =
{
    .name =         "offh",
    .refcnt =       ATOMIC_INIT(0),
    .module =       THIS_MODULE,
    .n_list  =      LIST_HEAD_INIT(ip_vs_offh_scheduler.n_list),
    .init_service =     ip_vs_offh_init_svc,
    .done_service =     ip_vs_offh_done_svc,
    .update_service =   ip_vs_offh_update_svc,
    .schedule =     ip_vs_offh_schedule,
};

static ssize_t ipvs_sch_offset_read(struct file *file, char __user *buf, size_t count, loff_t *ppos)
{
    int ret = 0;
    ret = sprintf(buf, "offset:%u;offlen:%u\n", offset, offlen);
    return ret;
}

/*
 * 设置offset/offset length
 * echo offset:$value1  offlen:$value2 >/proc/net/ipvs_sch_offset
 */
static int ipvs_sch_offset_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos)
{
    int ret = count;
    char *p = buf, *pstart;
    if ((p = strstr(p, "offset:")) == NULL) {
        ret = -EINVAL;
        goto out;
    }
    p += strlen("offset:");
    pstart = p;
    if ((p = strstr(p, " ")) == NULL) {
        ret = -EINVAL;
        goto out;
    }
    p[0] = 0;
    offset = skip_atoi(&pstart);
    if (offset == 0 && strcmp(pstart, "0")) {
        ret = -EINVAL;
        goto out;
    }
    p += strlen(";");
    if ((p = strstr(p, "offlen:")) == NULL) {
        ret = -EINVAL;
        goto out;
    }
    p  += strlen("offlen:");
    pstart = p;
    offlen = skip_atoi(&pstart);
    if (offlen == 0 && strcmp(pstart, "0")) {
        ret = -EINVAL;
        goto out;
    }
out:
    return ret;
}

/*
 * 由于不想修改用户态的配置接口,还是觉得procfs这种方式比较靠普
 **/
static const struct file_operations ipvs_sch_offset_file_ops = {
    .owner          = THIS_MODULE,
    .read           = ipvs_sch_offset_read,
    .write          = ipvs_sch_offset_write,
};

struct net *net = &init_net;
static int __init ip_vs_offh_init(void)
{
    int ret = -1;
    if (!proc_create("ipvs_sch_offset", 0644, net->proc_net, &ipvs_sch_offset_file_ops)) {
        printk("OFFH: create proc entry failed\n");
        goto out;
    }
    return register_ip_vs_scheduler(&ip_vs_offh_scheduler);
out:
    return ret;
}

static void __exit ip_vs_offh_cleanup(void)
{
    remove_proc_entry("ipvs_sch_offset", net->proc_net);
    unregister_ip_vs_scheduler(&ip_vs_offh_scheduler);
}

module_init(ip_vs_offh_init);
module_exit(ip_vs_offh_cleanup);
MODULE_LICENSE("GPL");

实际上,很多高大上的负载均衡实现都不是基于内核协议栈的,它们要么是直接用硬卡来做,要么是用户态协议栈,所以本文的原则也是可以用到那些方面的,只不过,我所能为力的并且简单的只有Linux IPVS,毕竟先把代码跑起来要比长篇大论好的多,起码我是这么认为的。

4.问题在哪里-连接缓存

我认为IPVS机制该改了,同时我觉得nf_conntrack也该改了。
我们知道,在IPVS中,可能只有一个流的第一个数据包才会去调用“特定协议”的conn_schedule回调,选出一个destination,即real server之后,这些信息就会被保存在“特定协议‘的conn缓存中。如果你看一下这个所谓的“特定协议”,就会发现它事实上是“第四层协议”,即传输层协议,TCP或者UDP,而在这一层,很显然,一个连接就是一个5元组。那么,即便我针对第一个数据包,即一个流的首包选择了一个real server,并将其存入了conn缓存,那么该流的客户端在IP地址变化了之后,显然conn缓存中找不到了,那么就会自动进入conn_schedule,由于使用固定偏移的paylaod进行schedule,那么肯定还是原来的那个real server被选择,此时会在conn缓存中增加一条新的条目用于以后的匹配,老的那条conn缓存没有用了,等待过期,只要客户端不改变IP地址且新的这个conn缓存项不过期,这个缓存将会一直命中,一旦客户端改变了IP地址,一切重新开始。可见,这是一个自动且正确的过程。但是,最好有一个针对旧五元组的删除通知机制,而不是等待它自己过期。
       如果等待它自己过期,那么试想一种超时时间很久的情况。客户端A五元组为tuple1使用sessionID1匹配到了一个real server1,设置了conn缓存conn1,过了一些时间,客户端A更换了IP地址,此时理所当然地,它不会再匹配到conn1,缓存不命中,依靠不变的sessionID1它在conn_schedule中选择了同样的real server1,设置了新的conn2缓存项,然后conn1就变成僵尸了,等待超时删除。过了很久,客户端2携带接管了客户端1的老的IP地址和UDP端口,访问了同样的UDP服务,此时客户端2的五元组为tuple1,携带sessionID2,由sessionID2计算得到的real server本应该是real server2,但是由于命中了僵尸conn1,它将被负载到real server1,此时客户端2更改了IP地址,它的五元组变成了tuple2‘,经过conn_schedule计算后,它匹配到了real server2,由于为它服务的初始real server为real server1,这将导致连接切换。这就是没有删除通知机制导致的问题。
       问题的解决似乎比较简单,办法有二,第一种办法就是为ip_vs_protocol结构体增加一个五元组变更通知的回调函数,比如叫做conn_in_update/conn_out_update,或者直接增加一个conn_in_delete/conn_out_delete,要么就是一种更加彻底的解决方案,即直接用sessionID来记录连接。而这后者正是我正准备为Linux的nf_conntrack所做的一个外科手术。
       当然,我不会走火入魔到彻底放弃五元组的连接跟踪方式,我只是为nf_conntrack增加了一种选择,正如conntrack增加了zone的支持一样。我相信,即便我不动手,过一个一年半载,肯定会有人这么做的,以往的事实预示了这一点。

时间: 2024-10-25 03:28:31

IPVS基于应用层任意偏移字段HASH值的负载均衡算法的相关文章

负载均衡算法(四)IP Hash负载均衡算法

/// <summary> /// IP Hash负载均衡算法 /// </summary> public static class IpHash { static Dictionary<string, int> dic = new Dictionary<string, int> { { "192.168.1.12", 1}, {"192.168.1.13", 1 }, { "192.168.1.14&quo

Centos 7基于DR(直接路由)模式的负载均衡配置详解

DR(直接路由)是三种负载均衡模式其中之一,也是使用最多的一种模式,关于该模式的介绍,可以参考博文:LVS负载均衡群集详解. DR的工作模式示意图如下: 该模式的原理已经在上面链接的博文中写了下来.现在直接搭建一个基于DR模式的负载均衡群集. 环境如下: 在上面这个环境中,需要解决的问题有下面几点: 1.所有web节点和调度器都配置上VIP:客户端访问VIP(群集的虚拟IP地址)时,若是 调度器将请求转发给web节点,然后由web节点直接去响应客户端,那么客户端在收到 数据包后,发现收到的数据包

基于amoeba实现mysql数据库的读写分离/负载均衡

一.Amoeba的简述:[来自百度百科] Amoeba是一个以MySQL为底层数据存储,并对应用提供MySQL协议接口的proxy.它集中地响应应用的请求,依据用户事先设置的规则,将SQL请求发送到特定的数据库上执行.基于此可以实现负载均衡.读写分离.高可用性等需求.与MySQL官方的MySQL Proxy相比,作者强调的是amoeba配置的方便(基于XML的配置文件,用SQLJEP语法书写规则,比基于lua脚本的MySQL Proxy简单). Amoeba相当于一个SQL请求的路由器,目的是为

基于 NAT(地址转换模式)构建 LVS 负载均衡

LVS 基本配置详解请参考博文:https://blog.51cto.com/14227204/2436891案例环境:实现结果如下: 使用NAT模式的群集技术,LVS负载调度器是所有节点访问Internet的网关服务器,其200.0.0.1也作为整个群集的VIP地址. 使用轮询(rr)的调度算法. web1和web2先搭建web服务,分别准备不同的网页文件,供客户端访问,以此来确定client访问LVS服务器的200.0.0.1,可以访问到两台wbe服务器. 待client测试成功后,web1

构建基于直接路由模式(DR)的负载均衡群集

关于LVS负载均衡工作原理及其概述请访问:Centos 7之LVS负载均衡群集 关于地址转换(NAT)模式的负载均衡群集详细配置请访问:构建基于地址转换(LVS-NAT)模式的负载均衡群集 一.开始配置DR模式LVS 准备工作:Centos 7操作系统四台:Centos01模拟Web1服务器:IP地址/192.168.100.10Centos02模拟Web2服务器:IP地址/192.168.100.20Centos03模拟NFS服务器: IP地址/192.168.100.30Centos05模拟

基于haproxy-1.5.12版本的http层负载均衡代理转发,附带测试效果

--前期环境部署: haproxy  192.168.64.129 nginx   192.168.64.129 client  192.168.64.128 (1.在server端口添加4个虚拟机 [haproxy和nginx共用一台服务器,不同端口] [[email protected] ~]# tree -n /usr/local/nginx/conf/conf.d/ /usr/local/nginx/conf/conf.d/ ├── bbs.example.com.8000.conf ├

基于DR(直接路由)模式的负载均衡配置详解

DR(直接路由)是三种负载均衡模式其中之一,也是使用最多的一种模式,关于该模式的介绍,可以参考博文:https://blog.51cto.com/14227204/2436891环境如下:1.所有web节点和调度器都配置上VIP:客户端访问VIP(群集的虚拟IP地址)时,若是 调度器将请求转发给web节点,然后由web节点直接去响应客户端,那么客户端在收到 数据包后,发现收到的数据包源地址不是200.0.0.254,那么就会直接丢弃web服务器返回 的数据包,为了解决这一问题,所以需要在所有we

LVS-DR实现基于LAMP的负载均衡

基于搭建LAMP环境,并实践基于DNS做基于域名的虚拟主机中的环境,重新搭建一个同样的环境要求: a)实现web服务文件更新的自动同步到另一台机器上 b)数据库实现主从复制 c)通过shell脚本实现网站源代码备份和mysql备份,备份策略包括全量备份.增量备份.差异备份 通过lvs实现负载均衡,要求使用DR模式,并能把其中的原理部分通过自己的理解汇总出来. lvs:Linux Virtual Server, Linux虚拟四层交换的服务器,是一个虚拟的服务器集群系统.LVS有LVS-NAT,L

为什么基于DNS的全局负载均衡(GSLB)不起作用?

Why DNS Based Global Server Load Balancing (GSLB) Doesn't Work 作者:Pete Tenereillo 3/11/04 Copyright Tenereillo, Inc. 2004 序言 弗雷德:乔,我要去赶一班飞机,从好莱坞到洛杉矶国际机场需要多长时间? 乔:恩...这取决于你走哪条路. 弗雷德:恩...我觉得我应该走高速公路,对吧? 乔:好吧,这是一个技术性问题,我能回答它.如果以60km/h的速度,走高速需要20分钟. 弗雷德: