目录
- 1. 引言
- 2. 启用vnswrr负载均衡模块
- 3. 源码剖析
- 3.1 配置指令分析
- 3.2 负载均衡算法配置初始化
- 3.3 负载均衡请求上下文的初始化
- 3.4 获取peer
1. 引言
之前有讨论了nginx的swrr算法的两个问题,并引出了阿里tengine的vnswrr算法如何来克服swrr的问题。本文通过源码层面对ngx_http_upstream_vnswrr_module模块进行分析,来深入理解vnswrr负载均衡算法。关于swrr算法的思考可以查看《nginx upstream server主动健康检测模块添加https检测功能》。关于vnswrr的算法原理可以参考《阿里七层流量入口负载均衡算法演变之路》。
2. 启用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;
}
3. 源码剖析
3.1 配置指令分析
本模块定义了配置指令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;
}
3.2 负载均衡算法配置初始化
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函数的逻辑就是分别对主服务器和备服务器组进行加载操作,初始化一部分虚拟节点,详细的逻辑在源码中已经进行了注释,不再赘述。
3.3 负载均衡请求上下文的初始化
当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。
3.4 获取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 >= 1011005if (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;}
#endifrrp->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;
#endifrrp = &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);}/* 在虚拟节点循环队列中分配下一个vpeerbegin_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 >= 1011005if (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代码的翻版,还是非常好理解的,本文列出的源码中也给出了注释,就不再赘述了。