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

上篇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。

 
  1. ngx_http_module_t ngx_http_upstream_module_ctx = {  
  2.     ...  
  3.     ngx_http_upstream_init_main_conf, /* init main configuration */  
  4.     ...  
  5. };  
 
  1. static char *ngx_http_upstream_init_main_conf(ngx_conf_t *cf, void *conf)  
  2. {  
  3.     ...  
  4.     /* 数组的元素类型是ngx_http_upstream_srv_conf_t */  
  5.     for (i = 0; i < umcf->upstreams.nelts; i++) {  
  6.         /* 如果没有指定upstream块的初始化函数,默认使用round robin的 */  
  7.         init = uscfp[i]->peer.init_upstream ? uscfp[i]->peer.init_upstream :  
  8.                         ngx_http_upstream_init_round_robin;  
  9.   
  10.         if (init(cf, uscfp[i] != NGX_OK) {  
  11.             return NGX_CONF_ERROR;  
  12.         }  
  13.     }  
  14.     ...  
  15. }  

ngx_http_upstream_init_round_robin做的工作很简单:

指定请求的负载均衡初始化函数,用于初始化per request的负载均衡数据。

创建和初始化后端集群、备份集群。

 
  1. ngx_int_t ngx_http_upstream_init_round_robin (ngx_conf_t *cf, ngx_http_upstream_srv_conf_t *us)  
  2. {  
  3.     ngx_url_t u;  
  4.     ngx_uint_t i, j, n, w;  
  5.     ngx_http_upstream_server_t *server;  
  6.     ngx_http_upstream_rr_peer_t *peer, **peerp;  
  7.     ngx_http_upstream_rr_peers_t *peers, *backup;  
  8.   
  9.     /* 指定请求的负载均衡初始化函数,用于初始化per request的负载均衡数据 */  
  10.     us->peer.init = ngx_http_upstream_init_round_robin_peer;  
  11.   
  12.     /* upstream配置块的servers数组,在解析配置文件时就创建好了 */  
  13.     if (us->servers) {  
  14.         server = us->servers->elts;  
  15.         n = 0;  
  16.         w = 0;  
  17.           
  18.         /* 数组元素类型为ngx_http_upstream_server_t,对应一条server指令 */  
  19.         for (i = 0; i < us->servers->nelts; i++) {  
  20.             if (server[i].backup)  
  21.                 continue;  
  22.   
  23.             n += server[i].naddrs; /* 所有后端服务器的数量 */  
  24.             w += server[i].naddrs * server[i].weight; /* 所有后端服务器的权重之和 */  
  25.         }  
  26.   
  27.         if (n == 0) { /* 至少得有一台后端吧 */  
  28.             ...  
  29.             return NGX_ERROR;  
  30.         }  
  31.   
  32.   
  33.         /* 创建一个后端集群的实例 */  
  34.         peers = ngx_pcalloc(cf->pool, sizeof(ngx_http_upstream_rr_peers_t));  
  35.         ...  
  36.   
  37.         /* 创建后端服务器的实例,总共有n台 */  
  38.         peer = ngx_pcalloc(cf->pool, sizeof(ngx_http_upstream_rr_peer_t) * n);  
  39.         ...  
  40.   
  41.         /* 初始化集群 */  
  42.         peers->single = (n == 1); /* 是否只有一台后端 */  
  43.         peers->number = n; /* 后端服务器的数量 */  
  44.         peers->weight = (w != n); /* 是否使用权重 */  
  45.         peers->total_weight = w; /* 所有后端服务器权重的累加值 */  
  46.         peers->name = &us->host; /* upstream配置块的名称 */  
  47.   
  48.         n = 0;  
  49.         peerp = &peers->peer;  
  50.   
  51.         /* 初始化代表后端的结构体ngx_http_upstream_peer_t. 
  52.          * server指令后跟的是域名的话,可能对应多台后端. 
  53.          */  
  54.         for(i = 0; i < us->servers->nelts; i++) {  
  55.             if (server[i].backup)  
  56.                 continue;  
  57.   
  58.             for (j = 0; j < server[i].naddrs; j++) {  
  59.                 peer[n].sockaddr = server[i].addrs[j].sockaddr; /* 后端服务器的地址 */  
  60.                 peer[n].socklen = server[i].addrs[j].socklen; /* 地址的长度*/  
  61.                 peer[n].name = server[i].addrs[j].name; /* 后端服务器地址的字符串 */  
  62.                 peer[n].weight = server[i].weight;  /* 配置项指定的权重,固定值 */  
  63.                 peer[n].effective_weight = server[i].weight; /* 有效的权重,会因为失败而降低 */  
  64.                 peer[n].current_weight = 0/* 当前的权重,动态调整,初始值为0 */  
  65.                 peer[n].max_fails = server[i].max_fails; /* "一段时间内",最大的失败次数,固定值 */  
  66.                 peer[n].fail_timeout = server[i].fail_timeout; /* "一段时间"的值,固定值 */  
  67.                 peer[n].down = server[i].down; /* 服务器永久不可用的标志 */  
  68.                 peer[n].server = server[i].name; /* server的名称 */  
  69.   
  70.                 /* 把后端服务器组成一个链表,第一个后端的地址保存在peers->peer */  
  71.                 *peerp = &peer[n];  
  72.                 peerp = &peer[n].next;  
  73.                 n++;  
  74.             }  
  75.         }  
  76.   
  77.         us->peer.data = peers; /* 保存后端集群的地址 */  
  78.     }  
  79.   
  80.     /* backup servers */  
  81.     /* 创建和初始化备份集群,peers->next指向备份集群,和上述流程类似,不再赘述 */  
  82.     ...  
  83.     /* an upstream implicitly defined by proxy_pass, etc. */  
  84.     /* 如果直接使用proxy_pass指令,没有定义upstream配置块 */  
  85.     if (us->port == 0) {  
  86.         ...          
  87.         return NGX_ERROR;  
  88.     }  
  89.   
  90.     ngx_memzero(&u, sizeof(ngx_url_t));  
  91.     u.host = us->host;  
  92.     u.port = us->port;  
  93.   
  94.     /* 根据URL解析域名 */  
  95.     if (ngx_inet_resolve_host(cf->pool, &u) != NGX_OK) {  
  96.         ...  
  97.         return NGX_ERROR;  
  98.     }  
  99.   
  100.     n = u.naddrs; /* 共有n个后端 */  
  101.     /* 接下来创建后端集群,并进行初始化,和上述流程类似,这里不再赘述 */  
  102.     ...  
  103.     return NGX_OK;  
  104. }  

 初始化请求的负载均衡数据

当收到一个请求后,一般使用的反向代理模块(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,请求最多允许尝试这么多个后端

 
  1. ngx_int_t ngx_http_upstream_init_round_robin_peer(ngx_http_request_t *r, ngx_http_upstream_srv_conf_t *us)  
  2. {  
  3.     ngx_uint_t n;  
  4.     ngx_http_upstream_rr_peer_data_t *rrp;  
  5.       
  6.     /* 创建请求的负载均衡数据块 */  
  7.     rrp = r->upstream->peer.data;  
  8.     if (rrp == NULL) {  
  9.         rrp = ngx_palloc(r->pool, sizeof(ngx_http_upstream_rr_peer_data_t));  
  10.         if (rrp == NULL)   
  11.             return NGX_ERROR;  
  12.           
  13.         r->upstream->peer.data = rrp; /* 保存请求负载均衡数据的地址 */  
  14.     }  
  15.   
  16.     rrp->peers = us->peer.data; /*  upstream块的后端集群 */  
  17.     rrp->current = NULL;  
  18.   
  19.     n = rrp->peers->number; /* 后端的数量 */  
  20.     /* 如果存在备份集群,且其服务器数量超过n */  
  21.     if (rrp->peers->next && rrp->peers->next->number > n) {  
  22.         n = rrp->peers->next->number;  
  23.     }  
  24.   
  25.     /* rrp->tried指向后端服务器的位图,每一位代表一台后端的状态,0表示可用,1表示不可用。 
  26.      * 如果后端数较少,直接使用rrp->data作为位图。如果后端数较多,则需要申请一块内存。 
  27.       */  
  28.     if (n <= 8 *sizeof(uintptr_t)) {  
  29.         rrp->tried = &rrp->data;  
  30.         rrp->data = 0;  
  31.     } else {  
  32.         n = ( n + (8 * sizeof(uintptr_t) - 1)) / (8 * sizeof(uintptr_t)); /* 向上取整 */  
  33.         rrp->tried = ngx_pcalloc(r->pool, n * sizeof(uintptr_t));  
  34.         if (rrp->tried == NULL) {  
  35.             return NGX_ERROR;  
  36.         }  
  37.     }  
  38.   
  39.     r->upstream->peer.get = ngx_http_upstream_get_round_robin_peer; /* 指定peer.get,用于从集群中选取一台后端服务器 */  
  40.     r->upstream->peer.free = ngx_http_upstream_free_round_robin_peer; /* 指定peer.free,当不用该后端时,进行数据的更新 */  
  41.     r->upstream->peer.tries = ngx_http_upstream_tries(rrp->peers); /* 指定peer.tries,是请求允许尝试的后端服务器个数 */  
  42.     ...  
  43.     return NGX_OK;  
  44. }  
  45.   
  46. #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)。

 
  1. ngx_int_t ngx_http_upstream_get_round_robin_peer(ngx_peer_connection_t *pc, void *data)  
  2. {  
  3.     ngx_http_upstream_rr_peer_data_t  *rrp = data; /* 请求的负载均衡数据 */  
  4.   
  5.     ngx_int_t                      rc;  
  6.     ngx_uint_t                     i, n;  
  7.     ngx_http_upstream_rr_peer_t   *peer;  
  8.     ngx_http_upstream_rr_peers_t  *peers;  
  9.     ...  
  10.     pc->cached = 0;  
  11.     pc->connection = NULL;  
  12.   
  13.     peers = rrp->peers; /* 后端集群 */  
  14.     ...  
  15.     /* 如果只有一台后端,那就不用选了 */  
  16.     if (peers->single) {  
  17.         peer = peers->peer;  
  18.         if (peer->down)   
  19.             goto failed;  
  20.           
  21.         rrp->current = peer;  
  22.   
  23.     } else {  
  24.         /* there are several peers */  
  25.         /* 调用ngx_http_upstream_get_peer来从后端集群中选定一台后端服务器 */  
  26.         peer = ngx_http_upstream_get_peer(rrp);  
  27.   
  28.         if (peer == NULL)  
  29.             goto failed;  
  30.         ...  
  31.     }  
  32.   
  33.     /* 保存选定的后端服务器的地址,之后会向这个地址发起连接 */  
  34.     pc->sockaddr = peer->sockaddr;  
  35.     pc->socklen = peer->socklen;  
  36.     pc->name = &peer->name;  
  37.   
  38.     peer->conns++; /* 增加选定后端的当前连接数 */  
  39.     ...  
  40.     return NGX_OK;  
  41.   
  42. failed:  
  43.     /* 如果不能从集群中选取一台后端,那么尝试备用集群 */  
  44.     if (peers->next) {  
  45.         ...          
  46.         rrp->peers = peers->next;  
  47.         n = (rrp->peers->number + (8 * sizeof(uintptr_t) - 1))  
  48.                 / (8 * sizeof(uintptr_t));  
  49.         for (i = 0; i < n; i++)  
  50.              rrp->tried[i] = 0;  
  51.          
  52.         /* 重新调用本函数 */          
  53.         rc = ngx_http_upstream_get_round_robin_peer(pc, rrp);  
  54.   
  55.         if (rc != NGX_BUSY)  
  56.             return rc;  
  57.     }  
  58.   
  59.     /* all peers failed, mark them as live for quick recovery */  
  60.     for (peer = peers->peer; peer; peer = peer->next) {  
  61.         peer->fails = 0;  
  62.     }  
  63.     pc->name = peers->name;  
  64.     return NGX_BUSY;  
  65. }  

ngx_http_upstream_get_peer用于从集群中选取一台后端服务器。

 
  1. static ngx_http_upstream_rr_peer_t *ngx_http_upstream_get_peer(ngx_http_upstream_rr_peer_data_t *rrp)  
  2. {  
  3.     time_t                        now;  
  4.     uintptr_t                     m;  
  5.     ngx_int_t                     total;  
  6.     ngx_uint_t                    i, n, p;  
  7.     ngx_http_upstream_rr_peer_t  *peer, *best;  
  8.   
  9.     now = ngx_time();  
  10.     best = NULL;  
  11.     total = 0;  
  12.     ...  
  13.   
  14.     /* 遍历集群中的所有后端 */  
  15.     for (peer = rrp->peers->peer, i = 0;  
  16.          peer;  
  17.          peer = peer->next, i++)  
  18.     {  
  19.   
  20.         n = i / (8 * sizeof(uintptr_t));  
  21.         m = (uintptr_t) 1 << i % (8 * sizeof(uintptr_t));  
  22.   
  23.   
  24.         /* 检查该后端服务器在位图中对应的位,为1时表示不可用 */  
  25.         if (rrp->tried[n] & m)  
  26.             continue;  
  27.          
  28.         /* 永久不可用的标志 */  
  29.         if (peer->down)   
  30.             continue;  
  31.           
  32.        /* 在一段时间内,如果此后端服务器的失败次数,超过了允许的最大值,那么不允许使用此后端了 */  
  33.        if (peer->max_fails  
  34.             && peer->fails >= peer->max_fails  
  35.             && now - peer->checked <= peer->fail_timeout)  
  36.             continue;  
  37.           
  38.         peer->current_weight += peer->effective_weight; /* 对每个后端,增加其当前权重 */  
  39.         total += peer->effective_weight; /* 累加所有后端的有效权重 */  
  40.   
  41.         /* 如果之前此后端发生了失败,会减小其effective_weight来降低它的权重。           
  42.          * 此后在选取后端的过程中,又通过增加其effective_weight来恢复它的权重。           
  43.          */          
  44.         if (peer->effective_weight < peer->weight)   
  45.             peer->effective_weight++;  
  46.           
  47.         /* 选取当前权重最大者,作为本次选定的后端 */  
  48.         if (best == NULL || peer->current_weight > best->current_weight) {  
  49.             best = peer;  
  50.             p = i;  
  51.         }  
  52.     }  
  53.   
  54.     if (best == NULL) /* 没有可用的后端 */  
  55.         return NULL;  
  56.       
  57.     rrp->current = best; /* 保存本次选定的后端 */  
  58.   
  59.     n = p / (8 * sizeof(uintptr_t));  
  60.     m = (uintptr_t) 1 << p % (8 * sizeof(uintptr_t));  
  61.   
  62.     /* 对于本次请求,如果之后需要再次选取后端,不能再选取这个后端了 */      
  63.     rrp->tried[n] |= m;  
  64.   
  65.     best->current_weight -= total; /* 选定后端后,需要降低其当前权重 */    
  66.     /* 更新checked时间 */  
  67.     if (now - best->checked > best->fail_timeout)  
  68.         best->checked = now;  
  69.       
  70.     return best;  
  71. }  

 释放一台后端服务器

当不再使用一台后端时,需要进行收尾处理,比如统计失败的次数。

这时候会调用第三步中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)。

 
  1. void ngx_http_upstream_free_round_robin_peer(ngx_peer_connection_t *pc, void *data,  
  2.     ngx_uint_t state)  
  3. {  
  4.     ngx_http_upstream_rr_peer_data_t  *rrp = data; /* 请求的负载均衡数据 */  
  5.     time_t                       now;  
  6.     ngx_http_upstream_rr_peer_t  *peer;  
  7.     ...  
  8.     peer = rrp->current; /* 当前使用的后端服务器 */  
  9.   
  10.     if (rrp->peers->single) {  
  11.         peer->conns--; /* 减少后端的当前连接数 */  
  12.         pc->tries = 0/* 不能再继续尝试了 */  
  13.         return;  
  14.     }  
  15.   
  16.     /* 如果连接后端失败了 */  
  17.     if (state & NGX_PEER_FAILED) {  
  18.         now = ngx_time();  
  19.   
  20.         peer->fails++; /* 一段时间内,已经失败的次数 */  
  21.         peer->accessed = now; /* 最近一次失败的时间点 */  
  22.         peer->checked = now; /* 用于检查是否超过了“一段时间” */  
  23.   
  24.         /* 当后端出错时,降低其有效权重 */  
  25.         if (peer->max_fails)   
  26.             peer->effective_weight -= peer->weight / peer->max_fails;  
  27.           
  28.   
  29.         /* 有效权重的最小值为0 */  
  30.         if (peer->effective_weight < 0)   
  31.             peer->effective_weight = 0;  
  32.           
  33.     } else {  
  34.         /* mark peer live if check passed */  
  35.         /* 说明距离最后一次失败的时间点,已超过fail_timeout了,清零fails */  
  36.         if (peer->accessed < peer->checked)  
  37.             peer->fails = 0;  
  38.     }  
  39.   
  40.     peer->conns--; /* 更新后端的当前连接数 */  
  41.   
  42.     if (pc->tries)  
  43.         pc->tries--;  /* 对于一个请求,允许尝试的后端个数 */  
  44. }  

判断后端是否可用

相关的变量的定义

ngx_uint_t fails; /* 一段时间内,已经失败的次数 */

time_t accessed; /* 最近一次失败的时间点 */

time_t checked; /* 用于检查是否超过了“一段时间” */

ngx_uint_t max_fails; /* 一段时间内,允许的最大的失败次数,固定值 */

time_t fail_timeout; /* “一段时间”的长度,固定值 */

 
  1. ngx_http_upstream_get_peeer  
  2.     /* 在一段时间内,如果此后端服务器的失败次数,超过了允许的最大值, 
  3.       * 那么在此后的一段时间内不允许使用此后端了。 
  4.       */  
  5.     if (peer->max_fails && peer->fails >= peer->max_fails &&  
  6.         now - peer->checked <= peer->fail_timeout)  
  7.         continue;  
  8.     ...  
  9.     /* 选定本后端了 */  
  10.     if (now - best->checked > best->fail_timeout)  
  11.         best->checked = now;  
 
  1. ngx_http_upstream_free_round_robin_peer  
  2.     if (state & NGX_PEER_FAILED) {  
  3.         peer->fails++;  
  4.         peer->accessed = now;  
  5.         peer->checked = now;  
  6.         ...  
  7.     } else if (peer->accessed < peer->checked)  
  8.         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。

 

下图用来简要说明

 

 

你可能感兴趣的:(Nginx的负载均衡 - 加权轮询 (Weighted Round Robin) 下篇)