之前有讨论了nginx的swrr算法的两个问题,并引出了阿里tengine的vnswrr算法如何来克服swrr的问题。本文通过源码层面对ngx_http_upstream_vnswrr_module模块进行分析,来深入理解vnswrr负载均衡算法。关于swrr算法的思考可以查看《nginx upstream server主动健康检测模块添加https检测功能》。关于vnswrr的算法原理可以参考《阿里七层流量入口负载均衡算法演变之路》。
配置指令的格式为:
指令: vnswrr [max_init=init_vode_num]
默认值: -
上下文: upstream
其中init_vnode_num是初始化虚拟节点的数量,具体可以参考《阿里七层流量入口负载均衡算法演变之路》中**接入层 VNSWRR 算法(V2)**部分的描述。
以5台rs服务器为例开启vnswrr,距离如下:
upstream {
vnswrr 5;
server 192.168.0.1 weight=1;
server 192.168.0.2 weight=1;
server 192.168.0.3 weight=3;
server 192.168.0.4 weight=3;
server 192.168.0.5 weight=5;
server 192.168.0.6 weight=5;
}
本模块定义了配置指令vnswrr,代码如下:
static ngx_command_t ngx_http_upstream_vnswrr_commands[] = {
{ ngx_string("vnswrr"),
NGX_HTTP_UPS_CONF|NGX_CONF_NOARGS|NGX_CONF_TAKE1,
ngx_http_upstream_vnswrr,
0,
0,
NULL },
ngx_null_command
};
以上定义了指令分析回调函数ngx_http_upstream_vnswrr, 其源码如下:
static char *
ngx_http_upstream_vnswrr(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
ngx_http_upstream_srv_conf_t *uscf;
ngx_http_upstream_vnswrr_srv_conf_t *uvnscf;
ngx_str_t *value;
ngx_int_t max_init;
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");
}
/* 将vnswrr的负载均衡算法配置初始化回调函数挂进去 */
uscf->peer.init_upstream = ngx_http_upstream_init_vnswrr;
/* 不象哈希负载均衡算法,本算法可以支持主备服务器 */
uscf->flags = NGX_HTTP_UPSTREAM_CREATE
|NGX_HTTP_UPSTREAM_WEIGHT
|NGX_HTTP_UPSTREAM_BACKUP
|NGX_HTTP_UPSTREAM_MAX_FAILS
|NGX_HTTP_UPSTREAM_FAIL_TIMEOUT
#if defined(nginx_version) && nginx_version >= 1011005
|NGX_HTTP_UPSTREAM_MAX_CONNS
#endif
|NGX_HTTP_UPSTREAM_DOWN;
/* 获取vnswrr的配置上下文 */
uvnscf = ngx_http_conf_upstream_srv_conf(uscf,
ngx_http_upstream_vnswrr_module);
value = cf->args->elts;
max_init = 0;
/* 如果有max_init参数,就从配置指令中解析初始虚拟节点数量 */
if (cf->args->nelts > 1) {
if (ngx_strncmp(value[1].data, "max_init=", 9) == 0) {
max_init = ngx_atoi(&value[1].data[9], value[1].len - 9);
if (max_init == NGX_ERROR) {
ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
"invalid parameter \"%V\"", &value[1]);
return NGX_CONF_ERROR;
}
}
}
uvnscf->max_init = max_init;
return NGX_CONF_OK;
}
nginx在解析完配置文件后,会为每个upstream调用前面设置好的init_upstream回调函数来初始化设置好的负载均衡算法,对于开启了vnswrr算法,则会回调ngx_http_upstream_init_vnswrr函数,该回调由3.1节中ngx_http_upstream_vnswrr函数设置。下面来分析一下ngx_http_upstream_init_vnswrr函数:
static ngx_int_t
ngx_http_upstream_init_vnswrr(ngx_conf_t *cf,
ngx_http_upstream_srv_conf_t *us)
{
ngx_http_upstream_rr_peers_t *peers, *backup;
ngx_http_upstream_vnswrr_srv_conf_t *uvnscf, *ubvnscf;
ngx_http_upstream_server_t *server;
ngx_uint_t i, g, bg, max_init;
ngx_log_debug0(NGX_LOG_DEBUG_HTTP, cf->log, 0, "init vnswrr");
/* 借用round-robin的ngx_http_upstream_init_round_robin初始化peer链表 */
if (ngx_http_upstream_init_round_robin(cf, us) != NGX_OK) {
return NGX_ERROR;
}
/* 对于配置的每个server(包括主和备),计算配置的所有server权重的最大公约数 */
g = 0;
bg = 0;
if (us->servers) {
server = us->servers->elts;
for (i = 0; i < us->servers->nelts; i++) {
if (server[i].backup) {
bg = ngx_http_upstream_gcd(bg, server[i].weight);
} else {
g = ngx_http_upstream_gcd(g , server[i].weight);
}
}
}
if (g == 0) {
g = 1;
}
if (bg == 0) {
bg = 1;
}
uvnscf = ngx_http_conf_upstream_srv_conf(us,
ngx_http_upstream_vnswrr_module);
if (uvnscf == NULL) {
return NGX_ERROR;
}
peers = (ngx_http_upstream_rr_peers_t *) us->peer.data;
max_init = uvnscf->max_init;
/* init_number为初始虚拟节点的序号
last_number为最后一次分配的虚拟节点的序号
last_peer为最后一次分配的peer的指针
*/
uvnscf->init_number = NGX_CONF_UNSET_UINT;
uvnscf->last_number = NGX_CONF_UNSET_UINT;
uvnscf->last_peer = NULL;
uvnscf->next = NULL;
uvnscf->gcd = g;
/* 如果没有配置max_init,则设置为peer的数量
max_init最大为总的权重
*/
if (!max_init) {
uvnscf->max_init = peers->number;
} else if (max_init > peers->total_weight) {
uvnscf->max_init = peers->total_weight;
}
/* 设置负载均衡请求上下文初始化回调函数 */
us->peer.init = ngx_http_upstream_init_vnswrr_peer;
/* 如果upstream是配置成带权重模式的,即所有服务器的weight不都等于1,则走正常vnswrr
算法,否则,退化为简单的round-robin算法。对于vnswrr,需要分配虚拟节点并进行初始化,
虚拟节点的数量是总权重除以上面算出的最大公约数。稍微思考一下,就知道这个是合理的,
譬如三台server,他们的权重都分别是2,4,6,那么其效果和1,2,3是一样的,
所以找到最大公约数,并把这个最大公约数除掉以后得到有效权重。
*/
if (peers->weighted) {
uvnscf->vpeers = ngx_pcalloc(cf->pool,
sizeof(ngx_http_upstream_rr_vpeers_t)
* peers->total_weight / uvnscf->gcd);
if (uvnscf->vpeers == NULL) {
return NGX_ERROR;
}
/* 初始化一批虚拟节点,最多是max_init个虚拟节点,避免一次性初始化大量的虚拟节点
当值nginx的cpu突发overload
*/
ngx_http_upstream_init_virtual_peers(peers, uvnscf, 0, uvnscf->max_init);
}
/* 下面是backup服务器部分的初始化逻辑,和主服务器是一样的 */
backup = peers->next;
if (backup) {
ubvnscf = ngx_pcalloc(cf->pool,
sizeof(ngx_http_upstream_vnswrr_srv_conf_t));
if (ubvnscf == NULL) {
return NGX_ERROR;
}
ubvnscf->init_number = NGX_CONF_UNSET_UINT;
ubvnscf->last_number = NGX_CONF_UNSET_UINT;
ubvnscf->last_peer = NULL;
ubvnscf->gcd = bg;
ubvnscf->max_init = max_init;
if (!max_init) {
ubvnscf->max_init = backup->number;
} else if (max_init > backup->total_weight) {
ubvnscf->max_init = backup->total_weight;
}
/* 把主服务器和backup服务器链起来 */
uvnscf->next = ubvnscf;
if (!backup->weighted) {
return NGX_OK;
}
ubvnscf->vpeers = ngx_pcalloc(cf->pool,
sizeof(ngx_http_upstream_rr_vpeers_t)
* backup->total_weight / ubvnscf->gcd);
if (ubvnscf->vpeers == NULL) {
return NGX_ERROR;
}
ngx_http_upstream_init_virtual_peers(backup, ubvnscf, 0,
ubvnscf->max_init);
}
return NGX_OK;
}
ngx_http_upstream_init_vnswrr函数的逻辑就是分别对主服务器和备服务器组进行加载操作,初始化一部分虚拟节点,详细的逻辑在源码中已经进行了注释,不再赘述。
当nginx接收到http请求需要连接上游服务器的时候,就会发起负载均衡请求上下文的初始化回调,对于vnswrr算法就是回调ngx_http_upstream_init_vnswrr_peer函数了。
static ngx_int_t
ngx_http_upstream_init_vnswrr_peer(ngx_http_request_t *r,
ngx_http_upstream_srv_conf_t *us)
{
ngx_http_upstream_vnswrr_srv_conf_t *uvnscf;
ngx_http_upstream_vnswrr_peer_data_t *vnsp;
uvnscf = ngx_http_conf_upstream_srv_conf(us,
ngx_http_upstream_vnswrr_module);
/* 创建请求上下文并进行初始化设置 */
vnsp = ngx_palloc(r->pool, sizeof(ngx_http_upstream_vnswrr_peer_data_t));
if (vnsp == NULL) {
return NGX_ERROR;
}
vnsp->uvnscf = uvnscf;
r->upstream->peer.data = &vnsp->rrp;
/* 因为本模块是依赖于round-robin模块的,譬如上游服务器的已分配状态等,
这里也需要调用ngx_http_upstream_init_round_robin_peer进行初始化 */
if (ngx_http_upstream_init_round_robin_peer(r, us) != NGX_OK) {
return NGX_ERROR;
}
/* 设置获取peer的回调 */
r->upstream->peer.get = ngx_http_upstream_get_vnswrr_peer;
return NGX_OK;
}
这里最关键的就是设置了获取peer的回调函数ngx_http_upstream_get_vnswrr_peer。
一切准备就绪后,nginx会在请求上游连接的时候调用ngx_event_connect_peer,而在ngx_event_connect_peer函数中将回调ngx_http_upstream_get_vnswrr_peer函数来获取目的服务器的地址信息。接下来来详细分析这个函数,源码如下:
static ngx_int_t
ngx_http_upstream_get_vnswrr_peer(ngx_peer_connection_t *pc, void *data)
{
ngx_http_upstream_vnswrr_peer_data_t *vnsp = data;
ngx_int_t rc;
ngx_uint_t i, n;
ngx_http_upstream_rr_peer_t *peer;
ngx_http_upstream_rr_peers_t *peers;
ngx_http_upstream_rr_peer_data_t *rrp;
ngx_log_debug1(NGX_LOG_DEBUG_HTTP, pc->log, 0,
"get vnswrr peer, try: %ui", pc->tries);
pc->cached = 0;
pc->connection = NULL;
rrp = &vnsp->rrp;
peers = rrp->peers;
ngx_http_upstream_rr_peers_wlock(peers); /* 共享内存加写锁 */
if (peers->single) {
/*对于只有一个peer的情况,如果这个peer没有down且连接数没有超过限制,
则直接分配这个peer*/
peer = peers->peer;
if (peer->down) {
goto failed;
}
#if defined(nginx_version) && nginx_version >= 1011005
if (peer->max_conns && peer->conns >= peer->max_conns) {
goto failed;
}
#endif
#if (NGX_HTTP_UPSTREAM_CHECK)
if (ngx_http_upstream_check_peer_down(peer->check_index)) {
goto failed;
}
#endif
rrp->current = peer;
} else {
/* 如果有多个peer,则调用ngx_http_upstream_get_vnswrr获取peer信息 */
peer = ngx_http_upstream_get_vnswrr(vnsp);
if (peer == NULL) {
goto failed;
}
ngx_log_debug2(NGX_LOG_DEBUG_HTTP, pc->log, 0,
"get vnswrr peer, current: %p %i",
peer, peer->current_weight);
}
/* 将分配到的peer的地址写入到ngx_peer_connection_t(pc_中 */
pc->sockaddr = peer->sockaddr;
pc->socklen = peer->socklen;
pc->name = &peer->name;
#if (T_NGX_HTTP_DYNAMIC_RESOLVE)
pc->host = &peer->host;
#endif
peer->conns++;
/* 释放上面加的写锁 */
ngx_http_upstream_rr_peers_unlock(peers);
return NGX_OK;
failed:
/* 主服务器分配失败了,如果有备服务器,那么从备服务器进行分配 */
if (peers->next) {
ngx_log_debug0(NGX_LOG_DEBUG_HTTP, pc->log, 0, "backup servers");
/* 切换到备服务器组 */
rrp->peers = peers->next;
vnsp->uvnscf = vnsp->uvnscf ? vnsp->uvnscf->next : vnsp->uvnscf;
n = (rrp->peers->number + (8 * sizeof(uintptr_t) - 1))
/ (8 * sizeof(uintptr_t));
for (i = 0; i < n; i++) {
rrp->tried[i] = 0;
}
/* 释放上面加的写锁 */
ngx_http_upstream_rr_peers_unlock(peers);
/* 递归调用本函数自己,重新进行一次获取peer的操作 */
rc = ngx_http_upstream_get_vnswrr_peer(pc, vnsp);
/* 备服务器也分配失败,则返回NGX_BUSY */
if (rc != NGX_BUSY) {
return rc;
}
/* 重新加上写锁,在返回前释放 */
ngx_http_upstream_rr_peers_wlock(peers);
}
/* 释放上面加的写锁 */
ngx_http_upstream_rr_peers_unlock(peers);
pc->name = peers->name;
return NGX_BUSY;
}
本函数针对如果只有一个peer的情况来说,就不需要再进行vnswrr算法了,反过来则进行vnswrr的分配操作,vnswrr算法调用了ngx_http_upstream_get_vnswrr函数进行实际的分配工作。下面就是vnswrr的最核心的代码了,源码如下:
static ngx_http_upstream_rr_peer_t *
ngx_http_upstream_get_vnswrr(ngx_http_upstream_vnswrr_peer_data_t *vnsp)
{
time_t now;
uintptr_t m;
ngx_uint_t i, n, p, flag, begin_number;
ngx_http_upstream_rr_peer_t *peer, *best;
ngx_http_upstream_rr_peers_t *peers;
ngx_http_upstream_rr_vpeers_t *vpeers;
ngx_http_upstream_rr_peer_data_t *rrp;
ngx_http_upstream_vnswrr_srv_conf_t *uvnscf;
now = ngx_time();
best = NULL;
#if (NGX_SUPPRESS_WARN)
p = 0;
#endif
rrp = &vnsp->rrp;
peers = rrp->peers;
uvnscf = vnsp->uvnscf;
vpeers = uvnscf->vpeers;
/* last_number == NGX_CONF_UNSET_UINT
表示本worker进程第一次进入到ngx_http_upstream_get_vnswrr函数,
这里通过将init_number设置为一个随机值来避免多进程产生的“共振”效应。
初始化随机值这个机制在《阿里七层流量入口负载均衡算法演变之路》中有提到
*/
if (uvnscf->last_number == NGX_CONF_UNSET_UINT) {
uvnscf->init_number = ngx_random() % peers->number;
/* 如果是带权重模式,则使用了虚拟节点来进行负载均衡,
所以从虚拟节点中选取peer
*/
if (peers->weighted) {
peer = vpeers[uvnscf->init_number].vpeer;
} else {
/* 如果是不带权重的模式,则没有虚拟节点,
需要直接在peers列表中循环init_number次数,选择第nit_number个peer
*/
for (peer = peers->peer, i = 0; i < uvnscf->init_number; i++) {
peer = peer->next;
}
}
uvnscf->last_number = uvnscf->init_number;
uvnscf->last_peer = peer;
}
if (peers->weighted) {
/* 如果当前初始化好的虚拟节点已经都被分配过一次了,并且还有没初始化过的虚拟节点,
则再次分配虚拟节点,最多max_init个。
*/
if (uvnscf->vnumber != peers->total_weight / uvnscf->gcd
&& (uvnscf->last_number + 1 == uvnscf->vnumber))
{
n = peers->total_weight / uvnscf->gcd - uvnscf->vnumber;
if (n > uvnscf->max_init) {
n = uvnscf->max_init;
}
ngx_http_upstream_init_virtual_peers(peers, uvnscf, uvnscf->vnumber,
n + uvnscf->vnumber);
}
/* 在虚拟节点循环队列中分配下一个vpeer
begin_numer为当前分配的虚拟节点在虚拟节点循环队列中的序号
*/
begin_number = (uvnscf->last_number + 1) % uvnscf->vnumber;
peer = vpeers[begin_number].vpeer;
} else {
/* 如果是不带权重模式,那么直接通过peer链进行peer的分配
一个peer中有多个地址的,那么先分配这个peer的地址,
否则,找下一个peer,begin_number为当前分配的peer在peer列表中的序号
*/
if (uvnscf->last_peer && uvnscf->last_peer->next) {
begin_number = (uvnscf->last_number + 1) % peers->number;
peer = uvnscf->last_peer->next;
} else {
begin_number = 0;
peer = peers->peer;
}
}
/* 以下对上面分配的peer进行状态过滤,如果分配的peer不能用,
需要再往下循环获取下一个peer */
/* 这里 i != begin_number || flag的判断用来检测是否已经循环了一圈回来了
循环了一圈回来的,那么所有的peer就已经遍历了,还是不能满足分配的需要。
*/
for (i = begin_number, flag = 1; i != begin_number || flag;
i = peers->weighted
? ((i + 1) % uvnscf->vnumber) : ((i + 1) % peers->number),
peer = peers->weighted
? vpeers[i].vpeer : (peer->next ? peer->next : peers->peer))
{
flag = 0;
if (peers->weighted) {
/* 这里也有可能分配的虚拟节点已经被遍历过一次了,并且还有没初始化过的虚拟节点,
则再次分配虚拟节点,最多max_init个 */
n = peers->total_weight / uvnscf->gcd - uvnscf->vnumber;
if (n > uvnscf->max_init) {
n = uvnscf->max_init;
}
if (n > 0) {
ngx_http_upstream_init_virtual_peers(peers, uvnscf, uvnscf->vnumber,
n + uvnscf->vnumber);
}
n = vpeers[i].rindex / (8 * sizeof(uintptr_t));
m = (uintptr_t) 1 << vpeers[i].rindex % (8 * sizeof(uintptr_t));
} else {
n = i / (8 * sizeof(uintptr_t));
m = (uintptr_t) 1 << i % (8 * sizeof(uintptr_t));
}
/* 节点是否已经分配过的状态判断 */
if (rrp->tried[n] & m) {
continue;
}
/* 节点是否已经被设置为down状态判断 */
if (peer->down) {
continue;
}
/* 节点是否故障保护状态判断 */
if (peer->max_fails
&& peer->fails >= peer->max_fails
&& now - peer->checked <= peer->fail_timeout)
{
continue;
}
/* 节点的当前在线连接是否超过限制判断 */
#if defined(nginx_version) && nginx_version >= 1011005
if (peer->max_conns && peer->conns >= peer->max_conns) {
continue;
}
#endif
#if (NGX_HTTP_UPSTREAM_CHECK)
if (ngx_http_upstream_check_peer_down(peer->check_index)) {
continue;
}
#endif
/* 得到了分配好的节点 */
best = peer;
uvnscf->last_peer = peer;
uvnscf->last_number = i;
p = i;
break;
}
if (best == NULL) {
return NULL;
}
rrp->current = best;
/* 在tried位表中设置当前节点已经被分配过 */
if (peers->weighted) {
n = vpeers[p].rindex / (8 * sizeof(uintptr_t));
m = (uintptr_t) 1 << vpeers[p].rindex % (8 * sizeof(uintptr_t));
} else {
n = p / (8 * sizeof(uintptr_t));
m = (uintptr_t) 1 << p % (8 * sizeof(uintptr_t));
}
rrp->tried[n] |= m;
if (now - best->checked > best->fail_timeout) {
best->checked = now;
}
return best;
}
以上函数中,如果是不带权重的模式,那么就是最简单的round-robin分配机制,每次分配就循环往后前进一个peer,一个特别的地方就是第一次分配的时候设置了一个随机值位置,从这个随机位置开始进行正式分配,避免产生“共振”;如果是带权重的模式,那么才是真正的vnswrr算法,这个算法另外创建了虚拟节点,虚拟节点的总数量是总权重/各服务器权重的最大公约数,为了避免一次性集中分配虚拟节点导致CPU压力突发,所以每次最多分配max_init个数的虚拟节点。
这是这些逻辑交织在一起,看上去ngx_http_upstream_get_vnswrr函数似乎有些复杂了。
和不带权重的模式一样,它也会在第一次分配的时候设置一个随机值位置,从随机的虚拟节点开始分配,避免“共振”现象的发生。
除了以上特别说明的部分,其他逻辑几乎就是round-robin代码的翻版,还是非常好理解的,本文列出的源码中也给出了注释,就不再赘述了。