Nginx的负载均衡 - 一致性哈希 (Consistent Hash)

http://blog.csdn.net/zhangskd/article/details/50256111

Nginx版本:1.9.1

我的博客:http://blog.csdn.net/zhangskd

 

算法介绍

 

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

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

在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. <span style="margin: 0px; padding: 0px; border: none; co
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值