Nginx版本:1.9.1
我的博客:[http://blog.csdn.net/zhangskd](http://blog.csdn.net/zhangskd)
?
**算法介紹**
?
ip_hash算法的原理很簡單,根據請求所屬的客戶端IP計算得到一個數值,然后把請求發往該數值對應的后端。
所以同一個客戶端的請求,都會發往同一臺后端,除非該后端不可用了。ip_hash能夠達到保持會話的效果。
ip_hash是基于round robin的,判斷后端是否可用的方法是一樣的。
?
第一步,根據客戶端IP計算得到一個數值。
hash1 = (hash0 * 113 + addr[0]) % 6271;
hash2 = (hash1 * 113 + addr[1]) % 6271;
hash3 = (hash2 * 113 + addr[2]) % 6271;
hash3就是計算所得的數值,它只和初始數值hash0以及客戶端的IP有關。
?
第二步,根據計算所得數值,找到對應的后端。
w = hash3 % total_weight;
while (w >= peer->weight) {
??? w -= peer->weight;
??? peer = peer->next;
??? p++;
}
total_weight為所有后端權重之和。遍歷后端鏈表時,依次減去每個后端的權重,直到w小于某個后端的權重。
選定的后端在鏈表中的序號為p。因為total_weight和每個后端的weight都是固定的,所以如果hash3值相同,
則找到的后端相同。
?
**指令的解析函數**
?
在一個upstream配置塊中,如果有ip_hash指令,表示使用ip_hash負載均衡算法。
ip_hash指令的解析函數為ngx_http_upstream_ip_hash,主要做了:
指定初始化此upstream塊的函數peer.init_upstream
指定此upstream塊中server指令支持的屬性
~~~
static char *ngx_http_upstream_ip_hash (ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
ngx_http_upstream_srv_conf_t *uscf;
/* 獲取對應的upstream配置塊 */
uscf = ngx_http_conf_get_module_srv_conf(cf, ngx_http_upstream_module);
if (uscf->peer.init_upstream)
ngx_conf_log_error(NGX_LOG_WARN, cf, 0, "load balancing method redefined");
/* 指定初始化此upstream塊的函數 */
uscf->peer.init_upstream = ngx_http_upstream_init_ip_hash;
/* 指定此upstream塊中server指令支持的屬性 */
uscf->flags = NGX_HTTP_UPSTREAM_CREATE
| NGX_HTTP_UPSTREAM_WEIGHT
| NGX_HTTP_UPSTREAM_MAX_FAILS
| NGX_HTTP_UPSTREAM_FAIL_TIMEOUT
| NGX_HTTP_UPSTREAM_DOWN;
return NGX_CONF_OK;
}
~~~
以下是upstream塊中server指令可支持的屬性
NGX_HTTP_UPSTREAM_CREATE:檢查是否重復創建,以及必要的參數是否填寫
NGX_HTTP_UPSTREAM_WEIGHT:server指令支持weight屬性
NGX_HTTP_UPSTREAM_MAX_FAILS:server指令支持max_fails屬性
NGX_HTTP_UPSTREAM_FAIL_TIMEOUT:server指令支持fail_timeout屬性
NGX_HTTP_UPSTREAM_DOWN:server指令支持down屬性
NGX_HTTP_UPSTREAM_BACKUP:server指令支持backup屬性
?
**初始化upstream塊**
?
執行完指令的解析函數后,緊接著會調用所有HTTP模塊的init main conf函數。
在執行ngx_http_upstream_module的init main conf函數時,會調用所有upstream塊的初始化函數。
對于使用ip_hash的upstream塊,其初始化函數(peer.init_upstream)就是上一步中指定的
ngx_http_upstream_init_ip_hash。它主要做了:
調用默認的初始化函數ngx_http_upstream_init_round_robin來創建和初始化后端集群,保存該upstream塊的數據
指定初始化請求的負載均衡數據的函數peer.init
?
因為臟活累活都讓默認的函數給干了,所以ngx_http_upstream_init_ip_hash的代碼就幾行:)
~~~
static ngx_int_t ngx_http_upstream_init_ip_hash (ngx_conf_t *cf, ngx_http_upstream_srv_conf_t *us)
{
if (ngx_http_upstream_init_round_robin(cf, us) != NGX_OK)
return NGX_ERROR;
us->peer.init = ngx_http_upstream_init_ip_hash_peer; /* 初始化請求負載均衡數據的函數 */
return NGX_OK;
}
~~~
?
**初始化請求的負載均衡數據**?
?
收到一個請求后,一般使用的反向代理模塊(upstream模塊)為ngx_http_proxy_module,
其NGX_HTTP_CONTENT_PHASE階段的處理函數為ngx_http_proxy_handler,在初始化upstream機制的
ngx_http_upstream_init_request函數中,調用在第二步中指定的peer.init,主要用于初始化請求的負載均衡數據。
對于ip_hash,peer.init實例為ngx_http_upstream_init_ip_hash_peer,主要做了:
調用round robin的per request負載均衡初始化函數,創建和初始化其per request負載均衡數據,即iphp->rrp。
重新指定peer.get,用于從集群中選取一臺后端服務器。
保存客戶端的地址,初始化ip_hash的per request負載均衡數據。
?
ip_hash的per request負載均衡數據的結構體為ngx_http_upstream_ip_hash_peer_data_t。
~~~
typedef struct {
ngx_http_upstream_rr_peer_data_t rrp; /* round robin的per request負載均衡數據 */
ngx_uint_t hash; /* 根據客戶端IP計算所得的hash值 */
u_char addrlen; /* 使用客戶端IP的后三個字節來計算hash值 */
u_char *addr; /* 客戶端的IP */
u_char tries; /* 已經嘗試了多少次 */
ngx_event_get_peer_pt get_rr_peer; /* round robin算法的peer.get函數 */
} ngx_http_upstream_ip_hash_peer_data_t;
~~~
~~~
static ngx_int_t ngx_http_upstream_init_ip_hash_peer (ngx_http_request_t *r, ngx_http_upstream_srv_conf_t *us)
{
struct sockaddr_in *sin;
...
ngx_http_upstream_ip_hash_peer_data_t *iphp;
/* 創建ip_hash的per request負載均衡數據的實例 */
iphp = ngx_palloc(r->pool, sizeof(ngx_http_upstream_ip_hash_peer_data_t));
if (iphp == NULL)
return NGX_ERROR;
/* 首先調用round robin的per request負載均衡數據的初始化函數,
* 創建和初始化round robin的per request負載均衡數據實例,即iphp->rrp。
*/
r->upstream->peer.data = &iphp->rrp;
if (ngx_http_upstream_init_round_robin_peer(r, us) != NGX_OK)
return NGX_ERROR:
/* 重新指定peer.get,用于從集群中選取一臺后端服務器 */
r->upstream->peer.get = ngx_http_upstream_get_ip_hash_peer;
/* 客戶端的地址類型 */
switch(r->connection->sockaddr->sa_family) {
case AF_INET:
sin = (struct sockaddr_in *) r->connection->sockaddr;
iphp->addr = (u_char *) &sin->sin_addr.s_addr; /* 客戶端的IP */
iphp->addrlen = 3; /* 使用客戶端IP的后三個字節來計算hash值 */
break;
#if (NGX_HAVE_INET6)
...
#endif
default:
iphp->addr = ngx_http_upstream_ip_hash_pseudo_addr;
iphp->addrlen = 3;
}
iphp->hash = 89;
iphp->tries = 0;
iphp->get_rr_peer = ngx_http_upstream_get_round_robin_peer; /* 保存round robin的peer.get函數 */
}
~~~
?
**選取一臺后端服務器**
?
一般upstream塊中會有多臺后端,那么對于本次請求,要選定哪一臺后端呢?
這時候第三步中r->upstream->peer.get指向的函數就派上用場了:
采用ip_hash算法,從集群中選出一臺后端來處理本次請求。 選定后端的地址保存在pc->sockaddr,pc為主動連接。
函數的返回值:
NGX_DONE:選定一個后端,和該后端的連接已經建立。之后會直接發送請求。
NGX_OK:選定一個后端,和該后端的連接尚未建立。之后會和后端建立連接。
NGX_BUSY:所有的后端(包括備份集群)都不可用。之后會給客戶端發送502(Bad Gateway)。
~~~
static ngx_int_t ngx_http_upstream_get_ip_hash_peer (ngx_peer_connection_t *pc, void *data)
{
ngx_http_upstream_ip_hash_peer_data_t *iphp = data; /* 請求的負載均衡數據 */
time_t now;
ngx_int_t w;
uintptr_t m;
ngx_uint_t i, n, p, hash;
ngx_http_upstream_rr_peer_t *peer;
...
/* 如果只有一臺后端,或者嘗試次數超過20次,則使用輪詢的方式來選取后端 */
if (iphp->tries > 20 || iphp->rrp.peers->single) {
return iphp->get_rr_peer(pc, &iphp->rrp);
}
now = ngx_time();
pc->cached = 0;
pc->connection = NULL;
hash = iphp->hash; /* 本次選取的初始hash值 */
for ( ; ; ) {
/* 根據客戶端IP、本次選取的初始hash值,計算得到本次最終的hash值 */
for (i = 0; i < (ngx_uint_t) iphp->addrlen; i++)
hash = (hash * 113 + iphp->addr[i]) % 6271;
/* total_weight和weight都是固定值 */
w = hash % iphp->rrp.peers->total_weight;
peer = iphp->rrp.peers->peer; /* 第一臺后端 */
p = 0;
while (w >= peer->weight) {
w -= peer->weight;
peer = peer->next;
p++;
}
/* 檢查第此后端在狀態位圖中對應的位,為1時表示不可用 */
n = p / (8 * sizeof(uintptr_t));
m = (uintptr_t) 1 << p % (8 * sizeof(uintptr_t));
if (iphp->rrp.tried[n] & m)
goto next;
/* 檢查后端是否永久不可用 */
if (peer->down)
goto next;
/* 在一段時間內,如果此后端服務器的失敗次數,超過了允許的最大值,那么不允許使用此后端了 */
if (peer->max_fails && peer->fails >= peer->max_fails &&
now - peer->checked <= peer->fail_timeout)
goto next;
break;
next:
/* 增加已嘗試的次數,如果超過20次,則使用輪詢的方式來選取后端 */
if (++iphp->tries > 20)
return iphp->get_rr_peer(pc, &iphp->rrp);
}
iphp->rrp.current = peer; /* 選定的可用后端 */
/* 保存選定的后端服務器的地址,之后會向這個地址發起連接 */
pc->sockaddr = peer->sockaddr;
pc->socklen = peer->socklen;
pc->name = &peer->name;
peer->conns++;
/* 更新checked時間 */
if (now - peer->checked > peer->fail_timeout)
peer->checked = now;
iphp->rrp.tried[n] |= m; /* 對于此請求,如果之后需要再次選取后端,不能再選取這個后端了 */
iphp->hash = hash; /* 保存hash值,下次可能還會用到 */
return NGX_OK:
}
~~~
?