**目錄**
- [負載均衡](#)
- [加權輪詢](#)
- [相關結構體](#)
- [加權輪詢策略的啟動](#)
- [加權輪詢工作流程](#)
- [初始化服務器列表](#)
- [選擇合適的后端服務器](#)
- [初始化后端服務器](#)
- [根據權重選擇后端服務器](#)
- [釋放后端服務器](#)
- [IP 哈希](#)
- [初始化后端服務器列表](#)
- [選擇后端服務器](#)
- [總結](#)
### 負載均衡
upstream 機制使得 Nginx 以反向代理的形式運行,因此 Nginx 接收客戶端的請求,并根據客戶端的請求,Nginx 選擇合適后端服務器來處理該請求。但是若存在多臺后端服務器時,Nginx 是根據怎樣的策略來決定哪個后端服務器負責處理請求?這就涉及到后端服務器的負載均衡問題。
Nginx 的負載均衡策略可以劃分為兩大類:內置策略 和 擴展策略。內置策略包含 加權輪詢 和 IP hash,在默認情況下這兩種策略會編譯進 Nginx 內核,只需在 Nginx 配置中指明參數即可。擴展策略有第三方模塊策略:fair、URL hash、consistent hash等,默認不編譯進 Nginx 內核。本文只講解 加權輪詢 和 IP_hash 策略。
### 加權輪詢
加權輪詢策略是先計算每個后端服務器的權重,然后選擇權重最高的后端服務器來處理請求。
### 相關結構體
**ngx_http_upstream_peer_t 結構體**
~~~
typedef struct {
/* 負載均衡的類型 */
ngx_http_upstream_init_pt init_upstream;
/* 負載均衡類型的初始化函數 */
ngx_http_upstream_init_peer_pt init;
/* 指向 ngx_http_upstream_rr_peers_t 結構體 */
void *data;
} ngx_http_upstream_peer_t;
~~~
**ngx_http_upstream_server_t 結構體**
~~~
/* 服務器結構體 */
typedef struct {
/* 指向存儲 IP 地址的數組,因為同一個域名可能會有多個 IP 地址 */
ngx_addr_t *addrs;
/* IP 地址數組中元素個數 */
ngx_uint_t naddrs;
/* 權重 */
ngx_uint_t weight;
/* 最大失敗次數 */
ngx_uint_t max_fails;
/* 失敗時間閾值 */
time_t fail_timeout;
/* 標志位,若為 1,表示不參與策略選擇 */
unsigned down:1;
/* 標志位,若為 1,表示為備用服務器 */
unsigned backup:1;
} ngx_http_upstream_server_t;
~~~
**ngx_http_upstream_rr_peer_t 結構體**
~~~
typedef struct {
/* 后端服務器 IP 地址 */
struct sockaddr *sockaddr;
/* 后端服務器 IP 地址的長度 */
socklen_t socklen;
/* 后端服務器的名稱 */
ngx_str_t name;
/* 后端服務器當前的權重 */
ngx_int_t current_weight;
/* 后端服務器有效權重 */
ngx_int_t effective_weight;
/* 配置項所指定的權重 */
ngx_int_t weight;
/* 已經失敗的次數 */
ngx_uint_t fails;
/* 訪問時間 */
time_t accessed;
time_t checked;
/* 最大失敗次數 */
ngx_uint_t max_fails;
/* 失敗時間閾值 */
time_t fail_timeout;
/* 后端服務器是否參與策略,若為1,表示不參與 */
ngx_uint_t down; /* unsigned down:1; */
#if (NGX_HTTP_SSL)
ngx_ssl_session_t *ssl_session; /* local to a process */
#endif
} ngx_http_upstream_rr_peer_t;
~~~
**ngx_http_upstream_rr_peers_t 結構體**
~~~
typedef struct ngx_http_upstream_rr_peers_s ngx_http_upstream_rr_peers_t;
struct ngx_http_upstream_rr_peers_s {
/* 競選隊列中后端服務器的數量 */
ngx_uint_t number;
/* ngx_mutex_t *mutex; */
/* 所有后端服務器總的權重 */
ngx_uint_t total_weight;
/* 標志位,若為 1,表示后端服務器僅有一臺,此時不需要選擇策略 */
unsigned single:1;
/* 標志位,若為 1,表示所有后端服務器總的權重等于服務器的數量 */
unsigned weighted:1;
ngx_str_t *name;
/* 后端服務器的鏈表 */
ngx_http_upstream_rr_peers_t *next;
/* 特定的后端服務器 */
ngx_http_upstream_rr_peer_t peer[1];
};
~~~
**ngx_http_upstream_rr_peer_data_t 結構體**
~~~
typedef struct {
ngx_http_upstream_rr_peers_t *peers;
ngx_uint_t current;
uintptr_t *tried;
uintptr_t data;
} ngx_http_upstream_rr_peer_data_t;
~~~
### 加權輪詢策略的啟動
在 Nginx 啟動過程中,在解析完 http 配置塊之后,會調用各個 http 模塊對應的初始函數。對于 upstream 機制的 `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;
}
}
~~~
在 `ngx_http_upstream_module` 模塊中,如果用戶沒有做任何策略選擇,那么執行默認采用加權輪詢策略初始函數為`ngx_http_upstream_init_round_robin`。否則的話執行的是`uscfp[i]->peer.init_upstream`指針函數。
當接收到來自客戶端的請求時,Nginx 會調用 `ngx_http_upstream_init_request` 初始化請求的過程中,調用 `uscf->peer.init(r, uscf)`,對于 upstream 機制的加權輪詢策略來說該方法就是 `ngx_http_upstream_init_round_robin_peer`,該方法完成請求初始化工作。
~~~
static void
ngx_http_upstream_init_request(ngx_http_request_t *r)
{
...
if (uscf->peer.init(r, uscf) != NGX_OK) {
ngx_http_upstream_finalize_request(r, u,
NGX_HTTP_INTERNAL_SERVER_ERROR);
return;
}
ngx_http_upstream_connect(r, u);
}
~~~
完成客戶端請求的初始化工作之后,會選擇一個后端服務器來處理該請求,選擇后端服務器由函數 `ngx_http_upstream_get_round_robin_peer` 實現。該函數在 `ngx_event_connect_peer`中被調用。
~~~
ngx_int_t
ngx_event_connect_peer(ngx_peer_connection_t *pc)
{
...
/* 調用 ngx_http_upstream_get_round_robin_peer */
rc = pc->get(pc, pc->data);
if (rc != NGX_OK) {
return rc;
}
s = ngx_socket(pc->sockaddr->sa_family, SOCK_STREAM, 0);
...
}
~~~
當已經選擇一臺后端服務器來處理請求時,接下來就會測試該后端服務器的連接情況,測試連接由函數 `ngx_http_upstream_test_connect` 實現,在函數 `ngx_http_upstream_send_request` 中被調用。
~~~
static void
ngx_http_upstream_send_request(ngx_http_request_t *r, ngx_http_upstream_t *u)
{
...
if (!u->request_sent && ngx_http_upstream_test_connect(c) != NGX_OK) {
/* 測試連接失敗 */
ngx_http_upstream_next(r, u, NGX_HTTP_UPSTREAM_FT_ERROR);
return;
}
...
}
~~~
若連接測試失敗,會由函數 `ngx_http_upstream_next` 發起再次測試,若測試成功,則處理完請求之后,會調用 `ngx_http_upstream_free_round_robin_peer` 釋放后端服務器。
### 加權輪詢工作流程
加權輪詢策略的基本工作過程是:初始化負載均衡服務器列表,初始化后端服務器,選擇合適后端服務器處理請求,釋放后端服務器。
#### 初始化服務器列表
初始化服務器列表由函數 `ngx_http_upstream_init_round_robin` 實現,該函數的執行流程如下所示:
- 第一種情況:若 upstream 機制配置項中配置了服務器:
- 初始化非備用服務器列表,并將其掛載到 `us->peer.data` 中;
- 初始化備用服務器列表,并將其掛載到 `peers->next` 中;
- 第二種情況:采用默認的方式 proxy_pass 配置后端服務器地址;
- 初始化非備用服務器列表,并將其掛載到 `us->peer.data` 中;
該方法執行完成之后得到的結構如下圖所示:

~~~
/* 初始化服務器負載均衡列表 */
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_peers_t *peers, *backup;
/* 設置 ngx_http_upstream_peer_t 結構體中 init 的回調方法 */
us->peer.init = ngx_http_upstream_init_round_robin_peer;
/* 第一種情況:若 upstream 機制中有配置后端服務器 */
if (us->servers) {
/* ngx_http_upstream_srv_conf_t us 結構體成員 servers 是一個指向服務器數組 ngx_array_t 的指針,*/
server = us->servers->elts;
n = 0;
w = 0;
/* 在這里說明下:一個域名可能會對應多個 IP 地址,upstream 機制中把一個 IP 地址看作一個后端服務器 */
/* 遍歷服務器數組中所有后端服務器,統計非備用后端服務器的 IP 地址總個數(即非備用后端服務器總的個數) 和 總權重 */
for (i = 0; i < us->servers->nelts; i++) {
/* 若當前服務器是備用服務器,則 continue 跳過以下檢查,繼續檢查下一個服務器 */
if (server[i].backup) {ngx_http_upstream_peer_t
continue;
}
/* 統計所有非備用后端服務器 IP 地址總的個數(即非備用后端服務器總的個數) */
n += server[i].naddrs;
/* 統計所有非備用后端服務器總的權重 */
w += server[i].naddrs * server[i].weight;
}
/* 若 upstream 機制中配置項指令沒有設置后端服務器,則出錯返回 */
if (n == 0) {
ngx_log_error(NGX_LOG_EMERG, cf->log, 0,
"no servers in upstream \"%V\" in %s:%ui",
&us->host, us->file_name, us->line);
return NGX_ERROR;
}
/* 值得注意的是:備用后端服務器列表 和 非備用后端服務器列表 是分開掛載的,因此需要分開設置 */
/* 為非備用后端服務器分配內存空間 */
peers = ngx_pcalloc(cf->pool, sizeof(ngx_http_upstream_rr_peers_t)
+ sizeof(ngx_http_upstream_rr_peer_t) * (n - 1));
if (peers == NULL) {
return NGX_ERROR;
}
/* 初始化非備用后端服務器列表 ngx_http_upstream_rr_peers_t 結構體 */
peers->single = (n == 1);/* 表示只有一個非備用后端服務器 */
peers->number = n;/* 非備用后端服務器總的個數 */
peers->weighted = (w != n);/* 設置默認權重為 1 或 0 */
peers->total_weight = w;/* 設置非備用后端服務器總的權重 */
peers->name = &us->host;/* 非備用后端服務器名稱 */
n = 0;
/* 遍歷服務器數組中所有后端服務器,初始化非備用后端服務器 */
for (i = 0; i < us->servers->nelts; i++) {
if (server[i].backup) {/* 若為備用服務器則 continue 跳過 */
continue;
}
/* 以下關于 ngx_http_upstream_rr_peer_t 結構體中三個權重值的說明 */
/*
* effective_weight 相當于質量(來源于配置文件配置項的 weight),current_weight 相當于重量。
* 前者反應本質,一般是不變的。current_weight 是運行時的動態權值,它的變化基于 effective_weight。
* 但是 effective_weight 在其對應的 peer 服務異常時,會被調低,
* 當服務恢復正常時,effective_weight 會逐漸恢復到實際值(配置項的weight);
*/
/* 遍歷非備用后端服務器所對應 IP 地址數組中的所有 IP 地址(即一個后端服務器域名可能會對應多個 IP 地址) */
for (j = 0; j < server[i].naddrs; j++) {
/* 為每個非備用后端服務器初始化 */
peers->peer[n].sockaddr = server[i].addrs[j].sockaddr;/* 設置非備用后端服務器 IP 地址 */
peers->peer[n].socklen = server[i].addrs[j].socklen;/* 設置非備用后端服務器 IP 地址長度 */
peers->peer[n].name = server[i].addrs[j].name;/* 設置非備用后端服務器域名 */
peers->peer[n].weight = server[i].weight;/* 設置非備用后端服務器配置項權重 */
peers->peer[n].effective_weight = server[i].weight;/* 設置非備用后端服務器有效權重 */
peers->peer[n].current_weight = 0;/* 設置非備用后端服務器當前權重 */
peers->peer[n].max_fails = server[i].max_fails;/* 設置非備用后端服務器最大失敗次數 */
peers->peer[n].fail_timeout = server[i].fail_timeout;/* 設置非備用后端服務器失敗時間閾值 */
peers->peer[n].down = server[i].down;/* 設置非備用后端服務器 down 標志位,若該標志位為 1,則不參與策略 */
n++;
}
}
/*
* 將非備用服務器列表掛載到 ngx_http_upstream_srv_conf_t 結構體成員結構體
* ngx_http_upstream_peer_t peer 的成員 data 中;
*/
us->peer.data = peers;
/* backup servers */
n = 0;
w = 0;
/* 遍歷服務器數組中所有后端服務器,統計備用后端服務器的 IP 地址總個數(即備用后端服務器總的個數) 和 總權重 */
for (i = 0; i < us->servers->nelts; i++) {
if (!server[i].backup) {
continue;
}
n += server[i].naddrs;/* 統計所有備用后端服務器的 IP 地址總的個數 */
w += server[i].naddrs * server[i].weight;/* 統計所有備用后端服務器總的權重 */
}
if (n == 0) {/* 若沒有備用后端服務器,則直接返回 */
return NGX_OK;
}
/* 分配備用服務器列表的內存空間 */
backup = ngx_pcalloc(cf->pool, sizeof(ngx_http_upstream_rr_peers_t)
+ sizeof(ngx_http_upstream_rr_peer_t) * (n - 1));
if (backup == NULL) {
return NGX_ERROR;
}
peers->single = 0;
/* 初始化備用后端服務器列表 ngx_http_upstream_rr_peers_t 結構體 */
backup->single = 0;
backup->number = n;
backup->weighted = (w != n);
backup->total_weight = w;
backup->name = &us->host;
n = 0;
/* 遍歷服務器數組中所有后端服務器,初始化備用后端服務器 */
for (i = 0; i < us->servers->nelts; i++) {
if (!server[i].backup) {/* 若是非備用后端服務器,則 continue 跳過當前后端服務器,檢查下一個后端服務器 */
continue;
}
/* 遍歷備用后端服務器所對應 IP 地址數組中的所有 IP 地址(即一個后端服務器域名可能會對應多個 IP 地址) */
for (j = 0; j < server[i].naddrs; j++) {
backup->peer[n].sockaddr = server[i].addrs[j].sockaddr;/* 設置備用后端服務器 IP 地址 */
backup->peer[n].socklen = server[i].addrs[j].socklen;/* 設置備用后端服務器 IP 地址長度 */
backup->peer[n].name = server[i].addrs[j].name;/* 設置備用后端服務器域名 */
backup->peer[n].weight = server[i].weight;/* 設置備用后端服務器配置項權重 */
backup->peer[n].effective_weight = server[i].weight;/* 設置備用后端服務器有效權重 */
backup->peer[n].current_weight = 0;/* 設置備用后端服務器當前權重 */
backup->peer[n].max_fails = server[i].max_fails;/* 設置備用后端服務器最大失敗次數 */
backup->peer[n].fail_timeout = server[i].fail_timeout;/* 設置備用后端服務器失敗時間閾值 */
backup->peer[n].down = server[i].down;/* 設置備用后端服務器 down 標志位,若該標志位為 1,則不參與策略 */
n++;
}
}
/*
* 將備用服務器列表掛載到 ngx_http_upstream_rr_peers_t 結構體中
* 的成員 next 中;
*/
peers->next = backup;
/* 第一種情況到此返回 */
return NGX_OK;
}
/* 第二種情況:若 upstream 機制中沒有直接配置后端服務器,則采用默認的方式 proxy_pass 配置后端服務器地址 */
/* an upstream implicitly defined by proxy_pass, etc. */
/* 若端口號為 0,則出錯返回 */
if (us->port == 0) {
ngx_log_error(NGX_LOG_EMERG, cf->log, 0,
"no port in upstream \"%V\" in %s:%ui",
&us->host, us->file_name, us->line);
return NGX_ERROR;
}
/* 初始化 ngx_url_t 結構體所有成員為 0 */
ngx_memzero(&u, sizeof(ngx_url_t));
u.host = us->host;
u.port = us->port;
/* 解析 IP 地址 */
if (ngx_inet_resolve_host(cf->pool, &u) != NGX_OK) {
if (u.err) {
ngx_log_error(NGX_LOG_EMERG, cf->log, 0,
"%s in upstream \"%V\" in %s:%ui",
u.err, &us->host, us->file_name, us->line);
}
return NGX_ERROR;
}
n = u.naddrs;
/* 分配非備用后端服務器列表的內存空間 */
peers = ngx_pcalloc(cf->pool, sizeof(ngx_http_upstream_rr_peers_t)
+ sizeof(ngx_http_upstream_rr_peer_t) * (n - 1));
if (peers == NULL) {
return NGX_ERROR;
}
/* 初始化非備用后端服務器列表 */
peers->single = (n == 1);
peers->number = n;
peers->weighted = 0;
peers->total_weight = n;
peers->name = &us->host;
for (i = 0; i < u.naddrs; i++) {
peers->peer[i].sockaddr = u.addrs[i].sockaddr;
peers->peer[i].socklen = u.addrs[i].socklen;
peers->peer[i].name = u.addrs[i].name;
peers->peer[i].weight = 1;
peers->peer[i].effective_weight = 1;
peers->peer[i].current_weight = 0;
peers->peer[i].max_fails = 1;
peers->peer[i].fail_timeout = 10;
}
/* 掛載非備用后端服務器列表 */
us->peer.data = peers;
/* implicitly defined upstream has no backup servers */
return NGX_OK;
}
~~~
#### 選擇合適的后端服務器
在選擇合適的后端服務器處理客戶請求時,首先需要初始化后端服務器,然后根據后端服務器的權重,選擇權重最高的后端服務器來處理請求。
##### 初始化后端服務器
上面的初始化負載服務器列表的全局初始化工作完成之后,當客戶端發起請求時,Nginx 會選擇一個合適的后端服務器來處理該請求。在本輪選擇后端服務器之前,Nginx 會對后端服務器進行初始化工作,該工作由函數 `ngx_http_upstream_init_round_robin_peer` 實現。
`ngx_http_upstream_init_round_robin_peer` 函數的執行流程如下所示:
- 計算服務器列表中的數量 n,n 的取值為 非備用后端服務器數量 與 備用后端服務器數量 較大者;
- 根據 n 的取值,創建一個位圖 `tried`,該位圖是記錄后端服務器是否被選擇過:
- 若 n 不大于 32, 只需要在一個 `int` 中記錄所有后端服務器的狀態;
- 若 n 大于 32,則需要從內存池申請內存來存儲所有后端服務器的狀態;
- 設置 `ngx_peer_connection_t` 結構體中 `get` 的回調方法為 `ngx_http_upstream_get_round_robin_peer`;`free` 的回調方法為 `ngx_http_upstream_free_round_robin_peer`,設置 `tries` 重試連接的次數為非備用后端服務器的個數;
~~~
/* 當客戶端發起請求時,upstream 機制為本輪選擇一個后端服務器做初始化工作 */
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;
/* 注意:r->upstream->peer 是 ngx_peer_connection_t 結構體類型 */
/* 獲取當前客戶端請求中的 ngx_http_upstream_rr_peer_data_t 結構體 */
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;
rrp->current = 0;/* 若采用遍歷方式選擇后端服務器時,作為起始節點編號 */
/* 下面是取值 n,若存在備用后端服務器列表,則 n 的值為非備用后端服務器個數 與 備用后端服務器個數 之間的較大者 */
n = rrp->peers->number;
if (rrp->peers->next && rrp->peers->next->number > n) {
n = rrp->peers->next->number;
}
/* rrp->tried 是一個位圖,在本輪選擇中,該位圖記錄各個后端服務器是否被選擇過 */
/*
* 如果后端服務器數量 n 不大于 32,則只需在一個 int 中即可記錄下所有后端服務器狀態;
* 如果后端服務器數量 n 大于 32,則需在內存池中申請內存來存儲所有后端服務器的狀態;
*/
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;
}
}
/*
* 設置 ngx_peer_connection_t 結構體中 get 、free 的回調方法;
* 設置 ngx_peer_connection_t 結構體中 tries 重試連接的次數為非備用后端服務器的個數;
*/
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;
#if (NGX_HTTP_SSL)
r->upstream->peer.set_session =
ngx_http_upstream_set_round_robin_peer_session;
r->upstream->peer.save_session =
ngx_http_upstream_save_round_robin_peer_session;
#endif
return NGX_OK;
}
~~~
##### 根據權重選擇后端服務器
完成后端服務器的初始化工作之后,根據各個后端服務器的權重來選擇權重最高的后端服務器處理客戶端請求,由函數 `ngx_http_upstream_get_round_robin_peer` 實現。
`ngx_http_upstream_get_round_robin_peer` 函數的執行流程如下所示:
- *步驟1*:檢查 `ngx_http_upstream_rr_peers_t` 結構體中的 `single` 標志位:
- 若 `single` 標志位為 1,表示只有一臺非備用后端服務器:
- 接著檢查該非備用后端服務器的 `down` 標志位:
- 若 `down` 標志位為 0,則選擇該非備用后端服務器來處理請求;
- 若 `down` 標志位為 1, 該非備用后端服務器表示不參與策略選擇,則跳至 `goto failed` 步驟從備用后端服務器列表中選擇后端服務器來處理請求;
- 若 `single` 標志位為 0,則表示不止一臺非備用后端服務器,則調用 `ngx_http_upstream_get_peer` 方法根據非備用后端服務器的權重來選擇一臺后端服務器處理請求,根據該方法的返回值 `peer` 進行判斷:
- 若該方法返回值 `peer = NULL`, 表示在非備用后端服務器列表中沒有選中到合適的后端服務器來處理請求,則跳至 `goto failed` 從備用后端服務器列表中選擇一臺后端服務器來處理請求;
- 若該方法返回值 peer 不為 NULL,表示已經選中了合適的后端服務器來處理請求,設置該服務器重試連接次數 tries,并 `return NGX_OK` 從當前函數返回;
- *goto failed 步驟*:計算備用后端服務器在位圖 tried 中的位置 n,并把他們在位圖的記錄都設置為 0,此時,把備用后端服務器列表作為參數調用 `ngx_http_upstream_get_round_robin_peer` 選擇一臺后端服務器來處理請求;
~~~
/* 選擇一個后端服務器來處理請求 */
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;
ngx_log_debug1(NGX_LOG_DEBUG_HTTP, pc->log, 0,
"get rr peer, try: %ui", pc->tries);
/* ngx_lock_mutex(rrp->peers->mutex); */
pc->cached = 0;
pc->connection = NULL;
/*
* 檢查 ngx_http_upstream_rr_peers_t 結構體中的 single 標志位;
* 若 single 標志位為 1,表示只有一臺非備用后端服務器,
* 接著檢查該非備用后端服務器的 down 標志位,若 down 標志位為 0,則選擇該非備用后端服務器來處理請求;
* 若 down 標志位為 1, 該非備用后端服務器表示不參與策略選擇,
* 則跳至 goto failed 步驟從備用后端服務器列表中選擇后端服務器來處理請求;
*/
if (rrp->peers->single) {
peer = &rrp->peers->peer[0];
if (peer->down) {
goto failed;
}
} else {/* 若 single 標志位為 0,表示不止一臺非備用后端服務器 */
/* there are several peers */
/* 根據非備用后端服務器的權重來選擇一臺后端服務器處理請求 */
peer = ngx_http_upstream_get_peer(rrp);
if (peer == NULL) {
/*
* 若從非備用后端服務器列表中沒有選擇一臺合適的后端服務器處理請求,
* 則 goto failed 從備用后端服務器列表中選擇一臺后端服務器來處理請求;
*/
goto failed;
}
ngx_log_debug2(NGX_LOG_DEBUG_HTTP, pc->log, 0,
"get rr peer, current: %ui %i",
rrp->current, peer->current_weight);
}
/*
* 若從非備用后端服務器列表中已經選到了一臺合適的后端服務器處理請求;
* 則獲取該后端服務器的地址信息;
*/
pc->sockaddr = peer->sockaddr;/* 獲取被選中的非備用后端服務器的地址 */
pc->socklen = peer->socklen;/* 獲取被選中的非備用后端服務器的地址長度 */
pc->name = &peer->name;/* 獲取被選中的非備用后端服務器的域名 */
/* ngx_unlock_mutex(rrp->peers->mutex); */
/*
* 檢查被選中的非備用后端服務器重試連接的次數為 1,且存在備用后端服務器列表,
* 則將該非備用后端服務器重試連接的次數設置為 備用后端服務器個數加 1;
* 否則不用重新設置;
*/
if (pc->tries == 1 && rrp->peers->next) {
pc->tries += rrp->peers->next->number;
}
/* 到此,表示已經選擇到了一臺合適的非備用后端服務器來處理請求,則成功返回 */
return NGX_OK;
failed:
/*
* 若從非備用后端服務器列表中沒有選擇到后端服務器處理請求,
* 若存在備用后端服務器,則從備用后端服務器列表中選擇一臺后端服務器來處理請求;
*/
peers = rrp->peers;
/* 若存在備用后端服務器,則從備用后端服務器列表中選擇一臺后端服務器來處理請求;*/
if (peers->next) {
/* ngx_unlock_mutex(peers->mutex); */
ngx_log_debug0(NGX_LOG_DEBUG_HTTP, pc->log, 0, "backup servers");
/* 獲取備用后端服務器列表 */
rrp->peers = peers->next;
/* 把后端服務器重試連接的次數 tries 設置為備用后端服務器個數 number */
pc->tries = rrp->peers->number;
/* 計算備用后端服務器在位圖中的位置 n */
n = (rrp->peers->number + (8 * sizeof(uintptr_t) - 1))
/ (8 * sizeof(uintptr_t));
/* 初始化備用后端服務器在位圖 rrp->tried[i] 中的值為 0 */
for (i = 0; i < n; i++) {
rrp->tried[i] = 0;
}
/* 把備用后端服務器列表當前非備用后端服務器列表遞歸調用 ngx_http_upstream_get_round_robin_peer 選擇一臺后端服務器 */
rc = ngx_http_upstream_get_round_robin_peer(pc, rrp);
/* 若選擇成功則返回 */
if (rc != NGX_BUSY) {
return rc;
}
/* ngx_lock_mutex(peers->mutex); */
}
/*
* 若從備用后端服務器列表中也沒有選擇到一臺后端服務器處理請求,
* 則重新設置非備用后端服務器連接失敗的次數 fails 為 0 ,以便重新被選擇;
*/
/* all peers failed, mark them as live for quick recovery */
for (i = 0; i < peers->number; i++) {
peers->peer[i].fails = 0;
}
/* ngx_unlock_mutex(peers->mutex); */
pc->name = peers->name;
/* 選擇失敗,則返回 */
return NGX_BUSY;
}
~~~
`ngx_http_upstream_get_peer` 函數是計算每一個后端服務器的權重值,并選擇一個權重最高的后端服務器。
`ngx_http_upstream_get_peer` 函數的執行流程如下所示:
- for 循環遍歷后端服務器列表,計算當前后端服務器在位圖 tried 中的位置 n,判斷當前服務器是否在位圖中記錄過,若已經記錄過,則 continue 繼續檢查下一個后端服務器;若沒有記錄過則繼續當前后端服務器檢查;
- 檢查當前后端服務器的標志位 down,若該標志位為 1,表示該后端服務器不參與選擇策略,則 continue 繼續檢查下一個后端服務器;若該標志位為 0,繼續當前后端服務器的檢查;
- 若當前后端服務器的連接失敗次數已到達 max_failes,且睡眠時間還沒到 fail_timedout ,則 continue 繼續檢查下一個后端服務器;否則繼續當前后端服務器的檢查;
- 計算當前后端服務器的權重,設置當前后端服務器的權重 current_weight 的值為原始值加上 effective_weight;設置總的權重 total 為原始值加上 effective_weight;
- 判斷當前后端服務器是否異常,若 effective_weight 小于 weight,表示正常,則調整 effective_weight 的值 effective_weight++;
- 根據權重在后端服務器列表中選擇權重最高的后端服務器 best;
- 計算被選中后端服務器咋服務器列表中的為 i,記錄被選中后端服務器在 `ngx_http_upstream_rr_peer_data_t` 結構體 current 成員的值,在釋放后端服務器時會用到該值;
- 計算被選中后端服務器在位圖中的位置 n,并在該位置記錄 best 后端服務器已經被選中過;
- 更新被選中后端服務器的權重,并返回被選中的后端服務器 best;
~~~
/* 根據后端服務器的權重來選擇一臺后端服務器處理請求 */
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;
ngx_http_upstream_rr_peer_t *peer, *best;
now = ngx_time();
best = NULL;
total = 0;
/* 遍歷后端服務器列表 */
for (i = 0; i < rrp->peers->number; i++) {
/* 計算當前后端服務器在位圖中的位置 n */
n = i / (8 * sizeof(uintptr_t));
m = (uintptr_t) 1 << i % (8 * sizeof(uintptr_t));
/* 當前后端服務器在位圖中已經有記錄,則不再次被選擇,即 continue 檢查下一個后端服務器 */
if (rrp->tried[n] & m) {
continue;
}
/* 若當前后端服務器在位圖中沒有記錄,則可能被選中,接著計算其權重 */
peer = &rrp->peers->peer[i];
/* 檢查當前后端服務器的 down 標志位,若為 1 表示不參與策略選擇,則 continue 檢查下一個后端服務器 */
if (peer->down) {
continue;
}
/*
* 當前后端服務器的 down 標志位為 0,接著檢查當前后端服務器連接失敗的次數是否已經達到 max_fails;
* 且睡眠的時間還沒到 fail_timeout,則當前后端服務器不被選擇,continue 檢查下一個后端服務器;
*/
if (peer->max_fails
&& peer->fails >= peer->max_fails
&& now - peer->checked <= peer->fail_timeout)
{
continue;
}
/* 若當前后端服務器可能被選中,則計算其權重 */
/*
* 在上面初始化過程中 current_weight = 0,effective_weight = weight;
* 此時,設置當前后端服務器的權重 current_weight 的值為原始值加上 effective_weight;
* 設置總的權重為原始值加上 effective_weight;
*/
peer->current_weight += peer->effective_weight;
total += peer->effective_weight;
/* 服務器正常,調整 effective_weight 的值 */
if (peer->effective_weight < peer->weight) {
peer->effective_weight++;
}
/* 若當前后端服務器的權重 current_weight 大于目前 best 服務器的權重,則當前后端服務器被選中 */
if (best == NULL || peer->current_weight > best->current_weight) {
best = peer;
}
}
if (best == NULL) {
return NULL;
}
/* 計算被選中后端服務器在服務器列表中的位置 i */
i = best - &rrp->peers->peer[0];
/* 記錄被選中后端服務器在 ngx_http_upstream_rr_peer_data_t 結構體 current 成員的值,在釋放后端服務器時會用到該值 */
rrp->current = i;
/* 計算被選中后端服務器在位圖中的位置 */
n = i / (8 * sizeof(uintptr_t));
m = (uintptr_t) 1 << i % (8 * sizeof(uintptr_t));
/* 在位圖相應的位置記錄被選中后端服務器 */
rrp->tried[n] |= m;
/* 更新被選中后端服務器的權重 */
best->current_weight -= total;
if (now - best->checked > best->fail_timeout) {
best->checked = now;
}
/* 返回被選中的后端服務器 */
return best;
}
~~~
#### 釋放后端服務器
成功連接后端服務器并且正常處理完成客戶端請求后需釋放后端服務器,由函數 `ngx_http_upstream_free_round_robin_peer` 實現。
~~~
/* 釋放后端服務器 */
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;
ngx_log_debug2(NGX_LOG_DEBUG_HTTP, pc->log, 0,
"free rr peer %ui %ui", pc->tries, state);
/* TODO: NGX_PEER_KEEPALIVE */
/* 若只有一個后端服務器,則設置 ngx_peer_connection_t 結構體成員 tries 為 0,并 return 返回 */
if (rrp->peers->single) {
pc->tries = 0;
return;
}
/* 若不止一個后端服務器,則執行以下程序 */
/* 獲取已經被選中的后端服務器 */
peer = &rrp->peers->peer[rrp->current];
/*
* 若在本輪被選中的后端服務器在進行連接測試時失敗,或者在處理請求過程中失敗,
* 則需要進行重新選擇后端服務器;
*/
if (state & NGX_PEER_FAILED) {
now = ngx_time();
/* ngx_lock_mutex(rrp->peers->mutex); */
peer->fails++;/* 增加當前后端服務器失敗的次數 */
/* 設置當前后端服務器訪問的時間 */
peer->accessed = now;
peer->checked = now;
if (peer->max_fails) {
/* 由于當前后端服務器失敗,表示發生異常,此時降低 effective_weight 的值 */
peer->effective_weight -= peer->weight / peer->max_fails;
}
ngx_log_debug2(NGX_LOG_DEBUG_HTTP, pc->log, 0,
"free rr peer failed: %ui %i",
rrp->current, peer->effective_weight);
/* 保證 effective_weight 的值不能小于 0 */
if (peer->effective_weight < 0) {
peer->effective_weight = 0;
}
/* ngx_unlock_mutex(rrp->peers->mutex); */
} else {/* 若被選中的后端服務器成功處理請求,并返回,則將其 fails 設置為 0 */
/* mark peer live if check passed */
/* 若 fail_timeout 時間已過,則將其 fails 設置為 0 */
if (peer->accessed < peer->checked) {
peer->fails = 0;
}
}
/* 減少 tries 的值 */
if (pc->tries) {
pc->tries--;
}
/* ngx_unlock_mutex(rrp->peers->mutex); */
}
~~~
### IP 哈希
IP 哈希策略選擇后端服務器時,將來自同一個 IP 地址的客戶端請求分發到同一臺后端服務器處理。在 Nginx 中,IP 哈希策略的一些初始化工作是基于加權輪詢策略的,這樣減少了一些工作。
Nginx 使用 IP 哈希負載均衡策略時,在進行策略選擇之前由 `ngx_http_upstream_init_ip_hash` 函數進行全局初始化工作,其實該函數也是調用加權輪詢策略的全局初始化函數。當一個客戶端請求過來時,Nginx 將調用 `ngx_http_upstream_init_ip_hash_peer()` 為選擇后端服務器處理該請求做初始化工作。在多次哈希選擇失敗后,Nginx 會將選擇策略退化到加權輪詢。
`ngx_http_upstream_get_ip_hash_peer` 函數會在選擇后端服務器時計算客戶端請求 IP 地址的哈希值,并根據哈希值得到被選中的后端服務器,判斷其是否可用,如果可用則保存服務器地址,若不可用則在上次哈希選擇結果基礎上再次進行哈希選擇。如果哈希選擇失敗次數達到 20 次以上,此時回退到采用輪詢策略進行選擇。
### 初始化后端服務器列表
初始化服務器列表工作是調用加權輪詢策略的初始化函數,只是最后設置 IP 哈希的回調方法為 `ngx_http_upstream_init_ip_hash_peer`。
~~~
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;
}
/* 由于 ngx_http_upstream_init_round_robin 方法的選擇后端服務器處理客戶請求的初始化函數
* 為 us->peer.init = ngx_http_upstream_init_round_robin_peer;
*/
/* 重新設置 ngx_http_upstream_peer_t 結構體中 init 的回調方法為 ngx_http_upstream_init_ip_hash_peer */
us->peer.init = ngx_http_upstream_init_ip_hash_peer;
return NGX_OK;
}
~~~
### 選擇后端服務器
選擇后端服務器之前會調用函數 `ngx_http_upstream_init_ip_hash_peer` 進行一些服務器初始化工作。最終由函數 `ngx_http_upstream_get_ip_hash_peer` 進行 IP 哈希選擇。
`ngx_http_upstream_init_ip_hash_peer` 函數執行流程:
- 調用加權輪詢策略的初始化函數 `ngx_http_upstream_init_round_robin_peer`;
- 設置 IP hash 的決策函數為 `ngx_http_upstream_get_ip_hash_peer`;
- 保存客戶端 IP 地址;
- 初始化 `ngx_http_upstream_ip_hash_peer_data_t`結構體成員 hash 值為 89;tries 重試連接次數為 0;`get_rr_peer` 為加權輪詢的決策函數 `ngx_http_upstream_get_round_robin_peer`;
`ngx_http_upstream_get_ip_hash_peer` 函數執行流程:
- 若重試連接的次數 tries 大于 20,或 只有一臺后端服務器,則直接調用加權輪詢策略 get_rr_peer 選擇當前后端服務器處理請求;
- 計算 IP 地址的 hash 值,下面根據哈希值進行選擇后端服務器;
- 若 `ngx_http_upstream_rr_peers_t` 結構體中 weighted 標志位為 1,則被選中的后端服務器在后端服務器列表中的位置為 hash 值與后端服務器數量的余數 p;
- 若 `ngx_http_upstream_rr_peers_t` 結構體中 weighted 標志位為 0,首先計算 hash 值與后端服務器總權重的余數 w; 將 w 值減去后端服務器的權重,直到有一個后端服務器使 w 值小于 0,則選中該后端服務器來處理請求,并記錄在后端服務器列表中的位置 p;
- 計算被選中后端服務器在位圖中的位置 n;
- 若當前被選中的后端服務器已經在位圖記錄過,則跳至 goto next 執行;
- 檢查當前被選中后端服務器的 down 標志位:
- 若該標志位為1,則跳至 goto next_try 執行;
- 若 down 標志位為 0,接著檢查當前被選中后端服務器失敗連接次數是否到達 max_fails,若已經達到 max_fails 次,并且睡眠時間還沒到 fail_timeout,則跳至 goto next_try 執行;
- 若不滿足以上條件,表示選擇成功,記錄當前后端服務器的地址信息,把當前后端服務器記錄在位圖相應的位置,更新哈希值,最后返回該后端服務器;
- *goto next*:tries 重試連接的次數加 1,并判斷 tries 是否大于閾值 20,若大于,則采用加權輪詢策略;
- *goto next_try *:把當前后端服務器記錄在位圖中,減少當前后端服務器重試連接的次數 tries;
~~~
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;
#if (NGX_HAVE_INET6)
struct sockaddr_in6 *sin6;
#endif
ngx_http_upstream_ip_hash_peer_data_t *iphp;
/* 分配 ngx_http_upstream_ip_hash_peer_data_t 結構體內存空間 */
iphp = ngx_palloc(r->pool, sizeof(ngx_http_upstream_ip_hash_peer_data_t));
if (iphp == NULL) {
return NGX_ERROR;
}
r->upstream->peer.data = &iphp->rrp;
/* 調用加權輪詢策略的初始化函數 ngx_http_upstream_init_round_robin_peer */
if (ngx_http_upstream_init_round_robin_peer(r, us) != NGX_OK) {
return NGX_ERROR;
}
/* 設置 IP hash 的決策函數 */
r->upstream->peer.get = ngx_http_upstream_get_ip_hash_peer;
switch (r->connection->sockaddr->sa_family) {
/* 保存客戶端 IP 地址 */
/* IPv4 地址 */
case AF_INET:
sin = (struct sockaddr_in *) r->connection->sockaddr;
iphp->addr = (u_char *) &sin->sin_addr.s_addr;
iphp->addrlen = 3;
break;
/* IPv6 地址 */
#if (NGX_HAVE_INET6)
case AF_INET6:
sin6 = (struct sockaddr_in6 *) r->connection->sockaddr;
iphp->addr = (u_char *) &sin6->sin6_addr.s6_addr;
iphp->addrlen = 16;
break;
#endif
/* 非法地址 */
default:
iphp->addr = ngx_http_upstream_ip_hash_pseudo_addr;
iphp->addrlen = 3;
}
/* 初始化 ngx_http_upstream_ip_hash_peer_data_t結構體成員 */
iphp->hash = 89;
iphp->tries = 0;
/* 這個是設置為加權輪詢策略的決策函數 */
iphp->get_rr_peer = ngx_http_upstream_get_round_robin_peer;
return NGX_OK;
}
~~~
~~~
/* 選擇后端服務器處理請求 */
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;
ngx_log_debug1(NGX_LOG_DEBUG_HTTP, pc->log, 0,
"get ip hash peer, try: %ui", pc->tries);
/* TODO: cached */
/* 若重試連接的次數 tries 大于 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;
for ( ;; ) {
/* 計算 IP 地址的 hash 值 */
for (i = 0; i < (ngx_uint_t) iphp->addrlen; i++) {
hash = (hash * 113 + iphp->addr[i]) % 6271;/* hash 函數 */
}
/* 以下是根據 hash 值選擇合適的后端服務器來處理請求 */
/* 若 ngx_http_upstream_rr_peers_t 結構體中 weighted 標志位為 1,
* 表示所有后端服務器的總權重 與 后端服務器的數量 相等,
* 則被選中的后端服務器在后端服務器列表中的位置為 hash 值與后端服務器數量的余數 p;
*/
if (!iphp->rrp.peers->weighted) {
p = hash % iphp->rrp.peers->number;
} else {
/* 若 ngx_http_upstream_rr_peers_t 結構體中 weighted 標志位為 0,
* 首先計算 hash 值與后端服務器總權重的余數 w;
* 將 w 值減去后端服務器的權重,直到有一個后端服務器使 w 值小于 0,
* 則選中該后端服務器來處理請求,并記錄在后端服務器列表中的位置 p;
*/
w = hash % iphp->rrp.peers->total_weight;
for (i = 0; i < iphp->rrp.peers->number; i++) {
w -= iphp->rrp.peers->peer[i].weight;
if (w < 0) {
break;
}
}
p = i;
}
/* 計算被選中后端服務器在位圖中的位置 n */
n = p / (8 * sizeof(uintptr_t));
m = (uintptr_t) 1 << p % (8 * sizeof(uintptr_t));
/* 若當前被選中的后端服務器已經在位圖記錄過,則跳至 goto next 執行 */
if (iphp->rrp.tried[n] & m) {
goto next;
}
ngx_log_debug2(NGX_LOG_DEBUG_HTTP, pc->log, 0,
"get ip hash peer, hash: %ui %04XA", p, m);
/* 獲取當前被選中的后端服務器 */
peer = &iphp->rrp.peers->peer[p];
/* ngx_lock_mutex(iphp->rrp.peers->mutex); */
/* 檢查當前被選中后端服務器的 down 標志位,若該標志位為1,則跳至 goto next_try 執行 */
if (peer->down) {
goto next_try;
}
/* 若 down 標志位為 0,接著檢查當前被選中后端服務器失敗連接次數是否到達 max_fails,
* 若已經達到 max_fails 次,并且睡眠時間還沒到 fail_timeout,則跳至 goto next_try 執行;
*/
if (peer->max_fails
&& peer->fails >= peer->max_fails
&& now - peer->checked <= peer->fail_timeout)
{
goto next_try;
}
/* 若不滿足以上條件,則表示選擇后方服務器成功 */
break;
next_try:
/* 把當前后端服務器記錄在位圖中 */
iphp->rrp.tried[n] |= m;
/* ngx_unlock_mutex(iphp->rrp.peers->mutex); */
/* 減少當前后端服務器重試連接的次數 */
pc->tries--;
next:
/* tries 重試連接的次數加 1,并判斷 tries 是否大于閾值 20,若大于,則采用加權輪詢策略 */
if (++iphp->tries >= 20) {
return iphp->get_rr_peer(pc, &iphp->rrp);
}
}
/* 到此已經成功選擇了后端服務器來處理請求 */
/* 記錄當前后端服務器在后端服務器列表中的位置,該位置方便釋放后端服務器調用 */
iphp->rrp.current = p;
/* 記錄當前后端服務器的地址信息 */
pc->sockaddr = peer->sockaddr;
pc->socklen = peer->socklen;
pc->name = &peer->name;
if (now - peer->checked > peer->fail_timeout) {
peer->checked = now;
}
/* ngx_unlock_mutex(iphp->rrp.peers->mutex); */
/* 把當前后端服務器記錄在位圖相應的位置 */
iphp->rrp.tried[n] |= m;
/* 記錄 hash 值 */
iphp->hash = hash;
return NGX_OK;
}
~~~
### 總結
**加權輪詢策略**:不依賴于客戶端的任何信息,完全依靠后端服務器的情況來進行選擇。但是同一個客戶端的多次請求可能會被分配到不同的后端服務器進行處理,無法滿足做會話保持的應用的需求。
**IP哈希策略**:把同一個 IP 地址的客戶端請求分配到同一臺服務器處理,避免了加權輪詢無法適用會話保持的需求。但是來自同一的 IP 地址的請求比較多時,會導致某臺后端服務器的壓力可能非常大,而其他后端服務器卻空閑的不均衡情況。
- 前言
- Nginx 配置文件
- Nginx 內存池管理
- Nginx 基本數據結構
- Nginx 數組結構 ngx_array_t
- Nginx 鏈表結構 ngx_list_t
- Nginx 隊列雙向鏈表結構 ngx_queue_t
- Nginx 哈希表結構 ngx_hash_t
- Nginx 紅黑樹結構 ngx_rbtree_t
- Nginx 模塊開發
- Nginx 啟動初始化過程
- Nginx 配置解析
- Nginx 中的 upstream 與 subrequest 機制
- Nginx 源碼結構分析
- Nginx 事件模塊
- Nginx 的 epoll 事件驅動模塊
- Nginx 定時器事件
- Nginx 事件驅動模塊連接處理
- Nginx 中 HTTP 模塊初始化
- Nginx 中處理 HTTP 請求
- Nginx 中 upstream 機制的實現
- Nginx 中 upstream 機制的負載均衡