Nginx版本:1.9.1
我的博客:[http://blog.csdn.net/zhangskd](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。
?
下圖用來簡要說明
?
?
?
?