負載均衡模塊用于從upstream
指令定義的后端主機列表中選取一臺主機。Nginx 先使用負載均衡模塊找到一臺主機,再使用 upstream 模塊實現(xiàn)與這臺主機的交互。為了方便介紹負載均衡模塊,做到言之有物,以下選取 Nginx 內置的 ip hash 模塊作為實際例子進行分析。
要了解負載均衡模塊的開發(fā)方法,首先需要了解負載均衡模塊的使用方法。因為負載均衡模塊與之前書中提到的模塊差別比較大,所以我們從配置入手比較容易理解。
在配置文件中,我們如果需要使用 ip hash 的負載均衡算法。我們需要寫一個類似下面的配置:
upstream test {
ip_hash;
server 192.168.0.1;
server 192.168.0.2;
}
從配置我們可以看出負載均衡模塊的使用場景:
ip_hash
只能在 upstream {}中使用。這條指令用于通知 Nginx 使用 ip hash 負載均衡算法。如果沒加這條指令,Nginx 會使用默認的 round robin 負載均衡模塊。請各位讀者對比 handler 模塊的配置,是不是有共同點?server
指令前,可能出現(xiàn)在server
指令后,也可能出現(xiàn)在兩條server
指令之間。各位讀者可能會有疑問,有什么差別么?那么請各位讀者嘗試下面這個配置: upstream test {
server 192.168.0.1 weight=5;
ip_hash;
server 192.168.0.2 weight=7;
}
神奇的事情出現(xiàn)了:
nginx: [emerg] invalid parameter "weight=7" in nginx.conf:103
configuration file nginx.conf test failed
可見 ip_hash 指令的確能影響到配置的解析。
配置決定指令系統(tǒng),現(xiàn)在就來看 ip_hash 的指令定義:
static ngx_command_t ngx_http_upstream_ip_hash_commands[] = {
{ ngx_string("ip_hash"),
NGX_HTTP_UPS_CONF|NGX_CONF_NOARGS,
ngx_http_upstream_ip_hash,
0,
0,
NULL },
ngx_null_command
};
沒有特別的東西,除了指令屬性是 NGX_HTTP_UPS_CONF。這個屬性表示該指令的適用范圍是 upstream{}。
以從前面的章節(jié)得到的經(jīng)驗,大家應該知道這里就是模塊的切入點了。負載均衡模塊的鉤子代碼都是有規(guī)律的,這里通過 ip_hash 模塊來分析這個規(guī)律。
static char *
ngx_http_upstream_ip_hash(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
ngx_http_upstream_srv_conf_t *uscf;
uscf = ngx_http_conf_get_module_srv_conf(cf, ngx_http_upstream_module);
uscf->peer.init_upstream = ngx_http_upstream_init_ip_hash;
uscf->flags = NGX_HTTP_UPSTREAM_CREATE
|NGX_HTTP_UPSTREAM_MAX_FAILS
|NGX_HTTP_UPSTREAM_FAIL_TIMEOUT
|NGX_HTTP_UPSTREAM_DOWN;
return NGX_CONF_OK;
}
這段代碼中有兩點值得我們注意。一個是 uscf->flags 的設置,另一個是設置 init_upstream 回調。
NGX_HTTP_UPSTREAM_CREATE:創(chuàng)建標志,如果含有創(chuàng)建標志的話,Nginx 會檢查重復創(chuàng)建,以及必要參數(shù)是否填寫;
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_WEIGHT:可以在 server 中使用 weight 屬性;
聰明的讀者如果聯(lián)想到剛剛遇到的那個神奇的配置錯誤,可以得出一個結論:在負載均衡模塊的指令處理函數(shù)中可以設置并修改 upstream{} 中server
指令支持的屬性。這是一個很重要的性質,因為不同的負載均衡模塊對各種屬性的支持情況都是不一樣的,那么就需要在解析配置文件的時候檢測出是否使用了不支持的負載均衡屬性并給出錯誤提示,這對于提升系統(tǒng)維護性是很有意義的。但是,這種機制也存在缺陷,正如前面的例子所示,沒有機制能夠追加檢查在更新支持屬性之前已經(jīng)配置了不支持屬性的server
指令。
Nginx 初始化 upstream 時,會在 ngx_http_upstream_init_main_conf 函數(shù)中調用設置的回調函數(shù)初始化負載均衡模塊。這里不太好理解的是 uscf 的具體位置。通過下面的示意圖,說明 upstream 負載均衡模塊的配置的內存布局。
從圖上可以看出,MAIN_CONF 中 ngx_upstream_module 模塊的配置項中有一個指針數(shù)組 upstreams,數(shù)組中的每個元素對應就是配置文件中每一個 upstream{}的信息。更具體的將會在后面的原理篇討論。
init_upstream 回調函數(shù)執(zhí)行時需要初始化負載均衡模塊的配置,還要設置一個新鉤子,這個鉤子函數(shù)會在 Nginx 處理每個請求時作為初始化函數(shù)調用,關于這個新鉤子函數(shù)的功能,后面會有詳細的描述。這里,我們先分析 IP hash 模塊初始化配置的代碼:
ngx_http_upstream_init_round_robin(cf, us);
us->peer.init = ngx_http_upstream_init_ip_hash_peer;
這段代碼非常簡單:IP hash 模塊首先調用另一個負載均衡模塊 Round Robin 的初始化函數(shù),然后再設置自己的處理請求階段初始化鉤子。實際上幾個負載均衡模塊可以組成一條鏈表,每次都是從鏈首的模塊開始進行處理。如果模塊決定不處理,可以將處理權交給鏈表中的下一個模塊。這里,IP hash 模塊指定 Round Robin 模塊作為自己的后繼負載均衡模塊,所以在自己的初始化配置函數(shù)中也對 Round Robin 模塊進行初始化。
Nginx 收到一個請求以后,如果發(fā)現(xiàn)需要訪問 upstream,就會執(zhí)行對應的 peer.init 函數(shù)。這是在初始化配置時設置的回調函數(shù)。這個函數(shù)最重要的作用是構造一張表,當前請求可以使用的 upstream 服務器被依次添加到這張表中。之所以需要這張表,最重要的原因是如果 upstream 服務器出現(xiàn)異常,不能提供服務時,可以從這張表中取得其他服務器進行重試操作。此外,這張表也可以用于負載均衡的計算。之所以構造這張表的行為放在這里而不是在前面初始化配置的階段,是因為upstream需要為每一個請求提供獨立隔離的環(huán)境。
為了討論 peer.init 的核心,我們還是看 IP hash 模塊的實現(xiàn):
r->upstream->peer.data = &iphp->rrp;
ngx_http_upstream_init_round_robin_peer(r, us);
r->upstream->peer.get = ngx_http_upstream_get_ip_hash_peer;
第一行是設置數(shù)據(jù)指針,這個指針就是指向前面提到的那張表;
第二行是調用 Round Robin 模塊的回調函數(shù)對該模塊進行請求初始化。面前已經(jīng)提到,一個負載均衡模塊可以調用其他負載均衡模塊以提供功能的補充。
第三行是設置一個新的回調函數(shù)get。該函數(shù)負責從表中取出某個服務器。除了 get 回調函數(shù),還有另一個r->upstream->peer.free
的回調函數(shù)。該函數(shù)在 upstream 請求完成后調用,負責做一些善后工作。比如我們需要維護一個 upstream 服務器訪問計數(shù)器,那么可以在 get 函數(shù)中對其加 1,在 free 中對其減 1。如果是 SSL 的話,Nginx 還提供兩個回調函數(shù) peer.set_session 和 peer.save_session。一般來說,有兩個切入點實現(xiàn)負載均衡算法,其一是在這里,其二是在 get 回調函數(shù)中。
這兩個函數(shù)是負載均衡模塊最底層的函數(shù),負責實際獲取一個連接和回收一個連接的預備操作。之所以說是預備操作,是因為在這兩個函數(shù)中,并不實際進行建立連接或者釋放連接的動作,而只是執(zhí)行獲取連接的地址或維護連接狀態(tài)的操作。需要理解的清楚一點,在 peer.get 函數(shù)中獲取連接的地址信息,并不代表這時連接一定沒有被建立,相反的,通過 get 函數(shù)的返回值,Nginx 可以了解是否存在可用連接,連接是否已經(jīng)建立。這些返回值總結如下:
返回值 | 說明 | Nginx 后續(xù)動作 |
---|---|---|
NGX_DONE | 得到了連接地址信息,并且連接已經(jīng)建立。 | 直接使用連接,發(fā)送數(shù)據(jù)。 |
NGX_OK | 得到了連接地址信息,但連接并未建立。 | 建立連接,如連接不能立即建立,設置事件, |
暫停執(zhí)行本請求,執(zhí)行別的請求。 | ||
NGX_BUSY | 所有連接均不可用。 | 返回502錯誤至客戶端。 |
各位讀者看到上面這張表,可能會有幾個問題浮現(xiàn)出來:
Q: 什么時候連接是已經(jīng)建立的?
A: 使用后端 keepalive 連接的時候,連接在使用完以后并不關閉,而是存放在一個隊列中,新的請求只需要從隊列中取出連接,這些連接都是已經(jīng)準備好的。
Q: 什么叫所有連接均不可用?
A: 初始化請求的過程中,建立了一張表,get 函數(shù)負責每次從這張表中不重復的取出一個連接,當無法從表中取得一個新的連接時,即所有連接均不可用。
Q: 對于一個請求,peer.get 函數(shù)可能被調用多次么?
A: 正式如此。當某次 peer.get 函數(shù)得到的連接地址連接不上,或者請求對應的服務器得到異常響應,Nginx 會執(zhí)行 ngx_http_upstream_next,然后可能再次調用 peer.get 函數(shù)嘗試別的連接。upstream 整體流程如下:
這一節(jié)介紹了負載均衡模塊的基本組成。負載均衡模塊的配置區(qū)集中在 upstream{}塊中。負載均衡模塊的回調函數(shù)體系是以 init_upstream 為起點,經(jīng)歷 init_peer,最終到達 peer.get 和 peer.free。其中 init_peer 負責建立每個請求使用的 server 列表,peer.get 負責從 server 列表中選擇某個 server(一般是不重復選擇),而 peer.free 負責 server 釋放前的資源釋放工作。最后,這一節(jié)通過一張圖將 upstream 模塊和負載均衡模塊在請求處理過程中的相互關系展現(xiàn)出來。
更多建議: