Nginx负载均衡算法-Hash

复制扩展无法解决数据增长问题,这样当业务增长到某一阶段,以致上游server无法将高频访问数据全部放于内存中时,性能便会一落千丈。Nginx通过ngx_http_upstream_ip_hash_module、ngx_http_upstream_hash_module这两个模块,实现了哈希与一致性哈希算法,它们可以限制每个上游server处理的数据量,以此提升上游server的性能。我们先来看ip_hash算法。
每个IP报文头部都含有源IP地址,它标识了唯一的客户端。因此,将IP地址依据字符串哈希函数转换为32位的整数,再对server总数取模,就可以将客户端与上游server的访问关系固定下来。
它的实现非常简单,包括以下3步:

将IP地址这个字符串转化为哈希值,代码如下所示:

ngx_uint_t    hash = 89;
for (i = 0; i < (ngx_uint_t) iphp->addrlen; i++) {
     hash = (hash * 113 + iphp->addr[i]) % 6271;
}

接着,基于所有上游server的权重和,缩小哈希值范围:

ngx_uint_t w = hash % iphp->rrp.peers->total_weight;

最后,通过遍历所有server的权重,选取上游server:

while (w >= peer->weight) {
    w -= peer->weight;
    peer = peer->next;
}

可见,相对于采用二分查找的Random算法,古老的ip_hash性能有所下降(时间复杂度从O(logN)降到O(N)),在server非常多时性能不够理想。

实际上,ip_hash还有一个很糟糕的问题,就是对于IPv4地址,它会忽略最后1个字节的值。比如若IP地址为AAA.BBB.CCC.DDD,那么ip_hash只会使用AAA.BBB.CCC来求取哈希值,相关代码如下所示:

case AF_INET:
    sin = (struct sockaddr_in *) r->connection->sockaddr;
    iphp->addr = (u_char *) &sin->sin_addr.s_addr;
    iphp->addrlen = 3;
    break;

对于后出现的IPv6地址,将会使用全部16个字节的地址。为了向前兼容,ip_hash模块很难修改掉IPv4地址的这个Bug。此时,你可以使用更新的hash指令来解决,它可以对Nginx变量(也接受变量与字符串的组合)取哈希值:

upstream {
    server A;
    server B;
    hash $remote_addr;
}

hash模块会使用CRC32函数,对变量求出32位的哈希值,之后执行与ip_hash相同的算法。
哈希算法最大的问题在于,当server数量、权重发生变化时,映射函数就会变化,很容易导致映射关系大幅变动。比如,当上游server数量为5时,关键字与上游server的映射如下所示:

一旦server4宕机,这5个关键字的映射关系就会全部变动:

如果上游server为数据建立了缓存,那么此时会导致这5个关键字对应的缓存全部失效。

一致性哈希算法是怎样实现的?
一致性哈希算法将哈希算法中的函数映射,改为32位数字构成的环映射,大幅降低了server变动时受影响的关键字数量。如下图所示,Nginx将关键字(hash指令后的变量)基于CRC32函数转换为无符号的32位整数,其中232与0相接构成了一个环:

这样,3个server将会基于weight权重,各自负责环中的一段弧线,划分了处理关键字的范围。当扩容或者宕机时,只会影响相邻server节点上的关键字。
当然,这样还是有2个问题:

  1. 当节点1宕机时,它负责的所有请求都被映射到了节点2,这样节点2可能会抗不住压力继续宕机,进而引发雪崩效应。扩容时也一样,当增加节点3时,只是分流了节点2上的请求,这对节点0、节点1完全没有帮助。
  2. 如果server只是基于权重划分哈希环,那么很难保证全部关键字均衡地落进每个server上。

解决这2个问题的方案,就是在哈希环上,增加一层二次映射的虚拟节点环。这样,二次哈希既可以让数据分布更均衡,也可以由全体server共同分担宕机压力,享受扩容带来的收益。
虚拟节点数量越多,分布会越均衡。然而,在有序的哈希环上选择server,最快的方法也只是二分查找法,它的时间复杂度是O(logN),因此,虚拟节点数还是得控制在一个范围内。通常,每个真实节点对应的虚拟节点数在100到200之间,而Nginx选择为每个权重分配160个虚拟节点。
下面我们看下Nginx是如何实现一致性哈希算法的。对于哈希环上的每个虚拟节点,Nginx都会分配1个ngx_http_upstream_chash_point_t结构体表示:

typedef struct {
    uint32_t                            hash;
    ngx_str_t                          *server;
} ngx_http_upstream_chash_point_t;

其中,hash是二次哈希映射后的值,server则指向真实的上游服务。这些虚拟节点环会以数组的形式放在ngx_http_upstream_chash_points_t结构体中:

typedef struct {
    ngx_uint_t                          number;
    ngx_http_upstream_chash_point_t     point[1];
} ngx_http_upstream_chash_points_t;

其中,number成员保存了虚拟节点的总数量,而point则是虚拟节点环的首地址。
构建虚拟节点环的初始化代码如下所示:

     ngx_uint_t    npoints = peer->weight * 160; //每个权重赋予160个虚拟节点
     for (ngx_uint_t j = 0; j < npoints; j++) {
         hash = base_hash; //基于上游server的IP、端口进行首次哈希
//基于相同的算法执行二次哈希
         ngx_crc32_update(&hash, prev_hash.byte, 4);
         ngx_crc32_final(hash);
//将二次哈希值放入ngx_http_upstream_chash_point_t环中
         points->point[points->number].hash = hash;
         points->point[points->number].server = server;
         points->number++;
         prev_hash.value = hash;
     }

当用户请求到达时,会首先基于Nginx变量生成CRC32哈希值,接着采用二分查找法,在O(logN)时间复杂度下找到对应的真实server:

ngx_http_upstream_chash_point_t  * point = &points->point[0]; //环上第1个虚拟节点
ngx_uint_t i = 0;
ngx_uint_t j = points->number;
while (i < j) {    //以二分法检索虚拟节点
    ngx_uint_t k = (i + j) / 2;   
    if (hash > point[k].hash) {
        i = k + 1;
    } else if (hash < point[k].hash) {
        j = k;
    } else {
        return k;    //返回哈希环中对应虚拟节点的下标
    }
}
return i;

如果选出的server由于失败次数、并发连接数达到限制后,将会采用闭散列函数的方案,尝试选择相邻哈希桶上的server。另外,如果连续选出的20个server都不能使用,一致性哈希算法将会回退到RoundRobin算法。

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇