一致性哈希 (Consistent Hash)
最后更新于:2022-04-01 10:13:48
Nginx版本:1.9.1
我的博客:[http://blog.csdn.net/zhangskd](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](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成员区分。
~~~
struct ngx_http_upstream_rr_peer_s {
struct sockaddr *sockaddr; /* 后端服务器的地址 */
socklen_t socklen; /* 地址的长度*/
ngx_str_t name; /* 后端服务器地址的字符串,server.addrs[i].name */
ngx_str_t server; /* server的名称,server.name */
ngx_int_t current_weight; /* 当前的权重,动态调整,初始值为0 */
ngx_int_t effective_weight; /* 有效的权重,会因为失败而降低 */
ngx_int_t weight; /* 配置项指定的权重,固定值 */
ngx_uint_t conns; /* 当前连接数 */
ngx_uint_t fails; /* "一段时间内",已经失败的次数 */
time_t accessed; /* 最近一次失败的时间点 */
time_t checked; /* 用于检查是否超过了"一段时间" */
ngx_uint_t max_fails; /* "一段时间内",最大的失败次数,固定值 */
time_t fail_timeout; /* "一段时间"的值,固定值 */
ngx_uint_t down; /* 服务器永久不可用的标志 */
...
ngx_http_upstream_rr_peer_t *next; /* 指向下一个后端,用于构成链表 */
...
} ngx_http_upstream_rr_peer_t;
~~~
ngx_http_upstream_rr_peers_t表示一组后端服务器,比如一个后端集群。
~~~
struct ngx_http_upstream_rr_peers_s {
ngx_uint_t number; /* 后端服务器的数量 */
...
ngx_uint_t total_weight; /* 所有后端服务器权重的累加值 */
unsigned single:1; /* 是否只有一台后端服务器 */
unsigned weighted:1; /* 是否使用权重 */
ngx_str_t *name; /* upstream配置块的名称 */
ngx_http_upstream_rr_peers_t *next; /* backup服务器集群 */
ngx_http_upstream_rr_peer_t *peer; /* 后端服务器组成的链表 */
};
~~~
**2. 虚拟节点**
一个真实节点,一般会对应weight * 160个虚拟节点。
虚拟节点的server成员,指向它所归属的真实节点的server成员,如此一来找到了一个虚拟节点后,
就能找到其归属的真实节点。
但这里有一个问题,通过一个虚拟节点的server成员,可能会找到多个真实节点,而不是一个。
因为如果server指令的第一个参数为域名,那么多个真实节点的server成员都是一样的。
~~~
typedef struct {
uint32_t hash; /* 虚拟节点的哈希值 */
ngx_str_t *server; /* 虚拟节点归属的真实节点,对应真实节点的server成员 */
} ngx_http_upstream_chash_point_t;
typedef struct {
ngx_uint_t number; /* 虚拟节点的个数 */
ngx_http_upstream_chash_point_t point[1]; /* 虚拟节点的数组 */
} ngx_http_upstream_chash_points_t;
typedef struct {
ngx_http_complex_value_t key; /* 关联hash指令的第一个参数,用于计算请求的hash值 */
ngx_http_upstream_chash_points_t *points; /* 虚拟节点的数组 */
} ngx_http_upstream_chash_points_t;
~~~
**3. 请求的一致性哈希数据**
~~~
typedef struct {
/* the round robin data must be first */
ngx_http_upstream_rr_peer_data_t rrp; /* round robin的per request负载均衡数据 */
ngx_http_upstream_hash_srv_conf_t *conf; /* server配置块 */
ngx_str_t key; /* 对于本次请求,hash指令的第一个参数的具体值,用于计算本次请求的哈希值 */
ngx_uint_t tries; /* 已经尝试的虚拟节点数 */
ngx_uint_t rehash; /* 本算法不使用此成员 */
uint32_t hash; /* 根据请求的哈希值,找到顺时方向最近的一个虚拟节点,hash为该虚拟节点在数组中的索引 */
ngx_event_get_peer_pt get_rr_peer; /* round robin算法的peer.get函数 */
} ngx_http_upstream_hash_peer_data_t;
~~~
round robin的per request负载均衡数据。
~~~
typedef struct {
ngx_http_upstream_rr_peers_t *peers; /* 后端集群 */
ngx_http_upstream_rr_peer_t *current; /* 当前使用的后端服务器 */
uintptr_t *tried; /* 指向后端服务器的位图 */
uintptr_t data; /* 当后端服务器的数量较少时,用于存放其位图 */
} 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。
~~~
static char *ngx_http_upstream_hash(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
ngx_http_upstream_hash_srv_conf_t *hcf = conf;
ngx_str_t *value;
ngx_http_upstream_srv_conf_t *uscf;
ngx_http_compile_complex_value_t ccv;
value = cf->args->elts;
ngx_memzero(&ccv, sizeof(ngx_http_compile_complex_value_t));
/* 把hash指令的第一个参数,关联到一个ngx_http_complex_value_t变量,
* 之后可以通过该变量获取参数的实时值。
*/
ccv.cf = conf;
ccv.value = &value[1];
ccv.complex_value = &hcf->key;
if (ngx_http_compile_complex_value(&ccv) != NGX_OK)
return NGX_CONF_ERROR;
/* 获取所在的upstream{}块 */
uscf = ngx_http_conf_get_module_srv_conf(cf, ngx_http_upstream_module);
if (uscf->peer.init_upstream)
ngx_conf_log_error(NGX_LOG_WARN, cf, 0, "load balancing method redefined");
/* 指定此upstream块中server指令支持的属性 */
uscf->flags = NGX_HTTP_UPSTREAM_CREATE
| NGX_HTTP_UPSTREAM_WEIGHT
| NGX_HTTP_UPSTREAM_MAX_FAILS
| NGX_HTTP_UPSTREAM_FAIL_TIMEOUT
| NGX_HTTP_UPSTREAM_DOWN;
/* 根据hash指令携带的参数来判断是使用哈希算法,还是一致性哈希算法。
* 每种算法都有自己的upstream块初始化函数。
*/
if (cf->args->nelts == 2)
uscf->peer.init_upstream = ngx_http_upstream_init_hash;
else if (ngx_strcmp(value[2].data, "consistent") == 0)
uscf->peer.init_upstream = ngx_http_upstream_init_chash;
else
ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "invalid parameter \"%V\"", &value[2]);
return NGX_CONF_OK;
}
~~~
**初始化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
创建和初始化虚拟节点数组,使该数组中的虚拟节点有序而不重复
~~~
static ngx_int_t ngx_http_upstream_init_chash(ngx_conf_t *cf, ngx_http_upstream_srv_conf_t *us)
{
u_char *host, *port, c;
size_t host_len, port_len, size;
uint32_t hash, base_hash;
ngx_str_t *server;
ngx_uint_t npoints, i, j;
ngx_http_upstream_rr_peer_t *peer;
ngx_http_upstream_rr_peers_t *peers;
ngx_http_upstream_chash_points_t *points;
ngx_http_upstream_hash_srv_conf_t *hcf;
union {
uint32_t value;
u_char byte[4];
} prev_hash;
/* 使用round robin的upstream块初始化函数,创建和初始化真实节点 */
if (ngx_http_upstream_init_round_robin(cf, us) != NGX_OK)
return NGX_ERROR:
/* 重新设置per request的负载均衡初始化函数 */
us->peer.init = ngx_http_upstream_init_chash_peer;
peers = us->peer.data; /* 真实节点的集群 */
npoints = peers->total_weight * 160;
/* 一共创建npoints个虚拟节点 */
size = sizeof(ngx_http_upstream_chash_points_t) +
sizeof(ngx_http_upstream_chash_point_t) * (npoints - 1);
points = ngx_palloc(cf->pool, size);
if (points == NULL)
return NGX_ERROR;
points->number = 0;
/* 初始化所有的虚拟节点 */
for (peer = peers->peer; peer; peer = peer->next) {
server = &peer->server; /* server指令的第一个参数, server.name */
/* Hash expression is compatible with Cache::Memcached::Fast:
* crc32(HOST 0 PORT PREV_HASH).
*/
if (server->len >= 5 && ngx_strncasecmp(server->data, (u_char *) "unix:", 5) == 0)
{
host = server->data + 5;
host_len = server->len - 5;
port = NULL;
port_len = 0;
goto done;
}
/* 把每个peer的server成员,解析为HOST和PORT */
for (j = 0; j < server->len; j++) {
c = server->data[server->len - j - 1];
if (c == ":") {
host = server->data;
host_len = server->len - j - 1;
port = server->data + server->len - j;
port_len = j;
goto done;
}
if (c < '0' || c > '9') /* 表示没有指定端口 */
break;
}
host = server->data;
host_len = server->len;
port = NULL;
port_len = 0;
done:
/* 根据解析peer的server成员所得的HOST和PORT,计算虚拟节点的base_hash值 */
ngx_crc32_init(base_hash);
ngx_crc32_update(&base_hash, host, host_len);
ngx_crc32_update(&base_hash, (u_char *) "", 1); /* 空字符串包含字符\0 */
ngx_crc32_update(&base_hash, port, port_len);
/* 对于归属同一个真实节点的虚拟节点,它们的base_hash值相同,而prev_hash不同 */
prev_hash.value = 0;
npoints = peer->weight * 160;
for (j = 0; j < npoints; j++) {
hash = base_hash;
ngx_crc32_update(&hash, prev_hash.byte, 4);
ngx_crc32_final(hash);
points->point[points->number].hash = hash; /* 虚拟节点的哈希值 */
points->point[points->number].server = server; /* 虚拟节点所归属的真实节点,对应真实节点的server成员 */
points->number++;
#if (NGX_HAVE_LITTLE_ENDIAN)
prev_hash.value = hash;
#else
prev_hash.byte[0] = (u_char) (hash & 0xff);
prev_hash.byte[1] = (u_char) ((hash >> 8) & 0xff);
prev_hash.byte[2] = (u_char) ((hash >> 16) & 0xff);
prev_hash.byte[3] = (u_char) ((hash >> 24) & 0xff);
#endif
}
}
/* 使用快速排序,使虚拟节点数组的元素,按照其hash值从小到大有序 */
ngx_qsort(points->point, points->number, sizeof(ngx_http_upstream_chash_point_t),
ngx_http_upstream_chash_cmp_points);
/* 如果虚拟节点数组中,有多个元素的hash值相同,只保留第一个 */
for (i = 0, j = 1; j < points->number; j++)
if (points->point[i].hash != points->point[j].hash)
points->point[++i] = points->point[j];
/* 经过上述步骤后,虚拟节点数组中的元素,有序而不重复 */
points->number = i + 1;
hcf = ngx_http_conf_upstream_srv_conf(us, ngx_http_upstream_hash_module);
hcf->points = points; /* 保存虚拟节点数组 */
return NGX_OK;
}
~~~
~~~
static int ngx_libc_cdel ngx_http_upstream_chash_cmp_points(const void *one, const void *two)
{
ngx_http_upstream_chash_point_t *first = (ngx_http_upstream_chash_point_t *) one;
ngx_http_upstream_chash_point_t *second = (ngx_http_upstream_chash_point_t *) two;
if (first->hash < second->hash)
return -1;
else if (first->hash > second->hash)
return 1;
else
return 0;
}
~~~
**初始化请求的负载均衡数据**
收到一个请求后,一般使用的反向代理模块(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值大于等于请求的哈希值的虚拟节点,即寻找“顺时针方向最近”的一个虚拟节点。
~~~
static ngx_int_t ngx_http_upstream_init_chash_peer(ngx_http_request_t *r, ngx_http_upstream_srv_conf_t *us)
{
uint32_t hash;
ngx_http_upstream_hash_srv_conf_t *hcf;
ngx_http_upstream_hash_peer_data_t *hp;
/* 调用hash算法的per request负载均衡初始化函数,创建和初始化请求的负载均衡数据 */
if (ngx_http_upstream_init_hash_peer(r, us) != NGX_OK)
return NGX_ERROR;
/* 重新指定peer.get,用于选取一个真实节点 */
r->upstream->peer.get = ngx_http_upstream_get_chash_peer;
hp = r->upstream->peer.data;
hcf = ngx_http_conf_upstream_srv_conf(us, ngx_http_upstream_hash_module);
/* 根据获取的本请求对应的hash指令的第一个参数值,计算请求的hash值 */
hash = ngx_crc32_long(hp->key.data, hp->key.len);
/* 根据请求的hash值,找到顺时针方向最近的一个虚拟节点,hp->hash记录此虚拟节点
* 在数组中的索引。
*/
hp->hash = ngx_http_upstream_find_chash_point(hcf->points, hash);
return NGX_OK:
}
~~~
hash算法的per request负载均衡初始化函数。
~~~
static ngx_int_t ngx_http_upstream_init_hash_peer(ngx_http_request_t *r, ngx_http_upstream_srv_conf_t *us)
{
ngx_http_upstream_hash_srv_conf_t *hcf;
ngx_http_upstream_hash_peer_data_t *hp;
hp = ngx_palloc(r->pool, sizeof(ngx_http_upstream_hash_peer_data_t));
if (hp == NULL)
return NGX_ERROR:
/* 调用round robin的per request负载均衡初始化函数 */
r->upstream->peer.data = &hp->rrp;
if (ngx_http_upstream_init_round_robin_peer(r, us) != NGX_OK)
return NGX_ERROR;
r->upstream->peer.get = ngx_http_upstream_get_hash_peer;
hcf = ngx_http_conf_upstream_srv_conf(us, ngx_http_upstream_hash_module);
/* 获取本请求对应的hash指令的第一个参数值,用于计算请求的hash值 */
if (ngx_http_complex_value(r, &hcf->key, &hp->key) != NGX_OK)
return NGX_ERROR;
...
hp->conf = hcf;
hp->tries = 0;
hp->rehash = 0;
hp->hash = 0;
hp->get_rr_peer = ngx_http_upstream_get_round_robin_peer; /* round robin的peer.get函数 */
return NGX_OK;
}
~~~
我们知道虚拟节点数组是有序的,事先已按照虚拟节点的hash值从小到大排序好了。
现在使用二分查找,寻找第一个hash值大于等于请求的哈希值的虚拟节点,即“顺时针方向最近”的一个虚拟节点。
~~~
static ngx_uint_t ngx_http_upstream_find_chash_point(ngx_http_upstream_chash_points_t *points, uint32_t hash)
{
ngx_uint_t i, j, k;
ngx_http_upstream_chash_point_t *point;
/* find first point >= hash */
point = &points->point[0];
i = 0;
j = points->number;'
while(i < j) {
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;
}
~~~
**选取一个真实节点**
一般upstream块中会有多个真实节点,那么对于本次请求,要选定哪一个真实节点呢?
对于一致性哈希算法,选取真实节点的peer.get函数为ngx_http_upstream_get_chash_peer。
其实在peer.init中,已经找到了该请求对应的虚拟节点了:
根据请求对应的hash指令的第一个参数值,计算请求的hash值。
寻找第一个哈希值大于等于请求的hash值的虚拟节点,即“顺时针方向最近”的一个虚拟节点。
在peer.get中,需查找此虚拟节点对应的真实节点。
根据虚拟节点的server成员,在真实节点数组中查找server成员一样的且可用的真实节点。
如果找不到,那么沿着顺时针方向,继续查找下一个虚拟节点对应的真实节点。
如果找到一个真实节点,那么就是它了。
如果找到多个真实节点,使用轮询的方法从中选取一个。
~~~
static ngx_http_upstream_get_chash_peer(ngx_peer_connection_t *pc, void *data)
{
ngx_http_upstream_hash_peer_data_t *hp = data; /* 请求的负载均衡数据 */
time_t now;
intptr_t m;
ngx_str_t *server;
ngx_int_t total;
ngx_uint_t i, n, best_i;
ngx_http_upstream_rr_peer_t *peer, *best;
ngx_http_upstream_chash_point_t *point;
ngx_http_upstream_chash_points_t *points;
ngx_http_upstream_hash_srv_conf_t *hcf;
...
pc->cached = 0;
pc->connection = NULL:
now = ngx_time();
hcf = hp->conf;
points = hcf->points; /* 虚拟节点数组 */
point = &points->point[0]; /* 指向第一个虚拟节点 */
for ( ; ; ) {
/* 在peer.init中,已根据请求的哈希值,找到顺时针方向最近的一个虚拟节点,
* hash为该虚拟节点在数组中的索引。
* 一开始hash值肯定小于number,之后每尝试一个虚拟节点后,hash++。取模是为了防止越界访问。
*/
server = point[hp->hash % points->number].server;
best = NULL;
best_i = 0;
total = 0;
/* 遍历真实节点数组,寻找可用的、该虚拟节点归属的真实节点(server成员相同),
* 如果有多个真实节点同时符合条件,那么使用轮询来从中选取一个真实节点。
*/
for (peer = hp->rrp.peers->peer, i = 0; peer; peer = peer->next, i++) {
/* 检查此真实节点在状态位图中对应的位,为1时表示不可用 */
n = i / (8 * sizeof(uintptr_t));
m = (uintptr_t) 1 << i % (8 * sizeof(uintptr_t));
if (hp->rrp.tried[n] & m)
continue;
/* server指令中携带了down属性,表示后端永久不可用 */
if (peer->down)
continue;
/* 如果真实节点的server成员和虚拟节点的不同,表示虚拟节点不属于此真实节点 */
if (peer->server.len != server->len ||
ngx_strncmp(peer->server.data, server->data, server->len) != 0)
continue;
/* 在一段时间内,如果此真实节点的失败次数,超过了允许的最大值,那么不允许使用了 */
if (peer->max_fails
&& peer->fails >= peer->max_fails
&& now - peer->checked <= peer->fail_timeout)
continue;
peer->current_weight += peer->effective_weight; /* 对每个真实节点,增加其当前权重 */
total += peer->effective_weight; /* 累加所有真实节点的有效权重 */
/* 如果之前此真实节点发生了失败,会减小其effective_weight来降低它的权重。
* 此后又通过增加其effective_weight来恢复它的权重。
*/
if (peer->effective_weight < peer->weight)
peer->effective_weight++;
/* 选取当前权重最大者,作为本次选定的真实节点 */
if (best == NULL || peer->current_weight > best->current_weight) {
best = peer;
best_i = i;
}
}
/* 如果选定了一个真实节点 */
if (best) {
best->current_weight -= total; /* 如果使用了轮询,需要降低选定节点的当前权重 */
goto found;
}
hp->hash++; /* 增加虚拟节点的索引,即“沿着顺时针方向” */
hp->tries++; /* 已经尝试的虚拟节点数 */
/* 如果把所有的虚拟节点都尝试了一遍,还找不到可用的真实节点 */
if (hp->tries >= points->number)
return NGX_BUSY;
}
found: /* 找到了和虚拟节点相对应的、可用的真实节点了 */
hp->rrp.current = best; /* 选定的真实节点 */
/* 保存选定的后端服务器的地址,之后会向这个地址发起连接 */
pc->sockaddr = peer->sockaddr;
pc->socklen = peer->socklen;
pc->name = &peer->name;
best->conns++;
/* 更新checked时间 */
if (now - best->checked > best->fail_timeout)
best->checked = now;
n = best_i / (8 * sizeof(uintptr_t));
m = (uintptr_t) 1 << best_i % (8 * sizeof(uintptr_t));
/* 对于本次请求,如果之后需要再次选取真实节点,不能再选取同一个了 */
hp->rrp->tried[n] |= m;
return NGX_OK;
}
~~~
最少连接 (least_conn)
最后更新于:2022-04-01 10:13:45
Nginx版本:1.9.1
我的博客:[http://blog.csdn.net/zhangskd](http://blog.csdn.net/zhangskd)
**算法介绍**
我们知道轮询算法是把请求平均的转发给各个后端,使它们的负载大致相同。
这有个前提,就是每个请求所占用的后端时间要差不多,如果有些请求占用的时间很长,会导致其所在的后端
负载较高。在这种场景下,把请求转发给连接数较少的后端,能够达到更好的负载均衡效果,这就是least_conn算法。
least_conn算法很简单,首选遍历后端集群,比较每个后端的conns/weight,选取该值最小的后端。
如果有多个后端的conns/weight值同为最小的,那么对它们采用加权轮询算法。
**指令的解析函数**
在一个upstream配置块中,如果有least_conn指令,表示使用least connected负载均衡算法。
least_conn指令的解析函数为ngx_http_upstream_least_conn,主要做了:
指定初始化此upstream块的函数uscf->peer.init_upstream
指定此upstream块中server指令支持的属性
~~~
static char *ngx_http_upstream_least_conn(ngx_conf_t *cf, ngx_command_t *cmd, void *ctx)
{
ngx_http_upstream_srv_conf_t *uscf;
/* 获取所在的upstream{}块 */
uscf = ngx_http_conf_get_module_srv_conf(cf, ngx_http_upstream_module);
if (uscf->peer.init_upstream)
ngx_conf_log_error(NGX_LOG_WARN, cf, 0, "load balancing method redefined");
/* 此upstream块的初始化函数 */
uscf->peer.init_upstream = ngx_http_upstream_init_least_conn;
/* 指定此upstream块中server指令支持的属性 */
uscf->flags = NGX_HTTP_UPSTREAM_CREATE
| NGX_HTTP_UPSTREAM_WEIGHT
| NGX_HTTP_UPSTREAM_MAX_FAILS
| NGX_HTTP_UPSTREAM_FAIL_TIMEOUT
| NGX_HTTP_UPSTREAM_DOWN
| NGX_HTTP_UPSTREAM_BACKUP;
return NGX_CONF_OK;
}
~~~
以下是upstream块中server指令可支持的属性
NGX_HTTP_UPSTREAM_CREATE:检查是否重复创建,以及必要的参数是否填写
NGX_HTTP_UPSTREAM_WEIGHT:server指令支持weight属性
NGX_HTTP_UPSTREAM_MAX_FAILS:server指令支持max_fails属性
NGX_HTTP_UPSTREAM_FAIL_TIMEOUT:server指令支持fail_timeout属性
NGX_HTTP_UPSTREAM_DOWN:server指令支持down属性
NGX_HTTP_UPSTREAM_BACKUP:server指令支持backup属性
**初始化upstream块**
执行完指令的解析函数后,紧接着会调用所有HTTP模块的init main conf函数。
在执行ngx_http_upstream_module的init main conf函数时,会调用所有upstream块的初始化函数。
对于使用least_conn的upstream块,其初始化函数(peer.init_upstream)就是上一步中指定
ngx_http_upstream_init_least_conn,它主要做了:
调用round robin的upstream块初始化函数来创建和初始化后端集群,保存该upstream块的数据
指定per request的负载均衡初始化函数peer.init
因为脏活累活都让round robin的upstream块初始化函数给干了,所以ngx_http_upstream_init_least_conn很简单。
~~~
static ngx_int_t ngx_http_upstream_init_least_conn(ngx_conf_t *cf, ngx_http_upstream_srv_conf_t *us)
{
ngx_log_debug0(NGX_LOG_DEBUG_HTTP, cf->log, 0, "init least conn");
/* 使用round robin的upstream块初始化函数,创建和初始化后端集群 */
if (ngx_http_upstream_init_round_robin(cf, us) != NGX_OK)
return NGX_ERROR;
/* 重新设置per request的负载均衡初始化函数 */
us->peer.init = ngx_http_upstream_init_least_conn_peer;
return NGX_OK;
}
~~~
**初始化请求的负载均衡数据**
收到一个请求后,一般使用的反向代理模块(upstream模块)为ngx_http_proxy_module,
其NGX_HTTP_CONTENT_PHASE阶段的处理函数为ngx_http_proxy_handler,在初始化upstream机制的
ngx_http_upstream_init_request函数中,调用在第二步中指定的peer.init,主要用于初始化请求的负载均衡数据。
对于least_conn,peer.init实例为ngx_http_upstream_init_least_conn_peer,主要做了:
调用round robin的peer.init来初始化请求的负载均衡数据
重新指定peer.get,用于从集群中选取一台后端服务器
least_conn的per request负载均衡数据和round robin的完全一样,都是一个ngx_http_upstream_rr_peer_data_t实例。
~~~
static ngx_int_t ngx_http_upstream_init_least_conn_peer(ngx_http_request_t *r, ngx_http_upstream_srv_conf_t *us)
{
ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "init least conn peer");
/* 调用round robin的per request负载均衡初始化函数 */
if (ngx_http_upstream_init_round_robin_peer(r, us) != NGX_OK)
return NGX_ERROR;
/* 指定peer.get,用于从集群中选取一台后端 */
r->upstream->peer.get = ngx_http_upstream_get_least_conn_peer;
return NGX_OK;
}
~~~
**选取一台后端服务器**
一般upstream块中会有多台后端,那么对于本次请求,要选定哪一台后端呢?
这时候第三步中r->upstream->peer.get指向的函数就派上用场了:
采用least connected算法,从集群中选出一台后端来处理本次请求。 选定后端的地址保存在pc->sockaddr,pc为主动连接。
函数的返回值:
NGX_DONE:选定一个后端,和该后端的连接已经建立。之后会直接发送请求。
NGX_OK:选定一个后端,和该后端的连接尚未建立。之后会和后端建立连接。
NGX_BUSY:所有的后端(包括备份集群)都不可用。之后会给客户端发送502(Bad Gateway)。
~~~
static ngx_int_t ngx_http_upstream_get_least_conn_peer(ngx_peer_connection_t *pc, void *data)
{
ngx_http_upstream_rr_peer_data_t *rrp = data; /* 请求的负载均衡数据 */
time_t now;
uintptr_t m;
ngx_int_t rc, total;
ngx_uint_t i, n, p, many;
ngx_http_upstream_rr_peer_t *peer, *best;
ngx_http_upstream_rr_peers_t *peers;
...
/* 如果集群只包含一台后端,那么就不用选了 */
if (rrp->peers->single)
return ngx_http_upstream_get_round_robin_peer(pc, rrp);
pc->cached = 0;
pc->connection = NULL;
now = ngx_time();
peers = rrp->peers; /* 后端集群 */
best = NULL;
total = 0;
...
/* 遍历后端集群 */
for (peer = peers->peer, i = 0; peer; peer = peer->next, i++)
{
/* 检查此后端在状态位图中对应的位,为1时表示不可用 */
n = i / (8 * sizeof(uintptr_t));
m = (uintptr_t) 1 << i % (8 * sizeof(uintptr_t));
if (rrp->tried[n] & m)
continue;
/* server指令中携带了down属性,表示后端永久不可用 */
if (peer->down)
continue;
/* 在一段时间内,如果此后端服务器的失败次数,超过了允许的最大值,那么不允许使用此后端了 */
if (peer->max_fails && peer->fails >= peer->max_fails
&& now - peer->checked <= peer->fail_timeout)
continue;
/* select peer with least number of connections; if there are multiple peers
* with the same number of connections, select based on round-robin.
*/
/* 比较各个后端的conns/weight,选取最小者;
* 如果有多个最小者,记录第一个的序号p,且设置many标志。
*/
if (best == NULL || peer->conns * best->weight < best->conns * peer->weight)
{
best = peer;
many = 0;
p = i;
} else if (peer->conns * best->weight == best->conns * peer->weight)
many = 1;
}
/* 找不到可用的后端 */
if (best == NULL)
goto failed;
/* 如果有多个后端的conns/weight同为最小者,则对它们使用轮询算法 */
if (many) {
for (peer = best, i = p; peer; peer->peer->next, i++)
{
/* 检查此后端在状态位图中对应的位,为1时表示不可用 */
n = i / (8 * sizeof(uintptr_t));
m = (uintptr_t) 1 << i % (8 * sizeof(uintptr_t));
/* server指令中携带了down属性,表示后端永久不可用 */
if (peer->down)
continue;
/* conns/weight必须为最小的 */
if (peer->conns * best->weight != best->conns * peer->weight)
continue;
/* 在一段时间内,如果此后端服务器的失败次数,超过了允许的最大值,那么不允许使用此后端了 */
if (peer->max_fails && peer->fails >= peer->max_fails
&& now - peer->checked <= peer->fail_timeout)
continue;
peer->current_weight += peer->effective_weight; /* 对每个后端,增加其当前权重 */
total += peer->effective_weight; /* 累加所有后端的有效权重 */
/* 如果之前此后端发生了失败,会减小其effective_weight来降低它的权重。
* 此后在选取后端的过程中,又通过增加其effective_weight来恢复它的权重。
*/
if (peer->effective_weight < peer->weight)
peer->effective_weight++;
/* 选取当前权重最大者,作为本次选定的后端 */
if (best == NULL || peer->current_weight > best->current_weight) {
best = peer;
p = i;
}
}
}
best->current_weight -= total; /* 如果使用轮询,要降低选定后端的当前权重 */
/* 更新checked时间 */
if (now - best->checked > best->fail_timeout)
best->checked = now;
/* 保存选定的后端服务器的地址,之后会向这个地址发起连接 */
pc->sockaddr = best->sockaddr;
pc->socklen = best->socklen;
pc->name = &best->name;
best->conns++; /* 增加选定后端的当前连接数 */
n = p / (8 * sizeof(uintptr_t));
m = (uintptr_t) 1 << p % (8 * sizeof(uintptr_t));
rrp->tried[n] |= m; /* 对于此请求,如果之后需要再次选取后端,不能再选取这个后端了 */
return NGX_OK;
failed:
/* 如果不能从集群中选取一台后端,那么尝试备用集群 */
if (peers->next) {
...
rrp->peers = peers->next;
n = (rrp->peers->number + (8 * sizeof(uintptr_t) - 1))
/ (8 * sizeof(uintptr_t));
for (i = 0; i < n; i++)
rrp->tried[i] = 0;
/* 重新调用本函数 */
rc = ngx_http_upstream_get_least_conn_peer(pc, rrp);
if (rc != NGX_BUSY)
return rc;
}
/* all peers failed, mark them as live for quick recovery */
for (peer = peers->peer; peer; peer = peer->next) {
peer->fails = 0;
}
pc->name = peers->name;
return NGX_BUSY;
}
~~~
保持会话 (ip_hash)
最后更新于:2022-04-01 10:13:43
Nginx版本:1.9.1
我的博客:[http://blog.csdn.net/zhangskd](http://blog.csdn.net/zhangskd)
**算法介绍**
ip_hash算法的原理很简单,根据请求所属的客户端IP计算得到一个数值,然后把请求发往该数值对应的后端。
所以同一个客户端的请求,都会发往同一台后端,除非该后端不可用了。ip_hash能够达到保持会话的效果。
ip_hash是基于round robin的,判断后端是否可用的方法是一样的。
第一步,根据客户端IP计算得到一个数值。
hash1 = (hash0 * 113 + addr[0]) % 6271;
hash2 = (hash1 * 113 + addr[1]) % 6271;
hash3 = (hash2 * 113 + addr[2]) % 6271;
hash3就是计算所得的数值,它只和初始数值hash0以及客户端的IP有关。
第二步,根据计算所得数值,找到对应的后端。
w = hash3 % total_weight;
while (w >= peer->weight) {
w -= peer->weight;
peer = peer->next;
p++;
}
total_weight为所有后端权重之和。遍历后端链表时,依次减去每个后端的权重,直到w小于某个后端的权重。
选定的后端在链表中的序号为p。因为total_weight和每个后端的weight都是固定的,所以如果hash3值相同,
则找到的后端相同。
**指令的解析函数**
在一个upstream配置块中,如果有ip_hash指令,表示使用ip_hash负载均衡算法。
ip_hash指令的解析函数为ngx_http_upstream_ip_hash,主要做了:
指定初始化此upstream块的函数peer.init_upstream
指定此upstream块中server指令支持的属性
~~~
static char *ngx_http_upstream_ip_hash (ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
ngx_http_upstream_srv_conf_t *uscf;
/* 获取对应的upstream配置块 */
uscf = ngx_http_conf_get_module_srv_conf(cf, ngx_http_upstream_module);
if (uscf->peer.init_upstream)
ngx_conf_log_error(NGX_LOG_WARN, cf, 0, "load balancing method redefined");
/* 指定初始化此upstream块的函数 */
uscf->peer.init_upstream = ngx_http_upstream_init_ip_hash;
/* 指定此upstream块中server指令支持的属性 */
uscf->flags = NGX_HTTP_UPSTREAM_CREATE
| NGX_HTTP_UPSTREAM_WEIGHT
| NGX_HTTP_UPSTREAM_MAX_FAILS
| NGX_HTTP_UPSTREAM_FAIL_TIMEOUT
| NGX_HTTP_UPSTREAM_DOWN;
return NGX_CONF_OK;
}
~~~
以下是upstream块中server指令可支持的属性
NGX_HTTP_UPSTREAM_CREATE:检查是否重复创建,以及必要的参数是否填写
NGX_HTTP_UPSTREAM_WEIGHT:server指令支持weight属性
NGX_HTTP_UPSTREAM_MAX_FAILS:server指令支持max_fails属性
NGX_HTTP_UPSTREAM_FAIL_TIMEOUT:server指令支持fail_timeout属性
NGX_HTTP_UPSTREAM_DOWN:server指令支持down属性
NGX_HTTP_UPSTREAM_BACKUP:server指令支持backup属性
**初始化upstream块**
执行完指令的解析函数后,紧接着会调用所有HTTP模块的init main conf函数。
在执行ngx_http_upstream_module的init main conf函数时,会调用所有upstream块的初始化函数。
对于使用ip_hash的upstream块,其初始化函数(peer.init_upstream)就是上一步中指定的
ngx_http_upstream_init_ip_hash。它主要做了:
调用默认的初始化函数ngx_http_upstream_init_round_robin来创建和初始化后端集群,保存该upstream块的数据
指定初始化请求的负载均衡数据的函数peer.init
因为脏活累活都让默认的函数给干了,所以ngx_http_upstream_init_ip_hash的代码就几行:)
~~~
static ngx_int_t ngx_http_upstream_init_ip_hash (ngx_conf_t *cf, ngx_http_upstream_srv_conf_t *us)
{
if (ngx_http_upstream_init_round_robin(cf, us) != NGX_OK)
return NGX_ERROR;
us->peer.init = ngx_http_upstream_init_ip_hash_peer; /* 初始化请求负载均衡数据的函数 */
return NGX_OK;
}
~~~
**初始化请求的负载均衡数据**
收到一个请求后,一般使用的反向代理模块(upstream模块)为ngx_http_proxy_module,
其NGX_HTTP_CONTENT_PHASE阶段的处理函数为ngx_http_proxy_handler,在初始化upstream机制的
ngx_http_upstream_init_request函数中,调用在第二步中指定的peer.init,主要用于初始化请求的负载均衡数据。
对于ip_hash,peer.init实例为ngx_http_upstream_init_ip_hash_peer,主要做了:
调用round robin的per request负载均衡初始化函数,创建和初始化其per request负载均衡数据,即iphp->rrp。
重新指定peer.get,用于从集群中选取一台后端服务器。
保存客户端的地址,初始化ip_hash的per request负载均衡数据。
ip_hash的per request负载均衡数据的结构体为ngx_http_upstream_ip_hash_peer_data_t。
~~~
typedef struct {
ngx_http_upstream_rr_peer_data_t rrp; /* round robin的per request负载均衡数据 */
ngx_uint_t hash; /* 根据客户端IP计算所得的hash值 */
u_char addrlen; /* 使用客户端IP的后三个字节来计算hash值 */
u_char *addr; /* 客户端的IP */
u_char tries; /* 已经尝试了多少次 */
ngx_event_get_peer_pt get_rr_peer; /* round robin算法的peer.get函数 */
} ngx_http_upstream_ip_hash_peer_data_t;
~~~
~~~
static ngx_int_t ngx_http_upstream_init_ip_hash_peer (ngx_http_request_t *r, ngx_http_upstream_srv_conf_t *us)
{
struct sockaddr_in *sin;
...
ngx_http_upstream_ip_hash_peer_data_t *iphp;
/* 创建ip_hash的per request负载均衡数据的实例 */
iphp = ngx_palloc(r->pool, sizeof(ngx_http_upstream_ip_hash_peer_data_t));
if (iphp == NULL)
return NGX_ERROR;
/* 首先调用round robin的per request负载均衡数据的初始化函数,
* 创建和初始化round robin的per request负载均衡数据实例,即iphp->rrp。
*/
r->upstream->peer.data = &iphp->rrp;
if (ngx_http_upstream_init_round_robin_peer(r, us) != NGX_OK)
return NGX_ERROR:
/* 重新指定peer.get,用于从集群中选取一台后端服务器 */
r->upstream->peer.get = ngx_http_upstream_get_ip_hash_peer;
/* 客户端的地址类型 */
switch(r->connection->sockaddr->sa_family) {
case AF_INET:
sin = (struct sockaddr_in *) r->connection->sockaddr;
iphp->addr = (u_char *) &sin->sin_addr.s_addr; /* 客户端的IP */
iphp->addrlen = 3; /* 使用客户端IP的后三个字节来计算hash值 */
break;
#if (NGX_HAVE_INET6)
...
#endif
default:
iphp->addr = ngx_http_upstream_ip_hash_pseudo_addr;
iphp->addrlen = 3;
}
iphp->hash = 89;
iphp->tries = 0;
iphp->get_rr_peer = ngx_http_upstream_get_round_robin_peer; /* 保存round robin的peer.get函数 */
}
~~~
**选取一台后端服务器**
一般upstream块中会有多台后端,那么对于本次请求,要选定哪一台后端呢?
这时候第三步中r->upstream->peer.get指向的函数就派上用场了:
采用ip_hash算法,从集群中选出一台后端来处理本次请求。 选定后端的地址保存在pc->sockaddr,pc为主动连接。
函数的返回值:
NGX_DONE:选定一个后端,和该后端的连接已经建立。之后会直接发送请求。
NGX_OK:选定一个后端,和该后端的连接尚未建立。之后会和后端建立连接。
NGX_BUSY:所有的后端(包括备份集群)都不可用。之后会给客户端发送502(Bad Gateway)。
~~~
static ngx_int_t ngx_http_upstream_get_ip_hash_peer (ngx_peer_connection_t *pc, void *data)
{
ngx_http_upstream_ip_hash_peer_data_t *iphp = data; /* 请求的负载均衡数据 */
time_t now;
ngx_int_t w;
uintptr_t m;
ngx_uint_t i, n, p, hash;
ngx_http_upstream_rr_peer_t *peer;
...
/* 如果只有一台后端,或者尝试次数超过20次,则使用轮询的方式来选取后端 */
if (iphp->tries > 20 || iphp->rrp.peers->single) {
return iphp->get_rr_peer(pc, &iphp->rrp);
}
now = ngx_time();
pc->cached = 0;
pc->connection = NULL;
hash = iphp->hash; /* 本次选取的初始hash值 */
for ( ; ; ) {
/* 根据客户端IP、本次选取的初始hash值,计算得到本次最终的hash值 */
for (i = 0; i < (ngx_uint_t) iphp->addrlen; i++)
hash = (hash * 113 + iphp->addr[i]) % 6271;
/* total_weight和weight都是固定值 */
w = hash % iphp->rrp.peers->total_weight;
peer = iphp->rrp.peers->peer; /* 第一台后端 */
p = 0;
while (w >= peer->weight) {
w -= peer->weight;
peer = peer->next;
p++;
}
/* 检查第此后端在状态位图中对应的位,为1时表示不可用 */
n = p / (8 * sizeof(uintptr_t));
m = (uintptr_t) 1 << p % (8 * sizeof(uintptr_t));
if (iphp->rrp.tried[n] & m)
goto next;
/* 检查后端是否永久不可用 */
if (peer->down)
goto next;
/* 在一段时间内,如果此后端服务器的失败次数,超过了允许的最大值,那么不允许使用此后端了 */
if (peer->max_fails && peer->fails >= peer->max_fails &&
now - peer->checked <= peer->fail_timeout)
goto next;
break;
next:
/* 增加已尝试的次数,如果超过20次,则使用轮询的方式来选取后端 */
if (++iphp->tries > 20)
return iphp->get_rr_peer(pc, &iphp->rrp);
}
iphp->rrp.current = peer; /* 选定的可用后端 */
/* 保存选定的后端服务器的地址,之后会向这个地址发起连接 */
pc->sockaddr = peer->sockaddr;
pc->socklen = peer->socklen;
pc->name = &peer->name;
peer->conns++;
/* 更新checked时间 */
if (now - peer->checked > peer->fail_timeout)
peer->checked = now;
iphp->rrp.tried[n] |= m; /* 对于此请求,如果之后需要再次选取后端,不能再选取这个后端了 */
iphp->hash = hash; /* 保存hash值,下次可能还会用到 */
return NGX_OK:
}
~~~
加权轮询 (Weighted Round Robin) 下篇
最后更新于:2022-04-01 10:13:41
Nginx版本:1.9.1
我的博客:[http://blog.csdn.net/zhangskd](http://blog.csdn.net/zhangskd)
上篇blog讲述了加权轮询算法的原理、以及负载均衡模块中使用的数据结构,接着我们来看看加权轮询算法的具体实现。
**指令的解析函数**
如果upstream配置块中没有指定使用哪种负载均衡算法,那么默认使用加权轮询。
也就是说使用加权轮询算法,并不需要特定的指令,因此也不需要实现指令的解析函数。
而实际上,和其它负载均衡算法不同(比如ip_hash),加权轮询算法并不是以模块的方式实现的,
而是作为Nginx框架的一部分。
**初始化upstream块**
在执行ngx_http_upstream_module的init main conf函数时,会遍历所有upstream配置块,调用它们
事先指定的初始化函数。对于一个upstream配置块,如果没有指定初始化函数,则调用加权轮询算法
提供的upstream块初始化函数 - ngx_http_upstream_init_round_robin。
来看下ngx_http_upstream_module。
~~~
ngx_http_module_t ngx_http_upstream_module_ctx = {
...
ngx_http_upstream_init_main_conf, /* init main configuration */
...
};
~~~
~~~
static char *ngx_http_upstream_init_main_conf(ngx_conf_t *cf, void *conf)
{
...
/* 数组的元素类型是ngx_http_upstream_srv_conf_t */
for (i = 0; i < umcf->upstreams.nelts; i++) {
/* 如果没有指定upstream块的初始化函数,默认使用round robin的 */
init = uscfp[i]->peer.init_upstream ? uscfp[i]->peer.init_upstream :
ngx_http_upstream_init_round_robin;
if (init(cf, uscfp[i] != NGX_OK) {
return NGX_CONF_ERROR;
}
}
...
}
~~~
ngx_http_upstream_init_round_robin做的工作很简单:
指定请求的负载均衡初始化函数,用于初始化per request的负载均衡数据。
创建和初始化后端集群、备份集群。
~~~
ngx_int_t ngx_http_upstream_init_round_robin (ngx_conf_t *cf, ngx_http_upstream_srv_conf_t *us)
{
ngx_url_t u;
ngx_uint_t i, j, n, w;
ngx_http_upstream_server_t *server;
ngx_http_upstream_rr_peer_t *peer, **peerp;
ngx_http_upstream_rr_peers_t *peers, *backup;
/* 指定请求的负载均衡初始化函数,用于初始化per request的负载均衡数据 */
us->peer.init = ngx_http_upstream_init_round_robin_peer;
/* upstream配置块的servers数组,在解析配置文件时就创建好了 */
if (us->servers) {
server = us->servers->elts;
n = 0;
w = 0;
/* 数组元素类型为ngx_http_upstream_server_t,对应一条server指令 */
for (i = 0; i < us->servers->nelts; i++) {
if (server[i].backup)
continue;
n += server[i].naddrs; /* 所有后端服务器的数量 */
w += server[i].naddrs * server[i].weight; /* 所有后端服务器的权重之和 */
}
if (n == 0) { /* 至少得有一台后端吧 */
...
return NGX_ERROR;
}
/* 创建一个后端集群的实例 */
peers = ngx_pcalloc(cf->pool, sizeof(ngx_http_upstream_rr_peers_t));
...
/* 创建后端服务器的实例,总共有n台 */
peer = ngx_pcalloc(cf->pool, sizeof(ngx_http_upstream_rr_peer_t) * n);
...
/* 初始化集群 */
peers->single = (n == 1); /* 是否只有一台后端 */
peers->number = n; /* 后端服务器的数量 */
peers->weight = (w != n); /* 是否使用权重 */
peers->total_weight = w; /* 所有后端服务器权重的累加值 */
peers->name = &us->host; /* upstream配置块的名称 */
n = 0;
peerp = &peers->peer;
/* 初始化代表后端的结构体ngx_http_upstream_peer_t.
* server指令后跟的是域名的话,可能对应多台后端.
*/
for(i = 0; i < us->servers->nelts; i++) {
if (server[i].backup)
continue;
for (j = 0; j < server[i].naddrs; j++) {
peer[n].sockaddr = server[i].addrs[j].sockaddr; /* 后端服务器的地址 */
peer[n].socklen = server[i].addrs[j].socklen; /* 地址的长度*/
peer[n].name = server[i].addrs[j].name; /* 后端服务器地址的字符串 */
peer[n].weight = server[i].weight; /* 配置项指定的权重,固定值 */
peer[n].effective_weight = server[i].weight; /* 有效的权重,会因为失败而降低 */
peer[n].current_weight = 0; /* 当前的权重,动态调整,初始值为0 */
peer[n].max_fails = server[i].max_fails; /* "一段时间内",最大的失败次数,固定值 */
peer[n].fail_timeout = server[i].fail_timeout; /* "一段时间"的值,固定值 */
peer[n].down = server[i].down; /* 服务器永久不可用的标志 */
peer[n].server = server[i].name; /* server的名称 */
/* 把后端服务器组成一个链表,第一个后端的地址保存在peers->peer */
*peerp = &peer[n];
peerp = &peer[n].next;
n++;
}
}
us->peer.data = peers; /* 保存后端集群的地址 */
}
/* backup servers */
/* 创建和初始化备份集群,peers->next指向备份集群,和上述流程类似,不再赘述 */
...
/* an upstream implicitly defined by proxy_pass, etc. */
/* 如果直接使用proxy_pass指令,没有定义upstream配置块 */
if (us->port == 0) {
...
return NGX_ERROR;
}
ngx_memzero(&u, sizeof(ngx_url_t));
u.host = us->host;
u.port = us->port;
/* 根据URL解析域名 */
if (ngx_inet_resolve_host(cf->pool, &u) != NGX_OK) {
...
return NGX_ERROR;
}
n = u.naddrs; /* 共有n个后端 */
/* 接下来创建后端集群,并进行初始化,和上述流程类似,这里不再赘述 */
...
return NGX_OK;
}
~~~
**初始化请求的负载均衡数据**
当收到一个请求后,一般使用的反向代理模块(upstream模块)为ngx_http_proxy_module,
其NGX_HTTP_CONTENT_PHASE阶段的处理函数为ngx_http_proxy_handler,在初始化upstream机制的
函数ngx_http_upstream_init_request中,调用在第二步中指定的peer.init,主要用于:
创建和初始化该请求的负载均衡数据块
指定r->upstream->**peer.get**,用于从集群中选取一台后端服务器(这是我们最为关心的)
指定r->upstream->**peer.free**,当不用该后端时,进行数据的更新(不管成功或失败都调用)
指定r->upstream->peer.tries,请求最多允许尝试这么多个后端
~~~
ngx_int_t ngx_http_upstream_init_round_robin_peer(ngx_http_request_t *r, ngx_http_upstream_srv_conf_t *us)
{
ngx_uint_t n;
ngx_http_upstream_rr_peer_data_t *rrp;
/* 创建请求的负载均衡数据块 */
rrp = r->upstream->peer.data;
if (rrp == NULL) {
rrp = ngx_palloc(r->pool, sizeof(ngx_http_upstream_rr_peer_data_t));
if (rrp == NULL)
return NGX_ERROR;
r->upstream->peer.data = rrp; /* 保存请求负载均衡数据的地址 */
}
rrp->peers = us->peer.data; /* upstream块的后端集群 */
rrp->current = NULL;
n = rrp->peers->number; /* 后端的数量 */
/* 如果存在备份集群,且其服务器数量超过n */
if (rrp->peers->next && rrp->peers->next->number > n) {
n = rrp->peers->next->number;
}
/* rrp->tried指向后端服务器的位图,每一位代表一台后端的状态,0表示可用,1表示不可用。
* 如果后端数较少,直接使用rrp->data作为位图。如果后端数较多,则需要申请一块内存。
*/
if (n <= 8 *sizeof(uintptr_t)) {
rrp->tried = &rrp->data;
rrp->data = 0;
} else {
n = ( n + (8 * sizeof(uintptr_t) - 1)) / (8 * sizeof(uintptr_t)); /* 向上取整 */
rrp->tried = ngx_pcalloc(r->pool, n * sizeof(uintptr_t));
if (rrp->tried == NULL) {
return NGX_ERROR;
}
}
r->upstream->peer.get = ngx_http_upstream_get_round_robin_peer; /* 指定peer.get,用于从集群中选取一台后端服务器 */
r->upstream->peer.free = ngx_http_upstream_free_round_robin_peer; /* 指定peer.free,当不用该后端时,进行数据的更新 */
r->upstream->peer.tries = ngx_http_upstream_tries(rrp->peers); /* 指定peer.tries,是请求允许尝试的后端服务器个数 */
...
return NGX_OK;
}
#define ngx_http_upstream_tries(p) ((p)->number + ((p)->next ? (p)->next->number : 0))
~~~
**选取一台后端服务器**
一般upstream块中会有多台后端,那么对于本次请求,要选定哪一台后端呢?
这时候第三步中r->upstream->peer.get指向的函数就派上用场了:
采用加权轮询算法,从集群中选出一台后端来处理本次请求。 选定后端的地址保存在pc->sockaddr,pc为主动连接。
函数的返回值:
NGX_DONE:选定一个后端,和该后端的连接已经建立。之后会直接发送请求。
NGX_OK:选定一个后端,和该后端的连接尚未建立。之后会和后端建立连接。
NGX_BUSY:所有的后端(包括备份集群)都不可用。之后会给客户端发送502(Bad Gateway)。
~~~
ngx_int_t ngx_http_upstream_get_round_robin_peer(ngx_peer_connection_t *pc, void *data)
{
ngx_http_upstream_rr_peer_data_t *rrp = data; /* 请求的负载均衡数据 */
ngx_int_t rc;
ngx_uint_t i, n;
ngx_http_upstream_rr_peer_t *peer;
ngx_http_upstream_rr_peers_t *peers;
...
pc->cached = 0;
pc->connection = NULL;
peers = rrp->peers; /* 后端集群 */
...
/* 如果只有一台后端,那就不用选了 */
if (peers->single) {
peer = peers->peer;
if (peer->down)
goto failed;
rrp->current = peer;
} else {
/* there are several peers */
/* 调用ngx_http_upstream_get_peer来从后端集群中选定一台后端服务器 */
peer = ngx_http_upstream_get_peer(rrp);
if (peer == NULL)
goto failed;
...
}
/* 保存选定的后端服务器的地址,之后会向这个地址发起连接 */
pc->sockaddr = peer->sockaddr;
pc->socklen = peer->socklen;
pc->name = &peer->name;
peer->conns++; /* 增加选定后端的当前连接数 */
...
return NGX_OK;
failed:
/* 如果不能从集群中选取一台后端,那么尝试备用集群 */
if (peers->next) {
...
rrp->peers = peers->next;
n = (rrp->peers->number + (8 * sizeof(uintptr_t) - 1))
/ (8 * sizeof(uintptr_t));
for (i = 0; i < n; i++)
rrp->tried[i] = 0;
/* 重新调用本函数 */
rc = ngx_http_upstream_get_round_robin_peer(pc, rrp);
if (rc != NGX_BUSY)
return rc;
}
/* all peers failed, mark them as live for quick recovery */
for (peer = peers->peer; peer; peer = peer->next) {
peer->fails = 0;
}
pc->name = peers->name;
return NGX_BUSY;
}
~~~
ngx_http_upstream_get_peer用于从集群中选取一台后端服务器。
~~~
static ngx_http_upstream_rr_peer_t *ngx_http_upstream_get_peer(ngx_http_upstream_rr_peer_data_t *rrp)
{
time_t now;
uintptr_t m;
ngx_int_t total;
ngx_uint_t i, n, p;
ngx_http_upstream_rr_peer_t *peer, *best;
now = ngx_time();
best = NULL;
total = 0;
...
/* 遍历集群中的所有后端 */
for (peer = rrp->peers->peer, i = 0;
peer;
peer = peer->next, i++)
{
n = i / (8 * sizeof(uintptr_t));
m = (uintptr_t) 1 << i % (8 * sizeof(uintptr_t));
/* 检查该后端服务器在位图中对应的位,为1时表示不可用 */
if (rrp->tried[n] & m)
continue;
/* 永久不可用的标志 */
if (peer->down)
continue;
/* 在一段时间内,如果此后端服务器的失败次数,超过了允许的最大值,那么不允许使用此后端了 */
if (peer->max_fails
&& peer->fails >= peer->max_fails
&& now - peer->checked <= peer->fail_timeout)
continue;
peer->current_weight += peer->effective_weight; /* 对每个后端,增加其当前权重 */
total += peer->effective_weight; /* 累加所有后端的有效权重 */
/* 如果之前此后端发生了失败,会减小其effective_weight来降低它的权重。
* 此后在选取后端的过程中,又通过增加其effective_weight来恢复它的权重。
*/
if (peer->effective_weight < peer->weight)
peer->effective_weight++;
/* 选取当前权重最大者,作为本次选定的后端 */
if (best == NULL || peer->current_weight > best->current_weight) {
best = peer;
p = i;
}
}
if (best == NULL) /* 没有可用的后端 */
return NULL;
rrp->current = best; /* 保存本次选定的后端 */
n = p / (8 * sizeof(uintptr_t));
m = (uintptr_t) 1 << p % (8 * sizeof(uintptr_t));
/* 对于本次请求,如果之后需要再次选取后端,不能再选取这个后端了 */
rrp->tried[n] |= m;
best->current_weight -= total; /* 选定后端后,需要降低其当前权重 */
/* 更新checked时间 */
if (now - best->checked > best->fail_timeout)
best->checked = now;
return best;
}
~~~
**释放一台后端服务器**
当不再使用一台后端时,需要进行收尾处理,比如统计失败的次数。
这时候会调用第三步中r->upstream->peer.get指向的函数。函数参数state的取值:
0,请求被成功处理
NGX_PEER_FAILED,连接失败
NGX_PEER_NEXT,连接失败,或者连接成功但后端未能成功处理请求
一个请求允许尝试的后端数为pc->tries,在第三步中指定。当state为后两个值时:
如果pc->tries不为0,需要重新选取一个后端,继续尝试,此后会重复调用r->upstream->peer.get。
如果pc->tries为0,便不再尝试,给客户端返回502错误码(Bad Gateway)。
~~~
void ngx_http_upstream_free_round_robin_peer(ngx_peer_connection_t *pc, void *data,
ngx_uint_t state)
{
ngx_http_upstream_rr_peer_data_t *rrp = data; /* 请求的负载均衡数据 */
time_t now;
ngx_http_upstream_rr_peer_t *peer;
...
peer = rrp->current; /* 当前使用的后端服务器 */
if (rrp->peers->single) {
peer->conns--; /* 减少后端的当前连接数 */
pc->tries = 0; /* 不能再继续尝试了 */
return;
}
/* 如果连接后端失败了 */
if (state & NGX_PEER_FAILED) {
now = ngx_time();
peer->fails++; /* 一段时间内,已经失败的次数 */
peer->accessed = now; /* 最近一次失败的时间点 */
peer->checked = now; /* 用于检查是否超过了“一段时间” */
/* 当后端出错时,降低其有效权重 */
if (peer->max_fails)
peer->effective_weight -= peer->weight / peer->max_fails;
/* 有效权重的最小值为0 */
if (peer->effective_weight < 0)
peer->effective_weight = 0;
} else {
/* mark peer live if check passed */
/* 说明距离最后一次失败的时间点,已超过fail_timeout了,清零fails */
if (peer->accessed < peer->checked)
peer->fails = 0;
}
peer->conns--; /* 更新后端的当前连接数 */
if (pc->tries)
pc->tries--; /* 对于一个请求,允许尝试的后端个数 */
}
~~~
**判断后端是否可用**
**相关的变量的定义**
ngx_uint_t fails; /* 一段时间内,已经失败的次数 */
time_t accessed; /* 最近一次失败的时间点 */
time_t checked; /* 用于检查是否超过了“一段时间” */
ngx_uint_t max_fails; /* 一段时间内,允许的最大的失败次数,固定值 */
time_t fail_timeout; /* “一段时间”的长度,固定值 */
~~~
ngx_http_upstream_get_peeer
/* 在一段时间内,如果此后端服务器的失败次数,超过了允许的最大值,
* 那么在此后的一段时间内不允许使用此后端了。
*/
if (peer->max_fails && peer->fails >= peer->max_fails &&
now - peer->checked <= peer->fail_timeout)
continue;
...
/* 选定本后端了 */
if (now - best->checked > best->fail_timeout)
best->checked = now;
~~~
~~~
ngx_http_upstream_free_round_robin_peer
if (state & NGX_PEER_FAILED) {
peer->fails++;
peer->accessed = now;
peer->checked = now;
...
} else if (peer->accessed < peer->checked)
peer->fails = 0;
~~~
相关变量的更新
accessed:释放peer时,如果发现后端出错了,则更新为now。
checked:释放peer时,如果发现后端出错了,则更新为now。选定该peer时,如果now - checked > fail_timeout,则更新为now。
fails:释放peer时,如果本次成功了且accessed < checked,说明距离最后一次失败的时间点,已超过fail_timeout了,清零fails。
**上述变量的准备定义**
fails并不是“一段时间内”的失败次数,而是两两间时间间隔小于“一段时间”的连续失败次数。
max_fails也不是“一段时间内”允许的最大失败次数,而是两两间的时间间隔小于“一段时间”的最大失败次数。
举例说明,假设fail_timeout为10s,max_fails为3。
10s内失败3次,肯定会导致接下来的10s不可用。
27s内失败3次,也可能导致接下来的10s不可用,只要3次失败两两之间的时间间隔为9s。
下图用来简要说明
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-22_56f0c8a54d085.jpg)
加权轮询 (Weighted Round Robin) 上篇
最后更新于:2022-04-01 10:13:38
Nginx版本:1.9.1
我的博客:[http://blog.csdn.net/zhangskd](http://blog.csdn.net/zhangskd)
**算法介绍**
来看一个简单的Nginx负载均衡配置。
~~~
http {
upstream cluster {
server a weight=5;
server b weight=1;
server c weight=1;
}
server {
listen 80;
location / {
proxy_pass http://cluster;
}
}
}
~~~
当在upstream配置块中没有指定使用的负载均衡算法时,默认使用的是加权轮询。
按照上述配置,Nginx每收到7个客户端的请求,会把其中的5个转发给后端a,把其中的1个转发给后端b,
把其中的1个转发给后端c。
这就是所谓的加权轮询,看起来很简单,但是最早使用的加权轮询算法有个问题,就是7个请求对应的
后端序列是这样的:{ c, b, a, a, a, a, a },会有5个连续的请求落在后端a上,分布不太均匀。
目前使用的加权轮询叫做平滑的加权轮询(smooth weighted round-robin balancing),它和前者的区别是:
每7个请求对应的后端序列为 { a, a, b, a, c, a, a },转发给后端a的5个请求现在分散开来,不再是连续的。
摘录此算法的描述:
On each peer selection we increase current_weight of each eligible peer by its weight,
select peer with greatest current_weight and reduce its current_weight by total number
of weight points distributed among peers.
To preserve weight reduction in case of failures the effective_weight variable was introduced,
which usually matches peer's weight, but is reduced temoprarily on peer failures.[1]
每个后端peer都有三个权重变量,先解释下它们的含义。
**(1) weight**
配置文件中指定的该后端的权重,这个值是固定不变的。
**(2) effective_weight**
后端的有效权重,初始值为weight。
在释放后端时,如果发现和后端的通信过程中发生了错误,就减小effective_weight。
此后有新的请求过来时,在选取后端的过程中,再逐步增加effective_weight,最终又恢复到weight。
之所以增加这个字段,是为了当后端发生错误时,降低其权重。
**(3) current_weight**
后端目前的权重,一开始为0,之后会动态调整。那么是怎么个动态调整呢?
每次选取后端时,会遍历集群中所有后端,对于每个后端,让它的current_weight增加它的effective_weight,
同时累加所有后端的effective_weight,保存为total。
如果该后端的current_weight是最大的,就选定这个后端,然后把它的current_weight减去total。
如果该后端没有被选定,那么current_weight不用减小。
弄清了三个weight字段的含义后,加权轮询算法可描述为:
1. 对于每个请求,遍历集群中的所有可用后端,对于每个后端peer执行:
peer->current_weight += peer->effecitve_weight。
同时累加所有peer的effective_weight,保存为total。
2. 从集群中选出current_weight最大的peer,作为本次选定的后端。
3. 对于本次选定的后端,执行:peer->current_weight -= total。
上述描述可能不太直观,来看个例子。
现在使用以下的upstream配置块:
upstream backend {
server a weight=4;
server b weight=2;
server c weight=1;
}
按照这个配置,每7个客户端请求中,a会被选中4次、b会被选中2次、c会被选中1次,且分布平滑。
我们来算算看是不是这样子的。
initial current_weight of a, b, c is { 0, 0, 0 }
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-22_56f0c8a51631d.jpg)
通过上述过程,可得以下结论:
1. 7个请求中,a、b、c分别被选取了4、2、1次,符合它们的权重值。
2. 7个请求中,a、b、c被选取的顺序为a, b, a, c, a, b, a,分布均匀,权重大的后端a没有被连续选取。
3. 每经过7个请求后,a、b、c的current_weight又回到初始值{ 0, 0, 0 },因此上述流程是不断循环的。
这个平滑的加权轮询算法背后应该有数学论证,这里就不继续研究了:)
**本模块的数据结构**
ngx_http_upstream_rr_peer_t
表示一台后端服务器。peer就是对端,指的是上游服务器端。
~~~
struct ngx_http_upstream_rr_peer_s {
struct sockaddr *sockaddr; /* 后端服务器的地址 */
socklen_t socklen; /* 地址的长度*/
ngx_str_t name; /* 后端服务器地址的字符串,server.addrs[i].name */
ngx_str_t server; /* server的名称,server.name */
ngx_int_t current_weight; /* 当前的权重,动态调整,初始值为0 */
ngx_int_t effective_weight; /* 有效的权重,会因为失败而降低 */
ngx_int_t weight; /* 配置项指定的权重,固定值 */
ngx_uint_t conns; /* 当前连接数 */
ngx_uint_t fails; /* "一段时间内",已经失败的次数 */
time_t accessed; /* 最近一次失败的时间点 */
time_t checked; /* 用于检查是否超过了"一段时间" */
ngx_uint_t max_fails; /* "一段时间内",最大的失败次数,固定值 */
time_t fail_timeout; /* "一段时间"的值,固定值 */
ngx_uint_t down; /* 服务器永久不可用的标志 */
...
ngx_http_upstream_rr_peer_t *next; /* 指向下一个后端,用于构成链表 */
...
} ngx_http_upstream_rr_peer_t;
~~~
ngx_http_upstream_rr_peers_t
表示一组后端服务器,比如一个后端集群。
~~~
struct ngx_http_upstream_rr_peers_s {
ngx_uint_t number; /* 后端服务器的数量 */
...
ngx_uint_t total_weight; /* 所有后端服务器权重的累加值 */
unsigned single:1; /* 是否只有一台后端服务器 */
unsigned weighted:1; /* 是否使用权重 */
ngx_str_t *name; /* upstream配置块的名称 */
ngx_http_upstream_rr_peers_t *next; /* backup服务器集群 */
ngx_http_upstream_rr_peer_t *peer; /* 后端服务器组成的链表 */
};
~~~
ngx_http_upstream_rr_peer_data_t
保存每个请求的负载均衡数据。
~~~
typedef struct {
ngx_http_upstream_rr_peers_t *peers; /* 后端集群 */
ngx_http_upstream_rr_peer_t *current; /* 当前使用的后端服务器 */
uintptr_t *tried; /* 指向后端服务器的位图 */
uintptr_t data; /* 当后端服务器的数量较少时,用于存放其位图 */
} ngx_http_upstream_rr_peer_data_t;
~~~
**通用的数据结构**
以下是所有负载均衡模块都会使用到的一些数据结构。
ngx_http_upstream_server_t
表示upstream配置块中的一条server指令。
~~~
typedef struct {
ngx_str_t name; /* 服务器的名称 */
ngx_addr_t *addrs; /* 服务器地址的数组,因为同一个域名可能解析为多个IP */
ngx_uint_t naddrs; /* 服务器地址数组的元素个数 */
ngx_uint_t weight; /* 服务器的权重 */
ngx_uint_t max_fails; /* 一段时间内,访问失败的次数超过此值,判定服务器不可用 */
time_t fail_timeout; /* 上述“一段时间”的长度 */
unsigned down:1; /* 服务器不可用的标志 */
unsigned backup:1; /* 服务器为备用的标志 */
} ngx_http_upstream_server_t;
~~~
**server指令**
Syntax: server address [parameters];
Context: upstream
Defines the address and other parameters of a server.
The address can be specified as domain name or IP address, with an optional port, or...
If a port is not specified, the port 80 is used. A domain name that resolves to serveral IP
addresses defines multiple servers at once.
**server指令支持如下参数**
*weight* = number
sets the weight of the server, by default 1.
*max_fails* = number
By default, the number of unsuccessful attempts is set to 1.
The zero value disables the accounting of attempts.
*fail_timout* = number
By default it is set to 10 seconds.
*backup*
marks the server as a backup server.
*down*
marks the server as permanently unavailable.
ngx_peer_connection_t
表示本机和后端的连接,也叫主动连接,用于upstream机制。
~~~
struct ngx_peer_connection_s {
ngx_connection_t *connection; /* 后端连接 */
struct sockaddr *sockaddr; /* 后端服务器的地址 */
socklen_t socklen; /* 后端服务器地址的长度 */
ngx_str_t *name; /* 后端服务器的名称 */
ngx_uint_t tries; /* 对于一个请求,允许尝试的后端服务器个数 */
ngx_event_get_peer_pt get; /* 负载均衡模块实现,用于选取一个后端服务器 */
ngx_event_free_peer_pt free; /* 负载均衡模块实现,用于释放一个后端服务器 */
void *data; /* 请求的负载均衡数据,一般指向ngx_http_upstream_<name>_peer_data_t */
...
ngx_addr_t *local; /* 本机地址 */
int rcvbuf; /* 套接字接收缓冲区的大小 */
ngx_log_t *log;
unsigned cached:1;
unsigned log_error:2;
};
~~~
ngx_peer_connection_t *pc;
pc->get 就是负载均衡模块中,用于选取后端服务器的函数。
当选定一台后端服务器时,把它的地址信息保存在pc->sockaddr、pc->socklen、pc->name。
pc->tries表示对于一个请求,最多能尝试多少个后端。当尝试一个后端失败时,会调用pc->free,
一个主要目的就是更新pc->tries,比如pc->tries--。如果pc->tries降到0,就不再尝试了。
在请求的负载均衡数据初始化函数peer.init中,会给该请求创建一个ngx_http_upstream_<name>_peer_data_t实例,
用于保存该请求的负载均衡数据,pc->data就是该实例的地址。
**ngx_http_upstream_peer_t**
保存upstream块的数据,是负载均衡中一个很重要的结构体。
~~~
typedef struct {
/* upstream块的初始化函数,ngx_http_upstream_module创建main配置时调用。
* 针对每个upstream块。
*/
ngx_http_upstream_init_pt init_upstream;
/* request在初始化upstream机制时调用,初始化该请求的负载均衡数据。
* 针对每个request。
*/
ngx_http_upstream_init_peer_pt init;
void *data; /* 保存upstream块的数据 */
} ngx_http_upstream_peer_t;
~~~
upstream块的数据,在解析配置文件时就创建和初始化了。
如果写了一个新的负载均衡模块,则需要在它的指令解析函数中指定init_upstream的值,
用来创建和初始化包含该指令的upstream配置块的数据。
**ngx_http_upstream_srv_conf_t**
ngx_http_upstream_module的server块。
~~~
struct ngx_http_upstream_srv_conf_s {
ngx_http_upstream_peer_t peer; /* upstream块的数据 */
void **srv_conf; /* 所有HTTP模块的server conf */
ngx_array_t *server; /* upstream块的server数组,元素类型为ngx_http_upstream_server_t */
ngx_uint_t flags; /* upstream块的server指令支持的参数 */
ngx_str_t host; /* upstream块的名称 */
u_char *file_name;
ngx_uint_t line;
in_port_t port; /* 使用的端口 */
in_port_t default_port; /* 默认的端口 */
ngx_uint_t no_port;
...
};
#define ngx_http_conf_upstream_srv_conf(uscf, module) uscf->srv_conf[module.ctx_index]
~~~
**Reference**
[1].[https://github.com/phusion/nginx/commit/27e94984486058d73157038f7950a0a36ecc6e35](https://github.com/phusion/nginx/commit/27e94984486058d73157038f7950a0a36ecc6e35)
整体架构
最后更新于:2022-04-01 10:13:36
Nginx版本:1.9.1
我的博客:[http://blog.csdn.net/zhangskd](http://blog.csdn.net/zhangskd)
**什么是负载均衡**
我们知道单台服务器的性能是有上限的,当流量很大时,就需要使用多台服务器来共同提供服务,这就是所谓的集群。
负载均衡服务器,就是用来把经过它的流量,按照某种方法,分配到集群中的各台服务器上。这样一来不仅可以承担
更大的流量、降低服务的延迟,还可以避免单点故障造成服务不可用。一般的反向代理服务器,都具备负载均衡的功能。
负载均衡功能可以由硬件来提供,比如以前的F5设备。也可以由软件来提供,LVS可以提供四层的负载均衡(利用IP和端口),
Haproxy和Nginx可以提供七层的负载均衡(利用应用层信息)。
来看一个最简单的Nginx负载均衡配置。
~~~
http {
upstream cluster {
server srv1;
server srv2;
server srv3;
}
server {
listen 80;
location / {
proxy_pass http://cluster;
}
}
}
~~~
通过上述配置,Nginx会作为HTTP反向代理,把访问本机的HTTP请求,均分到后端集群的3台服务器上。
此时使用的HTTP反向代理模块是ngx_http_proxy_module。
一般在upstream配置块中要指明使用的负载均衡算法,比如hash、ip_hash、least_conn。
这里没有指定,所以使用了默认的HTTP负载均衡算法 - 加权轮询。
**负载均衡流程图**
在描述负载均衡模块的具体实现前,先来看下它的大致流程:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-22_56f0c89f13acb.jpg)
**负载均衡模块**
Nginx目前提供的负载均衡模块:
ngx_http_upstream_round_robin,加权轮询,可均分请求,是默认的HTTP负载均衡算法,集成在框架中。
ngx_http_upstream_ip_hash_module,IP哈希,可保持会话。
ngx_http_upstream_least_conn_module,最少连接数,可均分连接。
ngx_http_upstream_hash_module,一致性哈希,可减少缓存数据的失效。
以上负载均衡模块的实现,基本上都遵循一套相似的流程。
**1. 指令的解析函数**
比如least_conn、ip_hash、hash指令的解析函数。
这些函数在解析配置文件时调用,主要用于:
检查指令参数的合法性
指定peer.init_upstream函数指针的值,此函数用于初始化upstream块。
**2. 初始化upstream块**
在执行完指令的解析函数后,紧接着会调用所有HTTP模块的init main conf函数。
在执行ngx_http_upstream_module的init main conf函数时,会调用所有upstream块的初始化函数,
即在第一步中指定的peer.init_upstream,主要用于:
创建和初始化后端集群,保存该upstream块的数据
指定peer.init,此函数用于初始化请求的负载均衡数据
来看下ngx_http_upstream_module。
~~~
ngx_http_module_t ngx_http_upstream_module_ctx = {
...
ngx_http_upstream_init_main_conf, /* init main configuration */
...
};
~~~
~~~
static char *ngx_http_upstream_init_main_conf(ngx_conf_t *cf, void *conf)
{
...
/* 数组的元素类型是ngx_http_upstream_srv_conf_t */
for (i = 0; i < umcf->upstreams.nelts; i++) {
/* 如果没有指定upstream块的初始化函数,默认使用round robin的 */
init = uscfp[i]->peer.init_upstream ? uscfp[i]->peer.init_upstream :
ngx_http_upstream_init_round_robin;
if (init(cf, uscfp[i] != NGX_OK) {
return NGX_CONF_ERROR;
}
}
...
}
~~~
**3. 初始化请求的负载均衡数据块**
当收到一个请求后,一般使用的反向代理模块(upstream模块)为ngx_http_proxy_module,
其NGX_HTTP_CONTENT_PHASE阶段的处理函数为ngx_http_proxy_handler,在初始化upstream机制的
函数ngx_http_upstream_init_request中,调用在第二步中指定的peer.init,主要用于:
创建和初始化该请求的负载均衡数据块
指定r->upstream->peer.get,用于从集群中选取一台后端服务器(这是我们最为关心的)
指定r->upstream->peer.free,当不用该后端时,进行数据的更新(不管成功或失败都调用)
请求的负载均衡数据块中,一般会有一个成员指向对应upstream块的数据,除此之外还会有自己独有的成员。
"The peer initialization function is called once per request.
It sets up a data structure that the module will use as it tries to find an appropriate
backend server to service that request; this structure is persistent across backend re-tries,
so it's a convenient place to keep track of the number of connection failures, or a computed
hash value. By convention, this struct is called ngx_http_upstream_<module_name>_peer_data_t."
**4. 选取一台后端服务器**
一般upstream块中会有多台后端,那么对于本次请求,要选定哪一台后端呢?
这时候第三步中r->upstream->peer.get指向的函数就派上用场了:
采用特定的算法,比如加权轮询或一致性哈希,从集群中选出一台后端,处理本次请求。
选定后端的地址保存在pc->sockaddr,pc为主动连接。
函数的返回值:
NGX_DONE:选定一个后端,和该后端的连接已经建立。之后会直接发送请求。
NGX_OK:选定一个后端,和该后端的连接尚未建立。之后会和后端建立连接。
NGX_BUSY:所有的后端(包括备份集群)都不可用。之后会给客户端发送502(Bad Gateway)。
**5. 释放一台后端服务器**
当不再使用一台后端时,需要进行收尾处理,比如统计失败的次数。
这时候会调用第三步中r->upstream->peer.get指向的函数。
函数参数state的取值:
0,请求被成功处理
NGX_PEER_FAILED,连接失败
NGX_PEER_NEXT,连接失败,或者连接成功但后端未能成功处理请求
一个请求允许尝试的后端数为pc->tries,在第三步中指定。当state为后两个值时:
如果pc->tries不为0,需要重新选取一个后端,继续尝试,此后会重复调用r->upstream->peer.get。
如果pc->tries为0,便不再尝试,给客户端返回502错误码(Bad Gateway)。
**在upstream模块的回调**
负载均衡模块的功能是从后端集群中选取一台后端服务器,而具体的反向代理功能是由upstream模块实现的,
比如和后端服务器建立连接、向后端服务器发送请求、处理后端服务器的响应等。
我们来看下负载均衡模块提供的几个钩子函数,是在upstream模块的什么地方回调的。
Nginx的HTTP反向代理模块为ngx_http_proxy_module,其NGX_HTTP_CONTENT_PHASE阶段的处理函数为
ngx_http_proxy_handler,每个请求的upstream机制是从这里开始的。
~~~
ngx_http_proxy_handler
ngx_http_upstream_create /* 创建请求的upstream实例 */
ngx_http_upstream_init /* 启动upstream机制 */
ngx_htp_upstream_init_request /* 负载均衡模块的入口 */
uscf->peer.init(r, uscf) /* 第三步,初始化请求的负载均衡数据块 */
...
ngx_http_upstream_connect /* 可能会被ngx_http_upstream_next重复调用 */
ngx_event_connect_peer(&u->peer); /* 连接后端 */
pc->get(pc, pc->data); /* 第四步,从集群中选取一台后端 */
...
/* 和后端建连成功后 */
c = u->peer.connection;
c->data = r;
c->write->handler = ngx_http_upstream_handler; /* 注册的连接的读事件处理函数 */
c->read->handler = ngx_http_upstream_handler; /* 注册的连接的写事件处理函数 */
u->write_event_handler = ngx_http_upstream_send_request_handler; /* 写事件的真正处理函数 */
u->read_event_handler = ngx_http_upstream_process_header; /* 读事件的真正处理函数 */
~~~
选定后端之后,在和后端通信的过程中如果发生了错误,会调用ngx_http_upstream_next来继续尝试其它的后端。
~~~
ngx_http_upstream_next
if (u->peer.sockaddr) {
if (ft_type == NGX_HTTP_UPSTREAM_FT_HTTP_403 ||
ft_type == NGX_HTTP_UPSTREAM_FT_HTTP_404)
state = NGX_PEER_NEXT;
else
state = NGX_PEER_FAILED;
/* 第五步,释放后端服务器 */
u->peer.free(&u->peer, u->peer.data, state);
u->peer.sockaddr = NULL;
}
~~~
**Reference**
[1]. [http://www.evanmiller.org/nginx-modules-guide.html#proxying](http://www.evanmiller.org/nginx-modules-guide.html#proxying)
[2]. [http://tengine.taobao.org/book/chapter_05.html#id5](http://tengine.taobao.org/book/chapter_05.html#id5)
前言
最后更新于:2022-04-01 10:13:34
> 原文出处:[Nginx开发](http://blog.csdn.net/column/details/sknginx.html)
作者:[zhangskd](http://blog.csdn.net/zhangskd)
**本系列文章经作者授权在看云整理发布,未经作者允许,请勿转载!**
# Nginx开发
> Nginx的开发和源码分析