一致性哈希

来源:互联网 发布:河南广电网络客服电话 编辑:程序博客网 时间:2024/06/03 02:27
一致性哈希算法在1997年由麻省理工学院提出的一种分布式哈希(DHT)实现算法,设计目标是为了解决因特网中的热点(Hot spot)问题,初衷和CARP十分类似。一致性哈希修正了CARP使用的简 单哈希算法带来的问题,使得分布式哈希(DHT)可以在P2P环境中真正得到应用。 
    一致性hash算法提出了在动态变化的Cache环境中,判定哈希算法好坏的四个定义:
1、平衡性(Balance):平衡性是指哈希的结果能够尽可能分布到所有的缓冲中去,这样可以使得所有的缓冲空间都得到利用。很多哈希算法都能够满足这一条件。
2、单调性(Monotonicity):单调性是指如果已经有一些内容通过哈希分派到了相应的缓冲中,又有新的缓冲加入到系统中。哈希的结果应能够保证原有已分配的内容可以被映射到原有的或者新的缓冲中去,而不会被映射到旧的缓冲集合中的其他缓冲区。 
3、分散性(Spread):在分布式环境中,终端有可能看不到所有的缓冲,而是只能看到其中的一部分。当终端希望通过哈希过程将内容映射到缓冲上时,由于不同终端所见的缓冲范围有可能不同,从而导致哈希的结果不一致,最终的结果是相同的内容被不同的终端映射到不同的缓冲区中。这种情况显然是应该避免的,因为它导致相同内容被存储到不同缓冲中去,降低了系统存储的效率。分散性的定义就是上述情况发生的严重程度。好的哈希算法应能够尽量避免不一致的情况发生,也就是尽量降低分散性。 
4、负载(Load):负载问题实际上是从另一个角度看待分散性问题。既然不同的终端可能将相同的内容映射到不同的缓冲区中,那么对于一个特定的缓冲区而言,也可能被不同的用户映射为不同 的内容。与分散性一样,这种情况也是应当避免的,因此好的哈希算法应能够尽量降低缓冲的负荷。

    在分布式集群中,对机器的添加删除,或者机器故障后自动脱离集群这些操作是分布式集群管理最基本的功能。如果采用常用的hash(object)%N算法,那么在有机器添加或者删除后,很多原有的数据就无法找到了,这样严重的违反了单调性原则。接下来主要讲解一下一致性哈希算法是如何设计的:

环形Hash空间
按照常用的hash算法来将对应的key哈希到一个具有2^32次方个桶的空间中,即0~(2^32)-1的数字空间中。现在我们可以将这些数字头尾相连,想象成一个闭合的环形。如下图
                                                                         
把数据通过一定的hash算法处理后映射到环上
现在我们将object1、object2、object3、object4四个对象通过特定的Hash函数计算出对应的key值,然后散列到Hash环上。如下图:
    Hash(object1) = key1;
    Hash(object2) = key2;
    Hash(object3) = key3;
    Hash(object4) = key4;
                                                           
将机器通过hash算法映射到环上
在采用一致性哈希算法的分布式集群中将新的机器加入,其原理是通过使用与对象存储一样的Hash算法将机器也映射到环中(一般情况下对机器的hash计算是采用机器的IP或者机器唯一的别名作为输入值),然后以顺时针的方向计算,将所有对象存储到离自己最近的机器中。
假设现在有NODE1,NODE2,NODE3三台机器,通过Hash算法得到对应的KEY值,映射到环中,其示意图如下:
Hash(NODE1) = KEY1;
Hash(NODE2) = KEY2;
Hash(NODE3) = KEY3;
                                                             
通过上图可以看出对象与机器处于同一哈希空间中,这样按顺时针转动object1存储到了NODE1中,object3存储到了NODE2中,object2、object4存储到了NODE3中。在这样的部署环境中,hash环是不会变更的,因此,通过算出对象的hash值就能快速的定位到对应的机器中,这样就能找到对象真正的存储位置了。

机器的删除与添加
普通hash求余算法最为不妥的地方就是在有机器的添加或者删除之后会照成大量的对象存储位置失效,这样就大大的不满足单调性了。下面来分析一下一致性哈希算法是如何处理的。
1. 节点(机器)的删除
    以上面的分布为例,如果NODE2出现故障被删除了,那么按照顺时针迁移的方法,object3将会被迁移到NODE3中,这样仅仅是object3的映射位置发生了变化,其它的对象没有任何的改动。如下图:
                                                              
2. 节点(机器)的添加 
    如果往集群中添加一个新的节点NODE4,通过对应的哈希算法得到KEY4,并映射到环中,如下图:
                                                              
    通过按顺时针迁移的规则,那么object2被迁移到了NODE4中,其它对象还保持这原有的存储位置。通过对节点的添加和删除的分析,一致性哈希算法在保持了单调性的同时,还是数据的迁移达到了最小,这样的算法对分布式集群来说是非常合适的,避免了大量数据迁移,减小了服务器的的压力。

平衡性
根据上面的图解分析,一致性哈希算法满足了单调性和负载均衡的特性以及一般hash算法的分散性,但这还并不能当做其被广泛应用的原由,因为还缺少了平衡性。下面将分析一致性哈希算法是如何满足平衡性的。hash算法是不保证平衡的,如上面只部署了NODE1和NODE3的情况(NODE2被删除的图),object1存储到了NODE1中,而object2、object3、object4都存储到了NODE3中,这样就照成了非常不平衡的状态。在一致性哈希算法中,为了尽可能的满足平衡性,其引入了虚拟节点。
    ——“虚拟节点”( virtual node )是实际节点(机器)在 hash 空间的复制品( replica ),一实际个节点(机器)对应了若干个“虚拟节点”,这个对应个数也成为“复制个数”,“虚拟节点”在 hash 空间中以hash值排列。
以上面只部署了NODE1和NODE3的情况(NODE2被删除的图)为例,之前的对象在机器上的分布很不均衡,现在我们以2个副本(复制个数)为例,这样整个hash环中就存在了4个虚拟节点,最后对象映射的关系图如下:
                                                                 
根据上图可知对象的映射关系:object1->NODE1-1,object2->NODE1-2,object3->NODE3-2,object4->NODE3-1。通过虚拟节点的引入,对象的分布就比较均衡了。那么在实际操作中,正真的对象查询是如何工作的呢?对象从hash到虚拟节点到实际节点的转换如下图:
                                         
“虚拟节点”的hash计算可以采用对应节点的IP地址加数字后缀的方式。例如假设NODE1的IP地址为192.168.1.100。引入“虚拟节点”前,计算 cache A 的 hash 值:
Hash(“192.168.1.100”);
引入“虚拟节点”后,计算“虚拟节”点NODE1-1和NODE1-2的hash值:
Hash(“192.168.1.100#1”); // NODE1-1
Hash(“192.168.1.100#2”); // NODE1-2



算法介绍

 

当后端是缓存服务器时,经常使用一致性哈希算法来进行负载均衡。

使用一致性哈希的好处在于,增减集群的缓存服务器时,只有少量的缓存会失效,回源量较小。

在nginx+ats / haproxy+squid等CDN架构中,nginx/haproxy所使用的负载均衡算法便是一致性哈希。

 

我们举个例子来说明一致性哈希的好处。

假设后端集群包含三台缓存服务器,A、B、C。

请求r1、r2落在A上。

请求r3、r4落在B上。

请求r5、r6落在C上。

使用一致性哈希时,当缓存服务器B宕机时,r1/r2会仍然落在A上,r5/r6会仍然落在C上,

也就是说这两台服务器上的缓存都不会失效。r3/r4会被重新分配给A或者C,并产生回源。

使用其它算法,当缓存服务器B宕机时,r1/r2不再落在A上,r5/r6不再落在C上了。

也就是说A、B、C上的缓存都失效了,所有的请求都要回源。

 

这里不介绍一致性哈希算法的基本原理,如果不了解,先花个10分钟看下这篇文章:

http://www.codeproject.com/Articles/56138/Consistent-hashing

 

在分析模块代码之前,先来看下nginx所实现的一致性哈希算法。

 

1. 初始化upstream块

主要工作是创建和初始化真实节点、创建和初始化虚拟节点。

其中真实节点是使用round robin的方法创建的。

 

Q:总共有多少个虚拟节点,一个真实节点对应多少个虚拟节点?

累加真实节点的权重,算出总的权重值total_weight,虚拟节点的个数一般为total_weight * 160。

一个权重为weight的真实节点,对应的虚拟节点数为weight * 160。

 

Q:对于每一个真实节点,是如何创建其对应的虚拟节点的?

1. 真实节点的server成员是其server指令的第一个参数,首先把它解析为HOST和PORT。

    base_hash = crc32(HOST 0 PORT)

    一个真实节点对应weight * 160个虚拟节点,对于每个虚拟节点来说,base_hash都是一样的。

2. 为了使每个虚拟节点的hash值都不同,又引入了PREV_HASH,它是上一个虚拟节点的hash值。

    hash = crc32(base_hash PREV_HASH)

3. 虚拟节点的server成员,指向真实节点的server成员。如此一来,通过比较虚拟节点和真实节点的

   server成员是否相同,可以判断它们是否是相对应的。

 

创建和初始化好虚拟节点数组后,对其中的虚拟节点按照hash值进行排序,对于hash值相同的虚拟节点,只保留第一个。

经过上述步骤,我们得到一个所有虚拟节点组成的数组,其元素的hash值有序而不重复。也就是说,ring建立起来了。

 

2. 初始话请求的负载均衡数据

根据hash指令第一个参数的实时值KEY,KEY一般是$host$uri之类的,计算出本次请求的哈希值。

hash = crc32(KEY)

根据请求的哈希值,在虚拟节点数组中,找到“顺时针方向”最近的一个虚拟节点,其索引为i。

什么叫顺时针方向最近?就是point[i - 1].hash < hash <= point[i].hash。

本次请求就落在该虚拟节点上了,之后交由其对应的真实节点来处理。

 

3. 选取真实节点

在peer.init中,已经知道请求落在哪个虚拟节点上了。

在peer.get中,需要查找虚拟节点对应的真实节点。

根据虚拟节点的server成员,在真实节点数组中查找server成员相同的、可用的真实节点。

如果找不到,那么沿着顺时针方向,继续查找下一个虚拟节点对应的真实节点。

如果找到了一个,那么就是它了。

如果找到了多个,使用轮询的方法从中选取一个。

 

4. 缺陷和改进

一个虚拟节点和一个真实节点,是依据它们的server成员来关联的。

这会出现一种情况,一个虚拟节点对应了多个真实节点,因为:

如果server指令的第一个参数为域名,可能解析为多个真实节点,那么这些真实节点的server成员都是一样的。

对于一个请求,计算其KEY的hash值,顺时针找到最近的虚拟节点后,发现该虚拟节点对应了多个真实节点。

使用哪个真实节点呢?本模块就使用轮询的方法,来从多个真实节点中选一个。

但我们知道使用一致性哈希的场景中,真实节点一般是缓存服务器。

一个虚拟节点对应多个真实节点,会导致一个文件被缓存在多个缓存服务器上。

这会增加磁盘的使用量,以及回源量,显然不是我们希望看到的。

 

解决这个问题的方法其实很简单,就是虚拟节点和真实节点通过name成员来建立关联。

因为就算对应同一条server配置,server的第一个参数为域名,各个真实节点的name成员也是唯一的。

这样一来,找到了一个虚拟节点,就能找到一个唯一的真实节点,不会有上述问题了。

 

数据结构

 

1. 真实节点

就是采用round robin算法所创建的后端服务器,类型为ngx_http_upstream_rr_peer_t。

需要注意的是,如果server指令的第一个参数是IP和端口,那么一条server指令只对应一个真实节点。

如果server指令的第一个参数是域名,一条server指令可能对应多个真实节点。

它们的server成员是相同的,可以通过name成员区分。

[java] view plain copy
  1. struct ngx_http_upstream_rr_peer_s {  
  2.     struct sockaddr *sockaddr; /* 后端服务器的地址 */  
  3.     socklen_t socklen; /* 地址的长度*/  
  4.     ngx_str_t name; /* 后端服务器地址的字符串,server.addrs[i].name */  
  5.     ngx_str_t server; /* server的名称,server.name */  
  6.        
  7.     ngx_int_t current_weight; /* 当前的权重,动态调整,初始值为0 */  
  8.     ngx_int_t effective_weight; /* 有效的权重,会因为失败而降低 */  
  9.     ngx_int_t weight; /* 配置项指定的权重,固定值 */  
  10.   
  11.     ngx_uint_t conns; /* 当前连接数 */  
  12.   
  13.     ngx_uint_t fails; /* "一段时间内",已经失败的次数 */  
  14.     time_t accessed; /* 最近一次失败的时间点 */  
  15.     time_t checked; /* 用于检查是否超过了"一段时间" */  
  16.   
  17.     ngx_uint_t max_fails; /* "一段时间内",最大的失败次数,固定值 */  
  18.     time_t fail_timeout; /* "一段时间"的值,固定值 */  
  19.     ngx_uint_t down; /* 服务器永久不可用的标志 */  
  20.     ...  
  21.     ngx_http_upstream_rr_peer_t *next; /* 指向下一个后端,用于构成链表 */  
  22.     ...  
  23. } ngx_http_upstream_rr_peer_t;  

ngx_http_upstream_rr_peers_t表示一组后端服务器,比如一个后端集群。

[java] view plain copy
  1. struct ngx_http_upstream_rr_peers_s {  
  2.     ngx_uint_t number; /* 后端服务器的数量 */  
  3.     ...  
  4.     ngx_uint_t total_weight; /* 所有后端服务器权重的累加值 */  
  5.   
  6.     unsigned single:1/* 是否只有一台后端服务器 */  
  7.     unsigned weighted:1/* 是否使用权重 */  
  8.   
  9.     ngx_str_t *name; /* upstream配置块的名称 */  
  10.   
  11.     ngx_http_upstream_rr_peers_t *next; /* backup服务器集群 */  
  12.     ngx_http_upstream_rr_peer_t *peer; /* 后端服务器组成的链表 */  
  13. };  

 

2. 虚拟节点

一个真实节点,一般会对应weight * 160个虚拟节点。

虚拟节点的server成员,指向它所归属的真实节点的server成员,如此一来找到了一个虚拟节点后,

就能找到其归属的真实节点。

但这里有一个问题,通过一个虚拟节点的server成员,可能会找到多个真实节点,而不是一个。

因为如果server指令的第一个参数为域名,那么多个真实节点的server成员都是一样的。

[java] view plain copy
  1. typedef struct {  
  2.     uint32_t hash; /* 虚拟节点的哈希值 */  
  3.     ngx_str_t *server; /* 虚拟节点归属的真实节点,对应真实节点的server成员 */  
  4. } ngx_http_upstream_chash_point_t;  
  5.   
  6. typedef struct {  
  7.     ngx_uint_t number; /* 虚拟节点的个数 */  
  8.     ngx_http_upstream_chash_point_t point[1]; /* 虚拟节点的数组 */  
  9. } ngx_http_upstream_chash_points_t;  
  10.   
  11. typedef struct {  
  12.     ngx_http_complex_value_t key; /* 关联hash指令的第一个参数,用于计算请求的hash值 */  
  13.     ngx_http_upstream_chash_points_t *points; /* 虚拟节点的数组 */  
  14. } ngx_http_upstream_chash_points_t;  

 

3. 请求的一致性哈希数据

[java] view plain copy
  1. typedef struct {  
  2.     /* the round robin data must be first */  
  3.     ngx_http_upstream_rr_peer_data_t rrp; /* round robin的per request负载均衡数据 */  
  4.     ngx_http_upstream_hash_srv_conf_t *conf; /* server配置块 */  
  5.     ngx_str_t key; /* 对于本次请求,hash指令的第一个参数的具体值,用于计算本次请求的哈希值 */  
  6.     ngx_uint_t tries; /* 已经尝试的虚拟节点数 */  
  7.     ngx_uint_t rehash; /* 本算法不使用此成员 */  
  8.     uint32_t hash; /* 根据请求的哈希值,找到顺时方向最近的一个虚拟节点,hash为该虚拟节点在数组中的索引 */  
  9.     ngx_event_get_peer_pt get_rr_peer; /* round robin算法的peer.get函数 */  
  10. } ngx_http_upstream_hash_peer_data_t;  

round robin的per request负载均衡数据。

[java] view plain copy
  1. typedef struct {  
  2.      ngx_http_upstream_rr_peers_t *peers; /* 后端集群 */  
  3.      ngx_http_upstream_rr_peer_t *current; /* 当前使用的后端服务器 */  
  4.      uintptr_t *tried; /* 指向后端服务器的位图 */  
  5.      uintptr_t data; /* 当后端服务器的数量较少时,用于存放其位图 */  
  6. } ngx_http_upstream_rr_peer_data_t;  

 

指令的解析函数

 

在一个upstream配置块中,如果有hash指令,且它只带一个参数,则使用的负载均衡算法为哈希算法,比如:

hash $host$uri;

在一个upstream配置块中,如果有hash指令,且它带了两个参数,且第二个参数为consistent,则使用的

负载均衡算法为一致性哈希算法,比如:

hash $host$uri consistent;

 

这说明hash指令所属的模块ngx_http_upstream_hash_module同时实现了两种负载均衡算法,而实际上

哈希算法、一致性哈希算法完全可以用两个独立的模块来实现,它们本身并没有多少关联。

哈希算法的实现比较简单,类似之前分析过的ip_hash,接下来分析的是一致性哈希算法。

 

hash指令的解析函数主要做了:

把hash指令的第一个参数,关联到一个ngx_http_complex_value_t变量,之后可以通过该变量获取参数的实时值。

指定此upstream块中server指令支持的属性。

根据hash指令携带的参数来判断是使用哈希算法,还是一致性哈希算法。如果hash指令的第二个参数为"consistent",

则表示使用一致性哈希算法,指定upstream块的初始化函数uscf->peer.init_upstream。

[java] view plain copy
  1. static char *ngx_http_upstream_hash(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)  
  2. {  
  3.     ngx_http_upstream_hash_srv_conf_t *hcf = conf;  
  4.     ngx_str_t *value;  
  5.     ngx_http_upstream_srv_conf_t *uscf;  
  6.     ngx_http_compile_complex_value_t ccv;  
  7.   
  8.     value = cf->args->elts;  
  9.     ngx_memzero(&ccv, sizeof(ngx_http_compile_complex_value_t));  
  10.   
  11.     /* 把hash指令的第一个参数,关联到一个ngx_http_complex_value_t变量, 
  12.      * 之后可以通过该变量获取参数的实时值。 
  13.      */  
  14.     ccv.cf = conf;  
  15.     ccv.value = &value[1];  
  16.     ccv.complex_value = &hcf->key;  
  17.   
  18.     if (ngx_http_compile_complex_value(&ccv) != NGX_OK)  
  19.         return NGX_CONF_ERROR;  
  20.   
  21.     /* 获取所在的upstream{}块 */  
  22.     uscf = ngx_http_conf_get_module_srv_conf(cf, ngx_http_upstream_module);  
  23.     if (uscf->peer.init_upstream)  
  24.         ngx_conf_log_error(NGX_LOG_WARN, cf, 0"load balancing method redefined");  
  25.   
  26.     /* 指定此upstream块中server指令支持的属性 */  
  27.     uscf->flags = NGX_HTTP_UPSTREAM_CREATE  
  28.         | NGX_HTTP_UPSTREAM_WEIGHT  
  29.         | NGX_HTTP_UPSTREAM_MAX_FAILS  
  30.         | NGX_HTTP_UPSTREAM_FAIL_TIMEOUT  
  31.         | NGX_HTTP_UPSTREAM_DOWN;  
  32.   
  33.     /* 根据hash指令携带的参数来判断是使用哈希算法,还是一致性哈希算法。 
  34.      * 每种算法都有自己的upstream块初始化函数。 
  35.       */  
  36.     if (cf->args->nelts == 2)  
  37.         uscf->peer.init_upstream = ngx_http_upstream_init_hash;  
  38.     else if (ngx_strcmp(value[2].data, "consistent") == 0)  
  39.         uscf->peer.init_upstream = ngx_http_upstream_init_chash;  
  40.     else  
  41.         ngx_conf_log_error(NGX_LOG_EMERG, cf, 0"invalid parameter \"%V\"", &value[2]);  
  42.   
  43.     return NGX_CONF_OK;  
  44. }  

 

初始化upstream块

 

执行完指令的解析函数后,紧接着会调用所有HTTP模块的init main conf函数。

在执行ngx_http_upstream_module的init main conf函数时,会调用所有upstream块的初始化函数。

对于使用一致性哈希的upstream块,其初始化函数(peer.init_upstream)就是上一步中指定

ngx_http_upstream_init_chash,它主要做了:

调用round robin的upstream块初始化函数来创建和初始化真实节点

指定per request的负载均衡初始化函数peer.init

创建和初始化虚拟节点数组,使该数组中的虚拟节点有序而不重复

[java] view plain copy
  1. static ngx_int_t ngx_http_upstream_init_chash(ngx_conf_t *cf, ngx_http_upstream_srv_conf_t *us)  
  2. {  
  3.     u_char *host, *port, c;  
  4.     size_t host_len, port_len, size;  
  5.     uint32_t hash, base_hash;  
  6.     ngx_str_t *server;  
  7.     ngx_uint_t npoints, i, j;  
  8.     ngx_http_upstream_rr_peer_t *peer;  
  9.     ngx_http_upstream_rr_peers_t *peers;  
  10.     ngx_http_upstream_chash_points_t *points;  
  11.     ngx_http_upstream_hash_srv_conf_t *hcf;  
  12.     union {  
  13.         uint32_t value;  
  14.         u_char byte[4];  
  15.     } prev_hash;  
  16.   
  17.     /* 使用round robin的upstream块初始化函数,创建和初始化真实节点 */  
  18.     if (ngx_http_upstream_init_round_robin(cf, us) != NGX_OK)  
  19.         return NGX_ERROR:  
  20.   
  21.     /* 重新设置per request的负载均衡初始化函数 */  
  22.     us->peer.init = ngx_http_upstream_init_chash_peer;  
  23.   
  24.     peers = us->peer.data; /* 真实节点的集群 */  
  25.     npoints = peers->total_weight * 160;  
  26.   
  27.     /* 一共创建npoints个虚拟节点 */  
  28.     size = sizeof(ngx_http_upstream_chash_points_t) +   
  29.         sizeof(ngx_http_upstream_chash_point_t) * (npoints - 1);  
  30.   
  31.     points = ngx_palloc(cf->pool, size);  
  32.     if (points == NULL)  
  33.         return NGX_ERROR;  
  34.   
  35.     points->number = 0;  
  36.   
  37.     /* 初始化所有的虚拟节点 */  
  38.     for (peer = peers->peer; peer; peer = peer->next) {  
  39.         server = &peer->server; /* server指令的第一个参数, server.name */  
  40.           
  41.         /* Hash expression is compatible with Cache::Memcached::Fast: 
  42.          * crc32(HOST 0 PORT PREV_HASH). 
  43.          */  
  44.         if (server->len >= 5 && ngx_strncasecmp(server->data, (u_char *) "unix:"5) == 0)  
  45.         {  
  46.             host = server->data + 5;  
  47.             host_len = server->len - 5;  
  48.             port = NULL;  
  49.             port_len = 0;  
  50.             goto done;  
  51.         }  
  52.   
  53.         /* 把每个peer的server成员,解析为HOST和PORT */  
  54.         for (j = 0; j < server->len; j++) {  
  55.             c = server->data[server->len - j - 1];  
  56.   
  57.             if (c == ":") {  
  58.                 host = server->data;  
  59.                 host_len = server->len - j - 1;  
  60.                 port = server->data + server->len - j;  
  61.                 port_len = j;  
  62.                 goto done;  
  63.             }  
  64.   
  65.             if (c < '0' || c > '9'/* 表示没有指定端口 */  
  66.                 break;  
  67.         }  
  68.   
  69.         host = server->data;  
  70.         host_len = server->len;  
  71.         port = NULL;  
  72.         port_len = 0;  
  73.   
  74.    done:  
  75.         /* 根据解析peer的server成员所得的HOST和PORT,计算虚拟节点的base_hash值 */  
  76.         ngx_crc32_init(base_hash);  
  77.         ngx_crc32_update(&base_hash, host, host_len);  
  78.         ngx_crc32_update(&base_hash, (u_char *) ""1); /* 空字符串包含字符\0 */  
  79.         ngx_crc32_update(&base_hash, port, port_len);  
  80.           
  81.         /* 对于归属同一个真实节点的虚拟节点,它们的base_hash值相同,而prev_hash不同 */  
  82.         prev_hash.value = 0;  
  83.         npoints = peer->weight * 160;  
  84.   
  85.         for (j = 0; j < npoints; j++) {  
  86.             hash = base_hash;  
  87.             ngx_crc32_update(&hash, prev_hash.byte4);  
  88.             ngx_crc32_final(hash);  
  89.   
  90.             points->point[points->number].hash = hash; /* 虚拟节点的哈希值 */  
  91.             points->point[points->number].server = server; /* 虚拟节点所归属的真实节点,对应真实节点的server成员 */  
  92.             points->number++;  
  93.   
  94. #if (NGX_HAVE_LITTLE_ENDIAN)  
  95.            prev_hash.value = hash;  
  96. #else  
  97.            prev_hash.byte[0] = (u_char) (hash & 0xff);  
  98.            prev_hash.byte[1] = (u_char) ((hash >> 8) & 0xff);  
  99.            prev_hash.byte[2] = (u_char) ((hash >> 16) & 0xff);  
  100.            prev_hash.byte[3] = (u_char) ((hash >> 24) & 0xff);  
  101. #endif  
  102.         }     
  103.     }  
  104.   
  105.     /* 使用快速排序,使虚拟节点数组的元素,按照其hash值从小到大有序 */  
  106.     ngx_qsort(points->point, points->number, sizeof(ngx_http_upstream_chash_point_t),  
  107.         ngx_http_upstream_chash_cmp_points);  
  108.   
  109.     /* 如果虚拟节点数组中,有多个元素的hash值相同,只保留第一个 */  
  110.     for (i = 0, j = 1; j < points->number; j++)  
  111.         if (points->point[i].hash != points->point[j].hash)  
  112.             points->point[++i] = points->point[j];  
  113.   
  114.     /* 经过上述步骤后,虚拟节点数组中的元素,有序而不重复 */  
  115.     points->number = i + 1;  
  116.      
  117.     hcf = ngx_http_conf_upstream_srv_conf(us, ngx_http_upstream_hash_module);  
  118.     hcf->points = points; /* 保存虚拟节点数组 */  
  119.   
  120.     return NGX_OK;  
  121. }  
[java] view plain copy
  1. static int ngx_libc_cdel ngx_http_upstream_chash_cmp_points(const void *one, const void *two)  
  2. {  
  3.     ngx_http_upstream_chash_point_t *first = (ngx_http_upstream_chash_point_t *) one;  
  4.     ngx_http_upstream_chash_point_t *second = (ngx_http_upstream_chash_point_t *) two;  
  5.   
  6.     if (first->hash < second->hash)  
  7.         return -1;  
  8.     else if (first->hash > second->hash)  
  9.         return 1;  
  10.     else  
  11.         return 0;  
  12. }  

 

初始化请求的负载均衡数据 

 

收到一个请求后,一般使用的反向代理模块(upstream模块)为ngx_http_proxy_module,

其NGX_HTTP_CONTENT_PHASE阶段的处理函数为ngx_http_proxy_handler,在初始化upstream机制的

ngx_http_upstream_init_request函数中,调用在第二步中指定的peer.init,主要用于初始化请求的负载均衡数据。

对于一致性哈希,peer.init实例为ngx_http_upstream_init_chash_peer,主要做了:

首先调用hash算法的per request负载均衡初始化函数,创建和初始化请求的负载均衡数据。

重新指定peer.get,用于选取一个真实节点来处理本次请求。

获取的本请求对应的hash指令的第一个参数值,计算请求的hash值。

寻找第一个hash值大于等于请求的哈希值的虚拟节点,即寻找“顺时针方向最近”的一个虚拟节点。

[java] view plain copy
  1. static ngx_int_t ngx_http_upstream_init_chash_peer(ngx_http_request_t *r, ngx_http_upstream_srv_conf_t *us)  
  2. {  
  3.     uint32_t hash;  
  4.     ngx_http_upstream_hash_srv_conf_t *hcf;  
  5.     ngx_http_upstream_hash_peer_data_t *hp;  
  6.   
  7.     /* 调用hash算法的per request负载均衡初始化函数,创建和初始化请求的负载均衡数据 */  
  8.     if (ngx_http_upstream_init_hash_peer(r, us) != NGX_OK)  
  9.         return NGX_ERROR;  
  10.   
  11.     /* 重新指定peer.get,用于选取一个真实节点 */  
  12.     r->upstream->peer.get = ngx_http_upstream_get_chash_peer;  
  13.   
  14.     hp = r->upstream->peer.data;  
  15.     hcf = ngx_http_conf_upstream_srv_conf(us, ngx_http_upstream_hash_module);  
  16.   
  17.     /* 根据获取的本请求对应的hash指令的第一个参数值,计算请求的hash值 */  
  18.     hash = ngx_crc32_long(hp->key.data, hp->key.len);  
  19.   
  20.     /* 根据请求的hash值,找到顺时针方向最近的一个虚拟节点,hp->hash记录此虚拟节点 
  21.      * 在数组中的索引。 
  22.      */  
  23.     hp->hash = ngx_http_upstream_find_chash_point(hcf->points, hash);  
  24.   
  25.     return NGX_OK:  
  26. }  

hash算法的per request负载均衡初始化函数。

[java] view plain copy
  1. static ngx_int_t ngx_http_upstream_init_hash_peer(ngx_http_request_t *r, ngx_http_upstream_srv_conf_t *us)  
  2. {  
  3.     ngx_http_upstream_hash_srv_conf_t *hcf;  
  4.     ngx_http_upstream_hash_peer_data_t *hp;  
  5.   
  6.     hp = ngx_palloc(r->pool, sizeof(ngx_http_upstream_hash_peer_data_t));  
  7.     if (hp == NULL)  
  8.         return NGX_ERROR:  
  9.   
  10.     /* 调用round robin的per request负载均衡初始化函数 */  
  11.     r->upstream->peer.data = &hp->rrp;  
  12.     if (ngx_http_upstream_init_round_robin_peer(r, us) != NGX_OK)  
  13.         return NGX_ERROR;  
  14.   
  15.     r->upstream->peer.get = ngx_http_upstream_get_hash_peer;  
  16.     hcf = ngx_http_conf_upstream_srv_conf(us, ngx_http_upstream_hash_module);  
  17.   
  18.    /* 获取本请求对应的hash指令的第一个参数值,用于计算请求的hash值 */  
  19.     if (ngx_http_complex_value(r, &hcf->key, &hp->key) != NGX_OK)  
  20.         return NGX_ERROR;  
  21.     ...  
  22.     hp->conf = hcf;  
  23.     hp->tries = 0;  
  24.     hp->rehash = 0;  
  25.     hp->hash = 0;  
  26.     hp->get_rr_peer = ngx_http_upstream_get_round_robin_peer; /* round robin的peer.get函数 */  
  27.   
  28.     return NGX_OK;  
  29. }  

我们知道虚拟节点数组是有序的,事先已按照虚拟节点的hash值从小到大排序好了。

现在使用二分查找,寻找第一个hash值大于等于请求的哈希值的虚拟节点,即“顺时针方向最近”的一个虚拟节点。

[java] view plain copy
  1. static ngx_uint_t ngx_http_upstream_find_chash_point(ngx_http_upstream_chash_points_t *points, uint32_t hash)  
  2. {  
  3.     ngx_uint_t i, j, k;  
  4.     ngx_http_upstream_chash_point_t *point;  
  5.       
  6.     /* find first point >= hash */  
  7.   
  8.     point = &points->point[0];  
  9.     i = 0;  
  10.     j = points->number;'  
  11.   
  12.     while(i < j) {  
  13.         k = (i + j) / 2;  
  14.   
  15.         if (hash > point[k].hash)  
  16.             i = k + 1;  
  17.         else if (hash < point[k].hash)  
  18.             j = k;  
  19.          else  
  20.             return k;  
  21.     }  
  22.   
  23.     return i;  
  24. }  

 

选取一个真实节点

 

一般upstream块中会有多个真实节点,那么对于本次请求,要选定哪一个真实节点呢?

对于一致性哈希算法,选取真实节点的peer.get函数为ngx_http_upstream_get_chash_peer。

 

其实在peer.init中,已经找到了该请求对应的虚拟节点了:

根据请求对应的hash指令的第一个参数值,计算请求的hash值。

寻找第一个哈希值大于等于请求的hash值的虚拟节点,即“顺时针方向最近”的一个虚拟节点。

 

在peer.get中,需查找此虚拟节点对应的真实节点。

根据虚拟节点的server成员,在真实节点数组中查找server成员一样的且可用的真实节点。

如果找不到,那么沿着顺时针方向,继续查找下一个虚拟节点对应的真实节点。

如果找到一个真实节点,那么就是它了。

如果找到多个真实节点,使用轮询的方法从中选取一个。

[java] view plain copy
  1. static ngx_http_upstream_get_chash_peer(ngx_peer_connection_t *pc, void *data)  
  2. {  
  3.     ngx_http_upstream_hash_peer_data_t *hp = data; /* 请求的负载均衡数据 */  
  4.     time_t now;  
  5.     intptr_t m;  
  6.     ngx_str_t *server;  
  7.     ngx_int_t total;  
  8.     ngx_uint_t i, n, best_i;  
  9.     ngx_http_upstream_rr_peer_t *peer, *best;  
  10.     ngx_http_upstream_chash_point_t *point;  
  11.     ngx_http_upstream_chash_points_t *points;  
  12.     ngx_http_upstream_hash_srv_conf_t *hcf;  
  13.     ...  
  14.     pc->cached = 0;  
  15.     pc->connection = NULL:  
  16.     now = ngx_time();  
  17.     hcf = hp->conf;  
  18.     points = hcf->points; /* 虚拟节点数组 */  
  19.     point = &points->point[0]; /* 指向第一个虚拟节点 */  
  20.   
  21.     for ( ; ; ) {  
  22.         /* 在peer.init中,已根据请求的哈希值,找到顺时针方向最近的一个虚拟节点, 
  23.          * hash为该虚拟节点在数组中的索引。 
  24.          * 一开始hash值肯定小于number,之后每尝试一个虚拟节点后,hash++。取模是为了防止越界访问。 
  25.          */  
  26.         server = point[hp->hash % points->number].server;  
  27.         best = NULL;  
  28.         best_i = 0;  
  29.         total = 0;  
  30.   
  31.         /* 遍历真实节点数组,寻找可用的、该虚拟节点归属的真实节点(server成员相同), 
  32.           * 如果有多个真实节点同时符合条件,那么使用轮询来从中选取一个真实节点。 
  33.           */  
  34.         for (peer = hp->rrp.peers->peer, i = 0; peer; peer = peer->next, i++) {  
  35.             /* 检查此真实节点在状态位图中对应的位,为1时表示不可用 */  
  36.             n = i / (8 * sizeof(uintptr_t));  
  37.             m = (uintptr_t) 1 << i % (8 * sizeof(uintptr_t));  
  38.             if (hp->rrp.tried[n] & m)  
  39.                 continue;              
  40.   
  41.             /* server指令中携带了down属性,表示后端永久不可用 */  
  42.             if (peer->down)  
  43.                 continue;  
  44.   
  45.             /* 如果真实节点的server成员和虚拟节点的不同,表示虚拟节点不属于此真实节点 */  
  46.             if (peer->server.len != server->len ||   
  47.                 ngx_strncmp(peer->server.data, server->data, server->len) != 0)  
  48.                 continue;  
  49.   
  50.            /* 在一段时间内,如果此真实节点的失败次数,超过了允许的最大值,那么不允许使用了 */  
  51.            if (peer->max_fails  
  52.                 && peer->fails >= peer->max_fails  
  53.                 && now - peer->checked <= peer->fail_timeout)  
  54.                 continue;  
  55.           
  56.             peer->current_weight += peer->effective_weight; /* 对每个真实节点,增加其当前权重 */  
  57.             total += peer->effective_weight; /* 累加所有真实节点的有效权重 */  
  58.   
  59.             /* 如果之前此真实节点发生了失败,会减小其effective_weight来降低它的权重。           
  60.              * 此后又通过增加其effective_weight来恢复它的权重。           
  61.              */          
  62.             if (peer->effective_weight < peer->weight)   
  63.                 peer->effective_weight++;  
  64.           
  65.             /* 选取当前权重最大者,作为本次选定的真实节点 */  
  66.             if (best == NULL || peer->current_weight > best->current_weight) {  
  67.                 best = peer;  
  68.                 best_i = i;  
  69.             }  
  70.         }  
  71.   
  72.         /* 如果选定了一个真实节点 */  
  73.         if (best) {  
  74.             best->current_weight -= total; /* 如果使用了轮询,需要降低选定节点的当前权重 */  
  75.             goto found;  
  76.         }  
  77.   
  78.         hp->hash++; /* 增加虚拟节点的索引,即“沿着顺时针方向” */  
  79.         hp->tries++; /* 已经尝试的虚拟节点数 */  
  80.   
  81.         /* 如果把所有的虚拟节点都尝试了一遍,还找不到可用的真实节点 */  
  82.         if (hp->tries >= points->number)  
  83.             return NGX_BUSY;  
  84.     }  
  85.   
  86. found: /* 找到了和虚拟节点相对应的、可用的真实节点了 */  
  87.     hp->rrp.current = best; /* 选定的真实节点 */  
  88.     /* 保存选定的后端服务器的地址,之后会向这个地址发起连接 */  
  89.     pc->sockaddr = peer->sockaddr;  
  90.     pc->socklen = peer->socklen;  
  91.     pc->name = &peer->name;  
  92.     best->conns++;  
  93.   
  94.     /* 更新checked时间 */  
  95.     if (now - best->checked > best->fail_timeout)  
  96.         best->checked = now;  
  97.   
  98.     n = best_i / (8 * sizeof(uintptr_t));  
  99.     m = (uintptr_t) 1 << best_i % (8 * sizeof(uintptr_t));  
  100.   
  101.     /* 对于本次请求,如果之后需要再次选取真实节点,不能再选取同一个了 */      
  102.     hp->rrp->tried[n] |= m;  
  103.   
  104.     return NGX_OK;  

算法介绍

 

当后端是缓存服务器时,经常使用一致性哈希算法来进行负载均衡。

使用一致性哈希的好处在于,增减集群的缓存服务器时,只有少量的缓存会失效,回源量较小。

在nginx+ats / haproxy+squid等CDN架构中,nginx/haproxy所使用的负载均衡算法便是一致性哈希。

 

我们举个例子来说明一致性哈希的好处。

假设后端集群包含三台缓存服务器,A、B、C。

请求r1、r2落在A上。

请求r3、r4落在B上。

请求r5、r6落在C上。

使用一致性哈希时,当缓存服务器B宕机时,r1/r2会仍然落在A上,r5/r6会仍然落在C上,

也就是说这两台服务器上的缓存都不会失效。r3/r4会被重新分配给A或者C,并产生回源。

使用其它算法,当缓存服务器B宕机时,r1/r2不再落在A上,r5/r6不再落在C上了。

也就是说A、B、C上的缓存都失效了,所有的请求都要回源。

 

这里不介绍一致性哈希算法的基本原理,如果不了解,先花个10分钟看下这篇文章:

http://www.codeproject.com/Articles/56138/Consistent-hashing

 

在分析模块代码之前,先来看下nginx所实现的一致性哈希算法。

 

1. 初始化upstream块

主要工作是创建和初始化真实节点、创建和初始化虚拟节点。

其中真实节点是使用round robin的方法创建的。

 

Q:总共有多少个虚拟节点,一个真实节点对应多少个虚拟节点?

累加真实节点的权重,算出总的权重值total_weight,虚拟节点的个数一般为total_weight * 160。

一个权重为weight的真实节点,对应的虚拟节点数为weight * 160。

 

Q:对于每一个真实节点,是如何创建其对应的虚拟节点的?

1. 真实节点的server成员是其server指令的第一个参数,首先把它解析为HOST和PORT。

    base_hash = crc32(HOST 0 PORT)

    一个真实节点对应weight * 160个虚拟节点,对于每个虚拟节点来说,base_hash都是一样的。

2. 为了使每个虚拟节点的hash值都不同,又引入了PREV_HASH,它是上一个虚拟节点的hash值。

    hash = crc32(base_hash PREV_HASH)

3. 虚拟节点的server成员,指向真实节点的server成员。如此一来,通过比较虚拟节点和真实节点的

   server成员是否相同,可以判断它们是否是相对应的。

 

创建和初始化好虚拟节点数组后,对其中的虚拟节点按照hash值进行排序,对于hash值相同的虚拟节点,只保留第一个。

经过上述步骤,我们得到一个所有虚拟节点组成的数组,其元素的hash值有序而不重复。也就是说,ring建立起来了。

 

2. 初始话请求的负载均衡数据

根据hash指令第一个参数的实时值KEY,KEY一般是$host$uri之类的,计算出本次请求的哈希值。

hash = crc32(KEY)

根据请求的哈希值,在虚拟节点数组中,找到“顺时针方向”最近的一个虚拟节点,其索引为i。

什么叫顺时针方向最近?就是point[i - 1].hash < hash <= point[i].hash。

本次请求就落在该虚拟节点上了,之后交由其对应的真实节点来处理。

 

3. 选取真实节点

在peer.init中,已经知道请求落在哪个虚拟节点上了。

在peer.get中,需要查找虚拟节点对应的真实节点。

根据虚拟节点的server成员,在真实节点数组中查找server成员相同的、可用的真实节点。

如果找不到,那么沿着顺时针方向,继续查找下一个虚拟节点对应的真实节点。

如果找到了一个,那么就是它了。

如果找到了多个,使用轮询的方法从中选取一个。

 

4. 缺陷和改进

一个虚拟节点和一个真实节点,是依据它们的server成员来关联的。

这会出现一种情况,一个虚拟节点对应了多个真实节点,因为:

如果server指令的第一个参数为域名,可能解析为多个真实节点,那么这些真实节点的server成员都是一样的。

对于一个请求,计算其KEY的hash值,顺时针找到最近的虚拟节点后,发现该虚拟节点对应了多个真实节点。

使用哪个真实节点呢?本模块就使用轮询的方法,来从多个真实节点中选一个。

但我们知道使用一致性哈希的场景中,真实节点一般是缓存服务器。

一个虚拟节点对应多个真实节点,会导致一个文件被缓存在多个缓存服务器上。

这会增加磁盘的使用量,以及回源量,显然不是我们希望看到的。

 

解决这个问题的方法其实很简单,就是虚拟节点和真实节点通过name成员来建立关联。

因为就算对应同一条server配置,server的第一个参数为域名,各个真实节点的name成员也是唯一的。

这样一来,找到了一个虚拟节点,就能找到一个唯一的真实节点,不会有上述问题了。

 

数据结构

 

1. 真实节点

就是采用round robin算法所创建的后端服务器,类型为ngx_http_upstream_rr_peer_t。

需要注意的是,如果server指令的第一个参数是IP和端口,那么一条server指令只对应一个真实节点。

如果server指令的第一个参数是域名,一条server指令可能对应多个真实节点。

它们的server成员是相同的,可以通过name成员区分。

[java] view plain copy
  1. struct ngx_http_upstream_rr_peer_s {  
  2.     struct sockaddr *sockaddr; /* 后端服务器的地址 */  
  3.     socklen_t socklen; /* 地址的长度*/  
  4.     ngx_str_t name; /* 后端服务器地址的字符串,server.addrs[i].name */  
  5.     ngx_str_t server; /* server的名称,server.name */  
  6.        
  7.     ngx_int_t current_weight; /* 当前的权重,动态调整,初始值为0 */  
  8.     ngx_int_t effective_weight; /* 有效的权重,会因为失败而降低 */  
  9.     ngx_int_t weight; /* 配置项指定的权重,固定值 */  
  10.   
  11.     ngx_uint_t conns; /* 当前连接数 */  
  12.   
  13.     ngx_uint_t fails; /* "一段时间内",已经失败的次数 */  
  14.     time_t accessed; /* 最近一次失败的时间点 */  
  15.     time_t checked; /* 用于检查是否超过了"一段时间" */  
  16.   
  17.     ngx_uint_t max_fails; /* "一段时间内",最大的失败次数,固定值 */  
  18.     time_t fail_timeout; /* "一段时间"的值,固定值 */  
  19.     ngx_uint_t down; /* 服务器永久不可用的标志 */  
  20.     ...  
  21.     ngx_http_upstream_rr_peer_t *next; /* 指向下一个后端,用于构成链表 */  
  22.     ...  
  23. } ngx_http_upstream_rr_peer_t;  

ngx_http_upstream_rr_peers_t表示一组后端服务器,比如一个后端集群。

[java] view plain copy
  1. struct ngx_http_upstream_rr_peers_s {  
  2.     ngx_uint_t number; /* 后端服务器的数量 */  
  3.     ...  
  4.     ngx_uint_t total_weight; /* 所有后端服务器权重的累加值 */  
  5.   
  6.     unsigned single:1/* 是否只有一台后端服务器 */  
  7.     unsigned weighted:1/* 是否使用权重 */  
  8.   
  9.     ngx_str_t *name; /* upstream配置块的名称 */  
  10.   
  11.     ngx_http_upstream_rr_peers_t *next; /* backup服务器集群 */  
  12.     ngx_http_upstream_rr_peer_t *peer; /* 后端服务器组成的链表 */  
  13. };  

 

2. 虚拟节点

一个真实节点,一般会对应weight * 160个虚拟节点。

虚拟节点的server成员,指向它所归属的真实节点的server成员,如此一来找到了一个虚拟节点后,

就能找到其归属的真实节点。

但这里有一个问题,通过一个虚拟节点的server成员,可能会找到多个真实节点,而不是一个。

因为如果server指令的第一个参数为域名,那么多个真实节点的server成员都是一样的。

[java] view plain copy
  1. typedef struct {  
  2.     uint32_t hash; /* 虚拟节点的哈希值 */  
  3.     ngx_str_t *server; /* 虚拟节点归属的真实节点,对应真实节点的server成员 */  
  4. } ngx_http_upstream_chash_point_t;  
  5.   
  6. typedef struct {  
  7.     ngx_uint_t number; /* 虚拟节点的个数 */  
  8.     ngx_http_upstream_chash_point_t point[1]; /* 虚拟节点的数组 */  
  9. } ngx_http_upstream_chash_points_t;  
  10.   
  11. typedef struct {  
  12.     ngx_http_complex_value_t key; /* 关联hash指令的第一个参数,用于计算请求的hash值 */  
  13.     ngx_http_upstream_chash_points_t *points; /* 虚拟节点的数组 */  
  14. } ngx_http_upstream_chash_points_t;  

 

3. 请求的一致性哈希数据

[java] view plain copy
  1. typedef struct {  
  2.     /* the round robin data must be first */  
  3.     ngx_http_upstream_rr_peer_data_t rrp; /* round robin的per request负载均衡数据 */  
  4.     ngx_http_upstream_hash_srv_conf_t *conf; /* server配置块 */  
  5.     ngx_str_t key; /* 对于本次请求,hash指令的第一个参数的具体值,用于计算本次请求的哈希值 */  
  6.     ngx_uint_t tries; /* 已经尝试的虚拟节点数 */  
  7.     ngx_uint_t rehash; /* 本算法不使用此成员 */  
  8.     uint32_t hash; /* 根据请求的哈希值,找到顺时方向最近的一个虚拟节点,hash为该虚拟节点在数组中的索引 */  
  9.     ngx_event_get_peer_pt get_rr_peer; /* round robin算法的peer.get函数 */  
  10. } ngx_http_upstream_hash_peer_data_t;  

round robin的per request负载均衡数据。

[java] view plain copy
  1. typedef struct {  
  2.      ngx_http_upstream_rr_peers_t *peers; /* 后端集群 */  
  3.      ngx_http_upstream_rr_peer_t *current; /* 当前使用的后端服务器 */  
  4.      uintptr_t *tried; /* 指向后端服务器的位图 */  
  5.      uintptr_t data; /* 当后端服务器的数量较少时,用于存放其位图 */  
  6. } ngx_http_upstream_rr_peer_data_t;  

 

指令的解析函数

 

在一个upstream配置块中,如果有hash指令,且它只带一个参数,则使用的负载均衡算法为哈希算法,比如:

hash $host$uri;

在一个upstream配置块中,如果有hash指令,且它带了两个参数,且第二个参数为consistent,则使用的

负载均衡算法为一致性哈希算法,比如:

hash $host$uri consistent;

 

这说明hash指令所属的模块ngx_http_upstream_hash_module同时实现了两种负载均衡算法,而实际上

哈希算法、一致性哈希算法完全可以用两个独立的模块来实现,它们本身并没有多少关联。

哈希算法的实现比较简单,类似之前分析过的ip_hash,接下来分析的是一致性哈希算法。

 

hash指令的解析函数主要做了:

把hash指令的第一个参数,关联到一个ngx_http_complex_value_t变量,之后可以通过该变量获取参数的实时值。

指定此upstream块中server指令支持的属性。

根据hash指令携带的参数来判断是使用哈希算法,还是一致性哈希算法。如果hash指令的第二个参数为"consistent",

则表示使用一致性哈希算法,指定upstream块的初始化函数uscf->peer.init_upstream。

[java] view plain copy
  1. static char *ngx_http_upstream_hash(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)  
  2. {  
  3.     ngx_http_upstream_hash_srv_conf_t *hcf = conf;  
  4.     ngx_str_t *value;  
  5.     ngx_http_upstream_srv_conf_t *uscf;  
  6.     ngx_http_compile_complex_value_t ccv;  
  7.   
  8.     value = cf->args->elts;  
  9.     ngx_memzero(&ccv, sizeof(ngx_http_compile_complex_value_t));  
  10.   
  11.     /* 把hash指令的第一个参数,关联到一个ngx_http_complex_value_t变量, 
  12.      * 之后可以通过该变量获取参数的实时值。 
  13.      */  
  14.     ccv.cf = conf;  
  15.     ccv.value = &value[1];  
  16.     ccv.complex_value = &hcf->key;  
  17.   
  18.     if (ngx_http_compile_complex_value(&ccv) != NGX_OK)  
  19.         return NGX_CONF_ERROR;  
  20.   
  21.     /* 获取所在的upstream{}块 */  
  22.     uscf = ngx_http_conf_get_module_srv_conf(cf, ngx_http_upstream_module);  
  23.     if (uscf->peer.init_upstream)  
  24.         ngx_conf_log_error(NGX_LOG_WARN, cf, 0"load balancing method redefined");  
  25.   
  26.     /* 指定此upstream块中server指令支持的属性 */  
  27.     uscf->flags = NGX_HTTP_UPSTREAM_CREATE  
  28.         | NGX_HTTP_UPSTREAM_WEIGHT  
  29.         | NGX_HTTP_UPSTREAM_MAX_FAILS  
  30.         | NGX_HTTP_UPSTREAM_FAIL_TIMEOUT  
  31.         | NGX_HTTP_UPSTREAM_DOWN;  
  32.   
  33.     /* 根据hash指令携带的参数来判断是使用哈希算法,还是一致性哈希算法。 
  34.      * 每种算法都有自己的upstream块初始化函数。 
  35.       */  
  36.     if (cf->args->nelts == 2)  
  37.         uscf->peer.init_upstream = ngx_http_upstream_init_hash;  
  38.     else if (ngx_strcmp(value[2].data, "consistent") == 0)  
  39.         uscf->peer.init_upstream = ngx_http_upstream_init_chash;  
  40.     else  
  41.         ngx_conf_log_error(NGX_LOG_EMERG, cf, 0"invalid parameter \"%V\"", &value[2]);  
  42.   
  43.     return NGX_CONF_OK;  
  44. }  

 

初始化upstream块

 

执行完指令的解析函数后,紧接着会调用所有HTTP模块的init main conf函数。

在执行ngx_http_upstream_module的init main conf函数时,会调用所有upstream块的初始化函数。

对于使用一致性哈希的upstream块,其初始化函数(peer.init_upstream)就是上一步中指定

ngx_http_upstream_init_chash,它主要做了:

调用round robin的upstream块初始化函数来创建和初始化真实节点

指定per request的负载均衡初始化函数peer.init

创建和初始化虚拟节点数组,使该数组中的虚拟节点有序而不重复

[java] view plain copy
  1. static ngx_int_t ngx_http_upstream_init_chash(ngx_conf_t *cf, ngx_http_upstream_srv_conf_t *us)  
  2. {  
  3.     u_char *host, *port, c;  
  4.     size_t host_len, port_len, size;  
  5.     uint32_t hash, base_hash;  
  6.     ngx_str_t *server;  
  7.     ngx_uint_t npoints, i, j;  
  8.     ngx_http_upstream_rr_peer_t *peer;  
  9.     ngx_http_upstream_rr_peers_t *peers;  
  10.     ngx_http_upstream_chash_points_t *points;  
  11.     ngx_http_upstream_hash_srv_conf_t *hcf;  
  12.     union {  
  13.         uint32_t value;  
  14.         u_char byte[4];  
  15.     } prev_hash;  
  16.   
  17.     /* 使用round robin的upstream块初始化函数,创建和初始化真实节点 */  
  18.     if (ngx_http_upstream_init_round_robin(cf, us) != NGX_OK)  
  19.         return NGX_ERROR:  
  20.   
  21.     /* 重新设置per request的负载均衡初始化函数 */  
  22.     us->peer.init = ngx_http_upstream_init_chash_peer;  
  23.   
  24.     peers = us->peer.data; /* 真实节点的集群 */  
  25.     npoints = peers->total_weight * 160;  
  26.   
  27.     /* 一共创建npoints个虚拟节点 */  
  28.     size = sizeof(ngx_http_upstream_chash_points_t) +   
  29.         sizeof(ngx_http_upstream_chash_point_t) * (npoints - 1);  
  30.   
  31.     points = ngx_palloc(cf->pool, size);  
  32.     if (points == NULL)  
  33.         return NGX_ERROR;  
  34.   
  35.     points->number = 0;  
  36.   
  37.     /* 初始化所有的虚拟节点 */  
  38.     for (peer = peers->peer; peer; peer = peer->next) {  
  39.         server = &peer->server; /* server指令的第一个参数, server.name */  
  40.           
  41.         /* Hash expression is compatible with Cache::Memcached::Fast: 
  42.          * crc32(HOST 0 PORT PREV_HASH). 
  43.          */  
  44.         if (server->len >= 5 && ngx_strncasecmp(server->data, (u_char *) "unix:"5) == 0)  
  45.         {  
  46.             host = server->data + 5;  
  47.             host_len = server->len - 5;  
  48.             port = NULL;  
  49.             port_len = 0;  
  50.             goto done;  
  51.         }  
  52.   
  53.         /* 把每个peer的server成员,解析为HOST和PORT */  
  54.         for (j = 0; j < server->len; j++) {  
  55.             c = server->data[server->len - j - 1];  
  56.   
  57.             if (c == ":") {  
  58.                 host = server->data;  
  59.                 host_len = server->len - j - 1;  
  60.                 port = server->data + server->len - j;  
  61.                 port_len = j;  
  62.                 goto done;  
  63.             }  
  64.   
  65.             if (c < '0' || c > '9'/* 表示没有指定端口 */  
  66.                 break;  
  67.         }  
  68.   
  69.         host = server->data;  
  70.         host_len = server->len;  
  71.         port = NULL;  
  72.         port_len = 0;  
  73.   
  74.    done:  
  75.         /* 根据解析peer的server成员所得的HOST和PORT,计算虚拟节点的base_hash值 */  
  76.         ngx_crc32_init(base_hash);  
  77.         ngx_crc32_update(&base_hash, host, host_len);  
  78.         ngx_crc32_update(&base_hash, (u_char *) ""1); /* 空字符串包含字符\0 */  
  79.         ngx_crc32_update(&base_hash, port, port_len);  
  80.           
  81.         /* 对于归属同一个真实节点的虚拟节点,它们的base_hash值相同,而prev_hash不同 */  
  82.         prev_hash.value = 0;  
  83.         npoints = peer->weight * 160;  
  84.   
  85.         for (j = 0; j < npoints; j++) {  
  86.             hash = base_hash;  
  87.             ngx_crc32_update(&hash, prev_hash.byte4);  
  88.             ngx_crc32_final(hash);  
  89.   
  90.             points->point[points->number].hash = hash; /* 虚拟节点的哈希值 */  
  91.             points->point[points->number].server = server; /* 虚拟节点所归属的真实节点,对应真实节点的server成员 */  
  92.             points->number++;  
  93.   
  94. #if (NGX_HAVE_LITTLE_ENDIAN)  
  95.            prev_hash.value = hash;  
  96. #else  
  97.            prev_hash.byte[0] = (u_char) (hash & 0xff);  
  98.            prev_hash.byte[1] = (u_char) ((hash >> 8) & 0xff);  
  99.            prev_hash.byte[2] = (u_char) ((hash >> 16) & 0xff);  
  100.            prev_hash.byte[3] = (u_char) ((hash >> 24) & 0xff);  
  101. #endif  
  102.         }     
  103.     }  
  104.   
  105.     /* 使用快速排序,使虚拟节点数组的元素,按照其hash值从小到大有序 */  
  106.     ngx_qsort(points->point, points->number, sizeof(ngx_http_upstream_chash_point_t),  
  107.         ngx_http_upstream_chash_cmp_points);  
  108.   
  109.     /* 如果虚拟节点数组中,有多个元素的hash值相同,只保留第一个 */  
  110.     for (i = 0, j = 1; j < points->number; j++)  
  111.         if (points->point[i].hash != points->point[j].hash)  
  112.             points->point[++i] = points->point[j];  
  113.   
  114.     /* 经过上述步骤后,虚拟节点数组中的元素,有序而不重复 */  
  115.     points->number = i + 1;  
  116.      
  117.     hcf = ngx_http_conf_upstream_srv_conf(us, ngx_http_upstream_hash_module);  
  118.     hcf->points = points; /* 保存虚拟节点数组 */  
  119.   
  120.     return NGX_OK;  
  121. }  
[java] view plain copy
  1. static int ngx_libc_cdel ngx_http_upstream_chash_cmp_points(const void *one, const void *two)  
  2. {  
  3.     ngx_http_upstream_chash_point_t *first = (ngx_http_upstream_chash_point_t *) one;  
  4.     ngx_http_upstream_chash_point_t *second = (ngx_http_upstream_chash_point_t *) two;  
  5.   
  6.     if (first->hash < second->hash)  
  7.         return -1;  
  8.     else if (first->hash > second->hash)  
  9.         return 1;  
  10.     else  
  11.         return 0;  
  12. }  

 

初始化请求的负载均衡数据 

 

收到一个请求后,一般使用的反向代理模块(upstream模块)为ngx_http_proxy_module,

其NGX_HTTP_CONTENT_PHASE阶段的处理函数为ngx_http_proxy_handler,在初始化upstream机制的

ngx_http_upstream_init_request函数中,调用在第二步中指定的peer.init,主要用于初始化请求的负载均衡数据。

对于一致性哈希,peer.init实例为ngx_http_upstream_init_chash_peer,主要做了:

首先调用hash算法的per request负载均衡初始化函数,创建和初始化请求的负载均衡数据。

重新指定peer.get,用于选取一个真实节点来处理本次请求。

获取的本请求对应的hash指令的第一个参数值,计算请求的hash值。

寻找第一个hash值大于等于请求的哈希值的虚拟节点,即寻找“顺时针方向最近”的一个虚拟节点。

[java] view plain copy
  1. static ngx_int_t ngx_http_upstream_init_chash_peer(ngx_http_request_t *r, ngx_http_upstream_srv_conf_t *us)  
  2. {  
  3.     uint32_t hash;  
  4.     ngx_http_upstream_hash_srv_conf_t *hcf;  
  5.     ngx_http_upstream_hash_peer_data_t *hp;  
  6.   
  7.     /* 调用hash算法的per request负载均衡初始化函数,创建和初始化请求的负载均衡数据 */  
  8.     if (ngx_http_upstream_init_hash_peer(r, us) != NGX_OK)  
  9.         return NGX_ERROR;  
  10.   
  11.     /* 重新指定peer.get,用于选取一个真实节点 */  
  12.     r->upstream->peer.get = ngx_http_upstream_get_chash_peer;  
  13.   
  14.     hp = r->upstream->peer.data;  
  15.     hcf = ngx_http_conf_upstream_srv_conf(us, ngx_http_upstream_hash_module);  
  16.   
  17.     /* 根据获取的本请求对应的hash指令的第一个参数值,计算请求的hash值 */  
  18.     hash = ngx_crc32_long(hp->key.data, hp->key.len);  
  19.   
  20.     /* 根据请求的hash值,找到顺时针方向最近的一个虚拟节点,hp->hash记录此虚拟节点 
  21.      * 在数组中的索引。 
  22.      */  
  23.     hp->hash = ngx_http_upstream_find_chash_point(hcf->points, hash);  
  24.   
  25.     return NGX_OK:  
  26. }  

hash算法的per request负载均衡初始化函数。

[java] view plain copy
  1. static ngx_int_t ngx_http_upstream_init_hash_peer(ngx_http_request_t *r, ngx_http_upstream_srv_conf_t *us)  
  2. {  
  3.     ngx_http_upstream_hash_srv_conf_t *hcf;  
  4.     ngx_http_upstream_hash_peer_data_t *hp;  
  5.   
  6.     hp = ngx_palloc(r->pool, sizeof(ngx_http_upstream_hash_peer_data_t));  
  7.     if (hp == NULL)  
  8.         return NGX_ERROR:  
  9.   
  10.     /* 调用round robin的per request负载均衡初始化函数 */  
  11.     r->upstream->peer.data = &hp->rrp;  
  12.     if (ngx_http_upstream_init_round_robin_peer(r, us) != NGX_OK)  
  13.         return NGX_ERROR;  
  14.   
  15.     r->upstream->peer.get = ngx_http_upstream_get_hash_peer;  
  16.     hcf = ngx_http_conf_upstream_srv_conf(us, ngx_http_upstream_hash_module);  
  17.   
  18.    /* 获取本请求对应的hash指令的第一个参数值,用于计算请求的hash值 */  
  19.     if (ngx_http_complex_value(r, &hcf->key, &hp->key) != NGX_OK)  
  20.         return NGX_ERROR;  
  21.     ...  
  22.     hp->conf = hcf;  
  23.     hp->tries = 0;  
  24.     hp->rehash = 0;  
  25.     hp->hash = 0;  
  26.     hp->get_rr_peer = ngx_http_upstream_get_round_robin_peer; /* round robin的peer.get函数 */  
  27.   
  28.     return NGX_OK;  
  29. }  

我们知道虚拟节点数组是有序的,事先已按照虚拟节点的hash值从小到大排序好了。

现在使用二分查找,寻找第一个hash值大于等于请求的哈希值的虚拟节点,即“顺时针方向最近”的一个虚拟节点。

[java] view plain copy
  1. static ngx_uint_t ngx_http_upstream_find_chash_point(ngx_http_upstream_chash_points_t *points, uint32_t hash)  
  2. {  
  3.     ngx_uint_t i, j, k;  
  4.     ngx_http_upstream_chash_point_t *point;  
  5.       
  6.     /* find first point >= hash */  
  7.   
  8.     point = &points->point[0];  
  9.     i = 0;  
  10.     j = points->number;'  
  11.   
  12.     while(i < j) {  
  13.         k = (i + j) / 2;  
  14.   
  15.         if (hash > point[k].hash)  
  16.             i = k + 1;  
  17.         else if (hash < point[k].hash)  
  18.             j = k;  
  19.          else  
  20.             return k;  
  21.     }  
  22.   
  23.     return i;  
  24. }  

 

选取一个真实节点

 

一般upstream块中会有多个真实节点,那么对于本次请求,要选定哪一个真实节点呢?

对于一致性哈希算法,选取真实节点的peer.get函数为ngx_http_upstream_get_chash_peer。

 

其实在peer.init中,已经找到了该请求对应的虚拟节点了:

根据请求对应的hash指令的第一个参数值,计算请求的hash值。

寻找第一个哈希值大于等于请求的hash值的虚拟节点,即“顺时针方向最近”的一个虚拟节点。

 

在peer.get中,需查找此虚拟节点对应的真实节点。

根据虚拟节点的server成员,在真实节点数组中查找server成员一样的且可用的真实节点。

如果找不到,那么沿着顺时针方向,继续查找下一个虚拟节点对应的真实节点。

如果找到一个真实节点,那么就是它了。

如果找到多个真实节点,使用轮询的方法从中选取一个。

[java] view plain copy
  1. static ngx_http_upstream_get_chash_peer(ngx_peer_connection_t *pc, void *data)  
  2. {  
  3.     ngx_http_upstream_hash_peer_data_t *hp = data; /* 请求的负载均衡数据 */  
  4.     time_t now;  
  5.     intptr_t m;  
  6.     ngx_str_t *server;  
  7.     ngx_int_t total;  
  8.     ngx_uint_t i, n, best_i;  
  9.     ngx_http_upstream_rr_peer_t *peer, *best;  
  10.     ngx_http_upstream_chash_point_t *point;  
  11.     ngx_http_upstream_chash_points_t *points;  
  12.     ngx_http_upstream_hash_srv_conf_t *hcf;  
  13.     ...  
  14.     pc->cached = 0;  
  15.     pc->connection = NULL:  
  16.     now = ngx_time();  
  17.     hcf = hp->conf;  
  18.     points = hcf->points; /* 虚拟节点数组 */  
  19.     point = &points->point[0]; /* 指向第一个虚拟节点 */  
  20.   
  21.     for ( ; ; ) {  
  22.         /* 在peer.init中,已根据请求的哈希值,找到顺时针方向最近的一个虚拟节点, 
  23.          * hash为该虚拟节点在数组中的索引。 
  24.          * 一开始hash值肯定小于number,之后每尝试一个虚拟节点后,hash++。取模是为了防止越界访问。 
  25.          */  
  26.         server = point[hp->hash % points->number].server;  
  27.         best = NULL;  
  28.         best_i = 0;  
  29.         total = 0;  
  30.   
  31.         /* 遍历真实节点数组,寻找可用的、该虚拟节点归属的真实节点(server成员相同), 
  32.           * 如果有多个真实节点同时符合条件,那么使用轮询来从中选取一个真实节点。 
  33.           */  
  34.         for (peer = hp->rrp.peers->peer, i = 0; peer; peer = peer->next, i++) {  
  35.             /* 检查此真实节点在状态位图中对应的位,为1时表示不可用 */  
  36.             n = i / (8 * sizeof(uintptr_t));  
  37.             m = (uintptr_t) 1 << i % (8 * sizeof(uintptr_t));  
  38.             if (hp->rrp.tried[n] & m)  
  39.                 continue;              
  40.   
  41.             /* server指令中携带了down属性,表示后端永久不可用 */  
  42.             if (peer->down)  
  43.                 continue;  
  44.   
  45.             /* 如果真实节点的server成员和虚拟节点的不同,表示虚拟节点不属于此真实节点 */  
  46.             if (peer->server.len != server->len ||   
  47.                 ngx_strncmp(peer->server.data, server->data, server->len) != 0)  
  48.                 continue;  
  49.   
  50.            /* 在一段时间内,如果此真实节点的失败次数,超过了允许的最大值,那么不允许使用了 */  
  51.            if (peer->max_fails  
  52.                 && peer->fails >= peer->max_fails  
  53.                 && now - peer->checked <= peer->fail_timeout)  
  54.                 continue;  
  55.           
  56.             peer->current_weight += peer->effective_weight; /* 对每个真实节点,增加其当前权重 */  
  57.             total += peer->effective_weight; /* 累加所有真实节点的有效权重 */  
  58.   
  59.             /* 如果之前此真实节点发生了失败,会减小其effective_weight来降低它的权重。           
  60.              * 此后又通过增加其effective_weight来恢复它的权重。           
  61.              */          
  62.             if (peer->effective_weight < peer->weight)   
  63.                 peer->effective_weight++;  
  64.           
  65.             /* 选取当前权重最大者,作为本次选定的真实节点 */  
  66.             if (best == NULL || peer->current_weight > best->current_weight) {  
  67.                 best = peer;  
  68.                 best_i = i;  
  69.             }  
  70.         }  
  71.   
  72.         /* 如果选定了一个真实节点 */  
  73.         if (best) {  
  74.             best->current_weight -= total; /* 如果使用了轮询,需要降低选定节点的当前权重 */  
  75.             goto found;  
  76.         }  
  77.   
  78.         hp->hash++; /* 增加虚拟节点的索引,即“沿着顺时针方向” */  
  79.         hp->tries++; /* 已经尝试的虚拟节点数 */  
  80.   
  81.         /* 如果把所有的虚拟节点都尝试了一遍,还找不到可用的真实节点 */  
  82.         if (hp->tries >= points->number)  
  83.             return NGX_BUSY;  
  84.     }  
  85.   
  86. found: /* 找到了和虚拟节点相对应的、可用的真实节点了 */  
  87.     hp->rrp.current = best; /* 选定的真实节点 */  
  88.     /* 保存选定的后端服务器的地址,之后会向这个地址发起连接 */  
  89.     pc->sockaddr = peer->sockaddr;  
  90.     pc->socklen = peer->socklen;  
  91.     pc->name = &peer->name;  
  92.     best->conns++;  
  93.   
  94.     /* 更新checked时间 */  
  95.     if (now - best->checked > best->fail_timeout)  
  96.         best->checked = now;  
  97.   
  98.     n = best_i / (8 * sizeof(uintptr_t));  
  99.     m = (uintptr_t) 1 << best_i % (8 * sizeof(uintptr_t));  
  100.   
  101.     /* 对于本次请求,如果之后需要再次选取真实节点,不能再选取同一个了 */      
  102.     hp->rrp->tried[n] |= m;  
  103.   
  104.     return NGX_OK;  
原创粉丝点击