nginx負載均衡之加權輪詢
當nginx作為代理伺服器時,需要將客戶端的請求轉發給後端伺服器進行處理,如果後端伺服器有多臺,那如何選擇合適的後端伺服器來處理當前請求,也就是本篇文章要介紹的內容。nginx儘可能的把請求分攤到各個後端伺服器進行處理,以保證服務的可用性和可靠行,提供給客戶端更好的使用者體驗。負載均衡的直接目的只有一個,儘量發揮多個後端伺服器的整體效能,實現1+1大於2的效果。
nginx提供了兩種策略,用於選擇一個合適的後端伺服器處理客戶端請求。一種是加權輪詢策略,顧名思義就是根據每臺伺服器的權重進行選擇,這將導致一個問題,同一個客戶端的不同請求有可能被不同的後端伺服器進行處理,不能維護會話的保持。 另一種是ip雜湊策略,也就是根據客戶端的ip地址進行雜湊運算,從而被分配到一個固定的後端伺服器。ip雜湊策略雖然能保證同一個客戶端的不同請求都被同一個後端伺服器進行處理,能夠做到會話的保持,但如果客戶端是經過nat地址對映後,將導致某臺後端伺服器的壓力劇增。如果ip雜湊失敗次數超過20次,也會退化為加權輪詢策略,使用加權輪詢策略選擇一臺後端伺服器。
需要注意的是客戶端與nginx伺服器之間是一個長連線,一個tcp連線可以處理多個http請求,也就是當http請求關閉後,這個tcp連線並沒有關閉。 而nginx與後端伺服器是一個短連線, 當nginx與後端伺服器的某個請求互動完成了,nginx與後端伺服器對應的tcp連線也就被關閉了,是一個短連線。
本篇文章只分析加權輪詢策略,至於ip雜湊策略留給讀者去分析。ip雜湊是基於加權輪詢實現的,如果加權輪詢理解了,那ip雜湊自然而然也就理解了。
一、負載均衡配置的解析
假設nginx.conf配置檔案中指定了下面的這個負載均衡配置
經過配置解析後,得到下面這種資料結構。配置解析後的資料儲存到了負載均衡模組的配置ngx_http_upstream_srv_conf_s中的servers陣列中。陣列中的每一個元素都是上面配置中每一個server解析後的內容。需要注意的是一個域名有可能解析後有多個ip地址。upstream backend_server { server www.sangfor.com weight=5; //這個域名解析後得到172.16.7.151; 172.16.7.152; 172.16.7.153三個ip地址 server www.sundray.com weight=4 max_fails=3 fail_timeout=30; //這個域名解析後得到172.16.7.154;172.16.7.155 server 172.16.7.156 backup; //備機 server 172.16.7.157 down; //該伺服器已經宕機,不作為後端伺服器 }
來看下程式碼的實現。
ip_hash配置項的回撥函式為ngx_http_upstream_ip_hash,因此在解析完配置後,如果指定了ip_hash配置項則會設定init_upstream回撥為:ngx_http_upstream_init_ip_hash ,表示使用ip雜湊策略,否則使用預設的加權輪詢策略。//開始解析http塊 static char * ngx_http_block(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) { //開始解析http塊 rv = ngx_conf_parse(cf, NULL); } //ip_hash配置解析,設定雜湊策略的回撥 static char * ngx_http_upstream_ip_hash(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) { uscf->peer.init_upstream = ngx_http_upstream_init_ip_hash; return NGX_CONF_OK; }
二、加權輪詢資料結構的維護
在解析完配置下後,又會呼叫各個模組的init_main_conf方法,負載均衡的init_main_conf回撥為: ngx_http_upstream_init_main_conf
//開始解析http塊
static char * ngx_http_block(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
//開始解析http塊
rv = ngx_conf_parse(cf, NULL);
for (m = 0; ngx_modules[m]; m++)
{
if (ngx_modules[m]->type != NGX_HTTP_MODULE)
{
continue;
}
//呼叫各個模組的init_main_conf回撥,負載均衡的回撥為: ngx_http_upstream_init_main_conf
if (module->init_main_conf)
{
rv = module->init_main_conf(cf, ctx->main_conf[mi]);
}
}
}
ngx_http_upstream_init_main_conf函式建立負載均衡的內部結構,如果是ip雜湊策略則呼叫ngx_http_upstream_init_ip_hash,否則呼叫預設的加權輪詢策略的回撥:ngx_http_upstream_init_round_robin, 先來看下加權輪詢策略的實現。//負載均衡的main塊初始化操作
//1、負載均衡策略的初始化,加權輪詢策略或者ip雜湊策略初始化操作
//2、建立負載均衡模組的頭部雜湊表
static char * ngx_http_upstream_init_main_conf(ngx_conf_t *cf, void *conf)
{
//賦值均衡策略初始化
for (i = 0; i < umcf->upstreams.nelts; i++)
{
//如果init_upstream為空, 則預設使用加權輪詢負載均衡策略;如果是ip雜湊策略則為;ngx_http_upstream_init_ip_hash
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函式前,先來看下這個函式維護的資料結構:
加權輪詢策略下,使用了兩個連結串列節點。一個連結串列節點存放所有的主後端伺服器列表, 另一個連結串列節點存放所有的備後端伺服器列表。ngx_http_upstream_init_round_robin函式內部就是將配置解析後的所有後端伺服器,按照主備分離思想。把所有的主伺服器放到一塊,把所有的備伺服器放到一塊。這樣在選擇一個後端伺服器時,優先選擇主後端伺服器, 當主後端伺服器都選擇失敗後,才會從備後端伺服器中選擇。
//加權策略初始化, 建立後端伺服器連結串列(最多兩個節點,一個主伺服器節點,存放所有主伺服器; //一個備伺服器節點,存放所有的備伺服器) ngx_int_t ngx_http_upstream_init_round_robin(ngx_conf_t *cf, ngx_http_upstream_srv_conf_t *us) { //在選擇後端伺服器前,該回調用於建立查詢介面的引數 us->peer.init = ngx_http_upstream_init_round_robin_peer; //配置了負載均衡模組,也就是指定了upstream xxx{}等 if (us->servers) { server = us->servers->elts; //統計主後端伺服器的個數 for (i = 0; i < us->servers->nelts; i++) { if (server[i].backup) { continue; } n += server[i].naddrs; } //開闢主後端伺服器的地址空間 peers = ngx_pcalloc(cf->pool, sizeof(ngx_http_upstream_rr_peers_t) + sizeof(ngx_http_upstream_rr_peer_t) * (n - 1)); peers->single = (n == 1); //儲存主後端伺服器的資訊 for (i = 0; i < us->servers->nelts; i++) { for (j = 0; j < server[i].naddrs; j++) { if (server[i].backup) { continue; } peers->peer[n].sockaddr = server[i].addrs[j].sockaddr; } } //主伺服器列表節點當做連結串列的第一個節點 us->peer.data = peers; //主後端伺服器安裝許可權從大道小排序 ngx_sort(&peers->peer[0], (size_t) n, sizeof(ngx_http_upstream_rr_peer_t),ngx_http_upstream_cmp_servers); //統計備後端服務區個數 for (i = 0; i < us->servers->nelts; i++) { if (!server[i].backup) { continue; } n += server[i].naddrs; } //開闢備後端伺服器的地址空間 backup = ngx_pcalloc(cf->pool, sizeof(ngx_http_upstream_rr_peers_t) + sizeof(ngx_http_upstream_rr_peer_t) * (n - 1)); //儲存備後端伺服器的資訊 for (i = 0; i < us->servers->nelts; i++) { for (j = 0; j < server[i].naddrs; j++) { if (!server[i].backup) { continue; } backup->peer[n].sockaddr = server[i].addrs[j].sockaddr; } } //被伺服器列表放到主伺服器列表後面 peers->next = backup; //權重權重從大到小排序備伺服器列表 ngx_sort(&backup->peer[0], (size_t) n, sizeof(ngx_http_upstream_rr_peer_t), ngx_http_upstream_cmp_servers); } }三、與後端伺服器建立tcp連線前的準備工作
在nginx與後端伺服器建立tcp連線前,需要設定獲取後端伺服器的回撥,以及建立回撥引數。
//負載均衡模組初始化,與上游伺服器建立一個tcp連線。
//同時將客戶端發來的請求頭部,包體轉為fastcgi格式的內容
static void ngx_http_upstream_init_request(ngx_http_request_t *r)
{
//加權輪詢策略為ngx_http_upstream_init_round_robin_peer,
//設定獲取後端伺服器的回撥
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);
}
對於每一個來自客戶端的請求,nginx都需要與後端伺服器建立一個tcp連線,如果是加權輪詢策略,則使用ngx_http_upstream_init_round_robin_peer函式獲取後端伺服器,以及建立獲取後端伺服器需要的引數。
//nginx與後端伺服器建立tcp連線前的初始化,設定獲取後端伺服器的回撥,以及建立回撥引數
ngx_int_t ngx_http_upstream_init_round_robin_peer(ngx_http_request_t *r,
ngx_http_upstream_srv_conf_t *us)
{
rrp = r->upstream->peer.data;
//建立的這個結構,將作為ngx_http_upstream_get_round_robin_peer的引數
if (rrp == NULL)
{
rrp = ngx_palloc(r->pool, sizeof(ngx_http_upstream_rr_peer_data_t));
r->upstream->peer.data = rrp;
}
//仍然指向的是配置解析後,加權輪詢策略維護的資料結構
rrp->peers = us->peer.data;
//獲取後端伺服器的個數
n = rrp->peers->number;
//一個整數佔4個位元組, 一共32個位。每一位代表一個後端伺服器。
//如果後端伺服器的個數小於一個整數位大小,也就是小於32,則使用data空間
if (n <= 8 * sizeof(uintptr_t))
{
rrp->tried = &rrp->data;
rrp->data = 0;
}
else
{
//如果後端伺服器的個數大於一個整數的總位數(也就是32), 則計算需要多少個整數才能存放
//所有的後端伺服器。
n = (n + (8 * sizeof(uintptr_t) - 1)) / (8 * sizeof(uintptr_t));
rrp->tried = ngx_pcalloc(r->pool, n * sizeof(uintptr_t));
}
//設定獲取後端伺服器,釋放後端伺服器的回撥
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;
return NGX_OK;
}
函式內部設定獲取後端伺服器的回撥為:ngx_http_upstream_get_round_robin_peer,釋放後端伺服器的回撥為:ngx_http_upstream_free_round_robin_peer。同時也建立了每一個客戶端請求都獨立擁有的一個私有結構ngx_http_upstream_rr_peer_data_t。看下這個結構的定義:peers仍然指向解析配置時建立的後端伺服器連結串列,每一個客戶端建立的私有結構,peers成員都指向這個連結串列,這樣每一個客戶端根據這個連結串列中的後端伺服器權重資訊動態的選擇一個後端伺服器,同時每一個客戶端都會修改這個連結串列中某些伺服器的權重,
最終使得後端伺服器的負載按照權重的比例,比較均衡的分部,例如5:3:2。另外需要注意的是tried指向的空間每一個位都代表一個後端伺服器,如果該後端伺服器被選中了,則相應的位會被設定為1
typedef struct
{
ngx_http_upstream_rr_peers_t *peers; //後端伺服器連結串列頭,指向配置ngx_http_upstream_srv_conf_s中的peer
ngx_uint_t current; //當前使用的是哪一個後端伺服器,假設有0-59供60個後端伺服器,如果current=4,則表示使用第5個伺服器
uintptr_t *tried; //陣列中的每一位代表已經選中的後端伺服器,如果後端伺服器的總數小於一個4位元組的整數空間,也就是32位,則tried指向下面這個data空間
//如果大於一個4位元組的整數空間,也就是32位,則開闢一個空間,tried指向這片空間
uintptr_t data;
} ngx_http_upstream_rr_peer_data_t;
四、選則後端伺服器
在nginx與後端伺服器建立tcp連線時,會根據加權輪詢策略選擇一個後端伺服器,進而獲取到後端伺服器的ip,埠資訊,從而與它建立連線。
//獲取一個後端伺服器的連線地址,並與後端伺服器進行連線
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);
}
來看下ngx_http_upstream_get_round_robin_peer函式的實現,在只有一臺伺服器的時候,沒得選擇,這能選擇這唯一的一臺。
(1)在有多臺伺服器的情況下,如果是第一次選擇,則按照權重的大小,每次從後端伺服器選擇一臺伺服器。如果已經宕機了的伺服器,則不會被使用;如果已經被選中了的,也不會被使用;如果在一定時間內失敗次數超過限制大小的也不會被選中。
//加權輪詢策略時從後端伺服器選擇一個伺服器地址,儲存到pc的sockaddr成員中
ngx_int_t ngx_http_upstream_get_round_robin_peer(ngx_peer_connection_t *pc,void *data)
{
//第一次選擇情況
if (pc->tries == rrp->peers->number)
{
for ( ;; )
{
//按照每臺後端伺服器的權重比例進而選擇一臺後端伺服器
rrp->current = ngx_http_upstream_get_peer(rrp->peers);
//n表示第幾個整數
n = rrp->current / (8 * sizeof(uintptr_t));
//m表示這個整數32位中的哪一位
m = (uintptr_t) 1 << rrp->current % (8 * sizeof(uintptr_t));
if (!(rrp->tried[n] & m))
{
//進入這個條件表示未選中情況
peer = &rrp->peers->peer[rrp->current];
if (!peer->down)
{
//選擇到了則退出
if (peer->max_fails == 0 || peer->fails < peer->max_fails)
{
break;
}
//條件大於的情況下,表示還沒有達到超時時間
if (now - peer->accessed > peer->fail_timeout)
{
peer->fails = 0;
break;
}
peer->current_weight = 0; //值為0表示不參與選擇
}
else
{
rrp->tried[n] |= m;
}
pc->tries--;
}
}
peer->current_weight--;
}
//儲存選擇的伺服器ip資訊
pc->sockaddr = peer->sockaddr;
pc->socklen = peer->socklen;
}
//根據權重的比例選擇一個後端伺服器
static ngx_uint_t ngx_http_upstream_get_peer(ngx_http_upstream_rr_peers_t *peers)
{
//核心演算法,每一個重點都會修改這個權重,從而實現負載均衡
if (peer[n].current_weight * 1000 / peer[i].current_weight
> peer[n].weight * 1000 / peer[i].weight)
{
return n;
}
}
(2)如果不是第一次選擇,也就是說,之前選中的後端伺服器不可用,有可能該後端伺服器關閉了。這樣就需要重新選擇一臺伺服器,重新選擇時不再按照權重比例來選擇,而是從當前這臺伺服器往後開始查詢,直到找到一臺可用的伺服器。//從後端伺服器選擇一個伺服器地址,儲存到pc的sockaddr成員中
ngx_int_t ngx_http_upstream_get_round_robin_peer(ngx_peer_connection_t *pc,void *data)
{
//使用迴圈方式,從current往後的伺服器開始選擇,而不在是按照權重選擇。具體為什麼這麼做,不得而知
for ( ;; )
{
//n表示第幾個整數
n = rrp->current / (8 * sizeof(uintptr_t));
//m表示這個整數32位中的哪一位
m = (uintptr_t) 1 << rrp->current % (8 * sizeof(uintptr_t));
if (!(rrp->tried[n] & m))
{
peer = &rrp->peers->peer[rrp->current];
if (!peer->down) //down=1表示宕機
{
}
else
{
rrp->tried[n] |= m;
}
pc->tries--;
}
rrp->current++;
}
//儲存選擇的伺服器ip資訊
pc->sockaddr = peer->sockaddr;
pc->socklen = peer->socklen;
}
(3)那如果主伺服器列表都選擇失敗,該怎麼辦呢? nginx從備伺服器列表中選擇一臺伺服器。備伺服器的選擇和主伺服器的選擇是一模一樣的,因此遞迴呼叫這個函式。那如果主備伺服器都選擇失敗怎麼辦? 此時函式返回NGX_BUSY, 然後給客戶端返回一個502錯誤。//加權輪詢策略時從後端伺服器選擇一個伺服器地址,儲存到pc的sockaddr成員中
ngx_int_t ngx_http_upstream_get_round_robin_peer(ngx_peer_connection_t *pc,void *data)
{
//在主後端伺服器全部選擇失敗的情況下, 開始選擇備用的後端伺服器
peers = rrp->peers;
if (peers->next)
{
//切換到備伺服器連結串列節點
rrp->peers = peers->next;
pc->tries = rrp->peers->number;
n = rrp->peers->number / (8 * sizeof(uintptr_t)) + 1;
for (i = 0; i < n; i++)
{
rrp->tried[i] = 0;
}
//被伺服器的選擇和主伺服器的選擇是一樣的,因此重新呼叫該函式(遞迴)
rc = ngx_http_upstream_get_round_robin_peer(pc, rrp);
}
//執行到這裡,主備伺服器都選擇失敗
for (i = 0; i < peers->number; i++)
{
peers->peer[i].fails = 0;
}
return NGX_BUSY;
}
五、再次選擇後端伺服器
當選中一個後端伺服器後,nginx會與這個後端伺服器建立tcp連線。如果建立tcp連線的時候,連線超時;或者傳送請求資料給後端伺服器失敗; 或者接收後端伺服器的響應超時等,這些操作都會導致重新獲取一個後端伺服器。呼叫ngx_http_upstream_next這個函式可以重新獲取一個後端伺服器。來看下這個函式的實現。
//嘗試選擇一個新的後端伺服器並與它建立tcp連線。如果嘗試完了所有後端伺服器都沒有找到一個可用的
//後端伺服器,則會結束請求
static void ngx_http_upstream_next(ngx_http_request_t *r, ngx_http_upstream_t *u, ngx_uint_t ft_type)
{
//該後端伺服器不可用,則釋放這個已經選擇的後端伺服器,以便下面重新選擇一個後端伺服器
if (ft_type != NGX_HTTP_UPSTREAM_FT_NOLIVE)
{
u->peer.free(&u->peer, u->peer.data, state);
}
if (status)
{
//如果嘗試完所有的後端伺服器都沒有一個可用的伺服器,則給客戶端返回錯誤。當然,
//如果fastcgi_next_upstream指定了在後端伺服器返回對應的錯誤碼時,才尋找下一個後端伺服器,否則立即給可以的返回錯誤
//,參考ngx_conf_set_bitmask_slot實現
if (u->peer.tries == 0 || !(u->conf->next_upstream & ft_type))
{
ngx_http_upstream_finalize_request(r, u, status);
return;
}
}
//與後端伺服器的連線存在,則先關閉, 下面重新選擇一個後端伺服器並與它建立tcp連線
if (u->peer.connection)
{
ngx_close_connection(u->peer.connection);
}
//重新選擇一個後端伺服器並與它建立tcp連線
ngx_http_upstream_connect(r, u);
}