由于曾经缺乏手撕源码的经验,这段时间从零开始一点点啃nginx源码实在是啃的过于痛苦,加之自己的代码水平实在有限,只能结合多方大佬的源码分析文章以及自己的“意会”,用自己能理解的大白话梳理一下我所理解的算法实现过程。
轮询算法的原理就不作叙述了,这里直接贴出每轮轮询中选出最后结果的方法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;
#if (NGX_SUPPRESS_WARN)
p = 0;
#endif
for (peer = rrp->peers->peer, i = 0; //peer表示当前服务器
peer;
peer = peer->next, i++)
{
//计算当前服务器的标记位在位图中的位置
n = i / (8 * sizeof(uintptr_t));
m = (uintptr_t) 1 << i % (8 * sizeof(uintptr_t));
//已经选择过,跳过
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;
}
if (peer->max_conns && peer->conns >= peer->max_conns) {
continue;
}
/*遍历服务列表的过程中,每遍历到一个服务,会在该服务的current_weight上加上其对应的effective_weight*/
peer->current_weight += peer->effective_weight;
/*total变量记录了针对一个服务列表的一次轮询过程中轮询到的所有服务的effective_weight总和*/
total += peer->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;
//取到该后端服务后,都会把该对象peer的current_weight减去total。因为该服务器刚被选中过,因此要降低权值
best->current_weight -= total;
if (now - best->checked > best->fail_timeout) {
best->checked = now; //设置最佳结点的检查时间为now;
}
return best;
}
其中我觉得最能表现该算法核心“轮询”二字的关键代码部分:
for (peer = rrp->peers->peer, i = 0; peer;peer = peer->next, i++)
{
...
peer->current_weight += peer->effective_weight;
total += peer->effective_weight;
...
if (best == NULL || peer->current_weight > best->current_weight) {
best = peer;
p = i;
}
...
}
其中,peer是指当前遍历到的服务器结点,current_weight指该结点的当前权值,total指本轮遍历中所有结点的总权值,它会累加;也就是说,current_weight只记录当前一次循环的这个peer的权值,而total会记录累加整个循环中所有peer的权值之和。best指最佳结点,循环结束时,即可找到最佳结点best。
找到最佳结点best后应当降低其权值,避免下次轮询的结果还是它,源码是这么做的:
best->current_weight -= total;
我觉得这里是最难理解的,为什么要让当前结点的权值减去总权值呢,这岂不是负数了吗?从这篇博客看出→参考的轮询策略实例,结果还真是负数。结合大佬的文章,我是这样理解这个过程的:
每个结点就像一位勇者,他的hp为current_weight。每次轮询就像一次回合制战斗,会选出当前hq最高的那位勇者前去攻击。被选择的这位勇者成功击中怪物一次(被选择成为best最佳结点),自己就会被怪物反击,需从hq中扣除一定的血量(即total)。此时这位勇者的hq太低,下次就不会被选取作战了。但每位勇者每回合都会回复一定hq(即effective_weight),也就是说effective_weight越高,其恢复能力越强,因而其没过多久hq又会变的最高,最后总的看下来,的确权值越高,该结点被选择的几率越大!