关闭

Nginx的负载均衡 - 加权轮询 (Weighted Round Robin) 下篇

标签: Nginx负载均衡
5479人阅读 评论(0) 收藏 举报
分类:

Nginx版本:1.9.1

我的博客: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。

 

下图用来简要说明

 

 

 

 

0
0
查看评论

轮转调度算法(RR)

RR算法是使用非常广泛的一种调度算法。 首先将所有就绪的队列按FCFS策略排成一个就绪队列,然后系统设置一定的时间片,每次给队首作业分配时间片。如果此作业运行结束,即使时间片没用完,立刻从队列中去除此作业,并给下一个作业分配新的时间片;如果作业时间片用完没有运行结束,则将此作业重新加入就绪队列尾部等...
  • zhaoshu01234
  • zhaoshu01234
  • 2015-12-03 01:00
  • 5978

Nginx学习之十二-负载均衡-加权轮询策略剖析

本问介绍的是客户端请求在多个后端服务器之间的均衡,注意与客户端请求在多个nginx进程之间的均衡相区别(Nginx根据每个工作进程的当前压力调整它们获取监听套接口的几率,那些当前比较空闲的工作进程有更多机会获取到监听套接口,从而客户端的请求到达后也就相应地被它捕获并处理)。如果Nginx是以反向代理...
  • xiajun07061225
  • xiajun07061225
  • 2013-07-13 15:52
  • 37768

Nginx的负载均衡 - 加权轮询 (Weighted Round Robin) 上篇

当在upstream配置块中没有指定使用的负载均衡算法时,默认使用的是加权轮询。 按照上述配置,Nginx每收到7个客户端的请求,会把其中的5个转发给后端a,把其中的1个转发给后端b, 把其中的1个转发给后端c。 这就是所谓的加权轮询,看起来很简单,但是最早使用的加权轮询算法有个问题,就是7个请求对...
  • zhangskd
  • zhangskd
  • 2015-12-12 23:33
  • 20133

负载均衡之加权轮询算法

在介绍加权轮询算法(WeightedRound-Robin)之前,首先介绍一下轮询算法(Round-Robin)。    一:轮询算法(Round-Robin)   轮询算法是最简单的一种负载均衡算法。它的原理是把来自用户的请求轮流分配给内部的服务器:从服务器1开始,直到服务器N,然后重新开始...
  • gqtcgq
  • gqtcgq
  • 2016-07-31 09:30
  • 9328

轮询调度算法(Round-Robin Scheduling) 和 权重轮询调度算法(Weighted Round-Robin Scheduling)

循环调度(Round Robin Scheduling)算法 循环调度(Round Robin Scheduling)算法就是以循环的方式依次将请求调度不同的服务器,即每次调度执行i = (i + 1) mod n,并选出第i台服务器。算法的优点是其简洁性,它无需记录当前所有连接的状态,所以它是一种...
  • huang_jing_ze
  • huang_jing_ze
  • 2016-08-04 10:59
  • 352

加权轮叫调度(Weighted Round-Robin Scheduling)

 轮叫调度(Round Robin Scheduling)算法就是以轮叫的方式依次将请求调度不同的服务器,即每次调度执行i = (i + 1) mod n,并选出第i台服务器。算法的优点是其简洁性,它无需记录当前所有连接的状态,所以它是一种无状态调度。在系统实现时,我们引入了一个额外条件,...
  • starxu85
  • starxu85
  • 2008-11-05 22:42
  • 4055

nginx upstream的五种分配方式

nginx负载均衡选项upstream用法举例 nginx中upstream的几种方式: 1、轮询(weight=1) 默认选项,当weight不指定时,各服务器weight相同, 每个请求按时间顺序逐一分配到不同的后端服务器,如果后端服务器down掉,能自动剔除。   upstre...
  • u010081710
  • u010081710
  • 2016-09-28 16:17
  • 4030

Nginx upstream的5种权重分配方式分享

1.轮询(默认)每个请求按时间顺序逐一分配到不同的后端服务器,如果后端服务器down掉,能自动剔除。2.weight指定轮询几率,weight和访问比率成正比,用于后端服务器性能不均的情况。 upstream backend { server 192.168.0.14 weight=10;...
  • wh2691259
  • wh2691259
  • 2016-08-24 14:47
  • 518

大型网站架构与分布式架构

大型互联网架构 解决问题的通用思路是将分而治之(divide-and-conquer),将大问题分为若干个小问题,各个击破。在大型互联网的架构实践中,无一不体现这种思想。 架构目标 低成本:任何公司存在的价值都是为了获取商业利益。在可能的情况下,希望一切都是低成本的。高性能:网站性能是客观的指...
  • he90227
  • he90227
  • 2015-10-08 15:47
  • 5424

大型互联网架构概述

典型实现 一、DNS (1)当用户在浏览器中输入网站地址后,浏览器会检查浏览器缓存中是否存在对应域名的解析结果。如果有,则解析过程结束;否则进入下一个步骤 (2)浏览器查找操作系统缓存中是否存在这个域名的解析结果。这个缓存的内容来源就是操作系统的hosts文件。如果有,则解析过程结束;否...
  • shenfuli
  • shenfuli
  • 2015-01-03 22:00
  • 605
    个人资料
    • 访问:1480368次
    • 积分:12833
    • 等级:
    • 排名:第1280名
    • 原创:150篇
    • 转载:8篇
    • 译文:0篇
    • 评论:224条
    博客专栏
    博客公告

    小程序员一个,坐标深圳。

    Email: zhangskd at gmail.com

    不总在,多包涵