首页 > 代码库 > Nginx学习——负载均衡

Nginx学习——负载均衡

负载均衡

Nginx提供了较多的负载均衡策略,包括加权轮询、IP哈希、fair、一致哈希等。前两个是Nginx官方源码内置的策略,而后面几个都是第三方模块,所以下面我们重点来看前两个内置策略。


Nginx默认采用round_robin加权算法,如果要采用IP哈希策略,那么必须在Nginx的配置文件里通过配置指令ip_hash明确指定。


当整个http配置块被Nginx解析完毕之后,会调用各个http模块对应的初始函数。对于模块ngx_http_upstream_module而言,对应的main配置初始函数是ngx_http_upstream_init_main_conf(),在这个函数中有这样一段代码:

for (i = 0; i < umcf->upstreams.nelts; i++) {

        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;
        }
}


默认采用加权轮询策略的原因就是在于上述代码中的init赋值一行。如果用户没有做任何策略选择,那么执行的策略初始函数为ngx_http_upstream_init_round_robin,也就是加权轮询策略。否则的话执行的是uscfp[i]->peer.init_upstream指针函数,如果有配置执行ip_hash ,那么就是ngx_http_upstream_init_ip_hash()。

加权轮询

加权轮询,直观上理解就是计算各个后端服务器的当前权值,然后选择得分最高的服务器处理当前请求,Nginx的处理大致如此,但是在具体实现时考虑很多其他细节,比如服务器可能具有不同的权值,某个服务器多次连接失败或处理出错后则在一定时间内不再参与被选择等。

准备工作

使用加权轮询时,upstream上下文内server配置可带的参数中,我们关心的有如下几个:
1.weight:权值,默认值为1,与加权轮询策略配合使用
2. fail_timeout和fail_timeout:他们配合使用,默认值分别为1和10s。具体含义是指,如果某台后段服务器在fail_timeout时间内发生了fail_timeout次连接失败,那么该后端服务器在这fail_timeout时间内就不在参与被选择,知道fail_timeout时间后才重新加入而有机会被选择,其直白意思也就是请先休息一会,然后再来。
3.backup:备机,平常不被选择,只有当其他所有非备机全部不可用时才被使用。值得宕略里,因为他会扰乱哈希的结果而违背ip_hash的初衷。

4.down:即主动表示其未宕机状态,不参与被选择。


需要注意的是,配置文件中出现的参数只能和某些策略配合使用,所以如果发现某参数没有生效,则应该检查这一点。在配置解析的过程中,这些选项设置都被转换为Nginx内对于的变量值,对应的结构体ngx_http_upstream_server_t如下(ngx_http_upstream.h):
typedef struct {
    ngx_addr_t                      *addrs;//指向存储IP地址的数组的指针,host信息(对应的是 ngx_url_t->addrs )
    ngx_uint_t                       naddrs;//与第一个参数配合使用,数组元素个数(对应的是 ngx_url_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;

这里函数ngx_http_upstream_init_round_robin()所做的工作除了把配置项解析后的结果转存到对应的变量以外,主要还有以下几项:创建后端服务器列表,并且将非后背服务器与后背服务器分开进行各自单独的列表,每一个后段服务器用一个结构体ngx_http_upstream_rr_peer_t对应,列表最前面需要带有一些head信息,所以用ngx_http_upstream_rr_peers_t结构体对应。非后背服务器列表挂载在us->peer.data字段下,而后背服务器列表挂载在非后背服务器列表head于里的next字段下。两个列表的服务器会按初始权重进行排序,高权重的在前面。

选择后端服务器

全局初始化完成之后,当一个客户端请求过来时,Nginx就要选择合适的后端服务器来处理该请求。在正式开始选择前,Nginx还要单独为本轮选择做一些初始化(针对一个客户端请求,nginx会进行多次尝试选择,尝试全部失败后才返回502错误,所以注意一轮选择与一次选择的区别)。

在ngx_http_upstream_init_round_robin()中,有如下语句


us->peer.init = ngx_http_upstream_init_round_robin_peer; //回调指针设置 

它的调用位置是函数ngx_http_upstream_init_request中,即在针对每个请求选择后端服务器之前被调用。下面对ngx_http_upstream_init_round_robin_peer做了什么做解释,它除了完成初始化工作外,核心是设置回调函数,部分代码如下:


//回调函数设置
    r->upstream->peer.get = ngx_http_upstream_get_round_robin_peer;
    r->upstream->peer.free = ngx_http_upstream_free_round_robin_peer;
    r->upstream->peer.tries = rrp->peers->number;

对后端服务器进行一次选择的逻辑是现在ngx_http_upstream_get_round_robin_peer内,流程图和代码如下:


对于只有一台后端服务器的情况,Nginx直接选择它并返回。如果有多台后端服务器,Nginx会循环调用函数ngx_http_uostream_get_peer()按照各台服务器的当前权值进行选择。如果对非后备服务器全部选择失败的话,此时开始尝试选择后备服务器,这同样是对一个服务器列表进行选择,所以处理情况与对非后备服务器处理情况进行选择的逻辑一致。如果对后备服务器选择也失败,那么ngx_http_upstream_get_round_robin_peer返回NGX_BUSY,意味着当前没有后端服务器来处理该请求。


后端服务器权值计算在函数ngx_http_uostream_get_peer()中,这个函数中还有一个变量total,但要理解这个函数的工作原理,先要区分下表示服务的ngx_http_upstream_rr_peer_t结构体中的一下三个成员变量,

ngx_int_t                       current_weight;
    ngx_int_t                       effective_weight;
    ngx_int_t                       weight;

它们在函数ngx_http_upstream_init_round_robin中被初始化:
for (i = 0; i < us->servers->nelts; i++) {
            for (j = 0; j < server[i].naddrs; j++) {
                if (server[i].backup) {
                    continue;
                }

                peers->peer[n].weight = server[i].weight;
                peers->peer[n].effective_weight = server[i].weight;
                peers->peer[n].current_weight = 0;
                n++;
            }
        }

        /* backup servers */
        for (i = 0; i < us->servers->nelts; i++) {
            for (j = 0; j < server[i].naddrs; j++) {
                if (!server[i].backup) {
                    continue;
                }

                backup->peer[n].weight = server[i].weight;
                backup->peer[n].effective_weight = server[i].weight;
                backup->peer[n].current_weight = 0;

                n++;
            }
        }

     /* an upstream implicitly defined by proxy_pass, etc. */
    for (i = 0; i < u.naddrs; i++) {
        peers->peer[i].weight = 1;
        peers->peer[i].effective_weight = 1;
        peers->peer[i].current_weight = 0;
    }

可以看到weight、effective_weight都是初始化为配置项中的weight值。current_weight初始化为0.

下面分析这三个变量在负载均衡过程中的变化:


weight的值在整个运行过程中不发生变化。total变量记录了针对一个服务列表的一次轮询过程中轮询到的所有服务的effective_weight总和。在每一次针对服务列表的轮询之前会置为为0。遍历服务列表的过程中,每遍历到一个服务,会在该服务的current_weight上加上其对应的effective_weight。这个是累加。如果对统一的服务列表进行另一次轮询,那么会在前面计算的current_weight的基础之上再加上effective_weight。


轮询策略是取current_weight最大的服务器。每次取到后端服务(用best表示)后,都会把该对象peer的current_weight减去total的值。因为该服务刚被选中过,因此要降低权值。


关于effective_weight的变化,有两处,一个是在函数ngx_http_upstream_get_peer中:

 //服务正常,effective_weight 逐渐恢复正常    
        if (peer->effective_weight < peer->weight) {
            peer->effective_weight++;
        }

另一处是在释放后端服务的函数ngx_http_upstream_free_round_robin_peer中:
 if (peer->max_fails) {
             //服务发生异常时,调低effective_weight
            peer->effective_weight -= peer->weight / peer->max_fails;
        }

权重高的会优先被选中,而且被选中的频率也更高。权重低的也会由于权重逐渐增长获得被选中的机会,如下表所示:


selected server

current_weight beforeselected

current_weight afterselected

a

{ 5, 1, 2 }

{ -3, 1, 2 }

c

{ 2, 2, 4 }

{ 2, 2, -4 }

a

{ 7, 3, -2 }

{ -1, 3, -2 }

a

{ 4, 4, 0 }

{ -4, 4, 0 }

b

{ 1, 5, 2 }

{ 1, -3, 2 }

a

{ 6, -2, 4 }

{ -2, -2, 4 }

c

{ 3, -1, 6 }

{ 3, -1, -2 }

a

{ 8, 0, 0 }

{ 0, 0, 0 }


释放后端服务器

连接后端服务器并且正常处理当前客户端请求后需释放后端服务器。如果在某一轮选择里,某次选择的服务器因连接失败或请求处理失败而需要重新进行选择,那么这时候就需要做一些额外的处理。

整个加权轮询的流程:

整个加权轮询的流程图如下:


1)首先是全局初始化,由函数ngx_http_upstream_init_round_robin完成,它在函数ngx_http_upstream_init_main_conf中被调用。
2)收到客户请求之后,针对当前请求进行初始化,完成此功能的函数是ngx_http_upstream_init_round_robin_peer,它在函数ngx_http_upstream_init_request中被调用。
3)然后是针对每个请求选择后端服务器,实现此功能的函数是ngx_http_upstream_get_round_robin_peer。它在函数ngx_event_connect_peer中被调用。
4)之后是测试连接ngx_http_upstream_test_connect。它在函数ngx_http_upstream_send_request被调用。
5)如果测试成功,继续后续处理,并释放后端服务器。
如果测试失败,调用ngx_http_upstream_next函数,这个函数可能再次调用peer.get调用别的连接。
6)函数ngx_http_upstream_connect中会调用ngx_event_connect_peer,进而调用ngx_http_upstream_get_round_robin_peer再次选择后端服务器。

IP哈希

用IP负载均衡策略时,当一个客户端请求过来时,Nginx将调用ngx_http_upstream_init_ip_hash_peer()做初始化。之所以这样做是因为在多次哈希选择失败后,Nginx会将选择策略退化到加权轮询。这里会设置ngx_http_upstream_get_ip_hash_peer以在便收到请求时调用。同时会转存Ipv4中三个字节,因为后面在具体的哈希计算时只会用到3个字节。


ngx_http_upstream_get_ip_hash_peer在会计算哈希值,并根据哈希值得到被选中的后端服务器,判断其是否可用,如果可用则保存服务器地址,不可用则在上次哈希结果的基础上再哈希。如果哈希选择失败20次以上或质疑一台后端服务器,此时采用轮询策略。


流程图如下:

两种策略对比:

加权轮询策略
优点:适用性更强,不依赖于客户端的任何信息,完全依靠后端服务器的情况来进行选择。能把客户端请求更合理更均匀地分配到各个后端服务器处理。
缺点:同一个客户端的多次请求可能会被分配到不同的后端服务器进行处理,无法满足做会话保持的应用的需求。

IP哈希策略
优点:能较好地把同一个客户端的多次请求分配到同一台服务器处理,避免了加权轮询无法适用会话保持的需求。
缺点:当某个时刻来自某个IP地址的请求特别多,那么将导致某台后端服务器的压力可能非常大,而其他后端服务器却空闲的不均衡情况。