大型網站,負載均衡時永恆的話題。
硬件負載均衡如:F5、BIG-IP、Citrix NetScaler、Radware、A10等,性能好價格昂貴
軟件負載均衡如:LVS、Nginx、HAProxy等
現在最火的當屬nginx
nginx特點:穩定性高、功能強大、資源消耗低,而且能做web服務器。
負載均衡問題的產生
在nginx中,建立連接的時候,會涉及負載均衡問題。在多個子進程爭搶處理一個新連接事件時,一定只有一個worker子進程最終會成功建立連接,
隨後它會一直處理這個連接直到連接關閉。那麼,就可能出現這樣的情況:有的子進程建立並處理了大部分連接,而有的子進程只處理了少量連接。
這對多核cpu架構下的應用時很不利的。因爲子進程之間應該是平等的,每個子進程應該儘量獨佔一個cpu核心。子進程負載不均衡,必定會影響整個
服務性能。
如何解決負載均衡問題
只有打開了accept_mutex鎖,才能實現子進程間的負載均衡。同時post事件機制也是解決負載均衡問題的關鍵。
在ngx_event_accept方法建立新連接的過程中,初始化一個全局變量ngx_accept_disabled。它就是負載均衡機制實現的關鍵閾值。
其定義(/src/event/ngx_event.c)
ngx_int_t ngx_accept_disabled;
初始化(src/event/ngx_event_accept.c)是在函數ngx_event_accept中:
ngx_accept_disabled=ngx_cycle->connection_n/8 - ngx_cycle->free_connection_n;
這樣,在nginx啓動的時候其實是個負值: -7/8 * ngx_cycle->connection_n
依據這個值進行負載均衡的核心代碼是在函數ngx_process_events_and_timers中(src/event/ngx_event.c)
//負載均衡處理
if (ngx_accept_disabled > 0) {
ngx_accept_disabled--;
} else {
//調用ngx_trylock_accept_mutex方法,嘗試獲取accept鎖
if (ngx_trylock_accept_mutex(cycle) == NGX_ERROR) {
return;
}
if (ngx_accept_mutex_held) {
flags |= NGX_POST_EVENTS;
} else {
if (timer == NGX_TIMER_INFINITE
|| timer > ngx_accept_mutex_delay)
{
timer = ngx_accept_mutex_delay;
}
}
}
解釋一下:
當ngx_accept_disabled爲負數時,不會觸發負載均衡操作,正常獲取accept鎖,試圖處理新連接。
當ngx_accept_disabled爲正數時,會觸發負載均衡操作。nginx此時不再處理新連接事件,取而代之的僅僅是把變量ngx_accept_disabled減一,這表示既然經過一輪事件處理,那麼負載肯定有所減小,所以要響應的調整這個值。
即當前使用的連接超過總連接數的7/8的時候纔會被觸發,值越大,表示負載越重。每次調用ngx_process_events_and_timers的時候只會將ngx_accept_disabled減一,直到ngx_accept_disabled降到0,即使用的連接數降到總連接輸的7/8,這樣就減少了該work子進程處理新連接的機會,這樣其他較空閒的worker子進程就有機會去處理更多的新連接,以達到整個web服務器的均衡效果。
Nginx默認將accept_mutex配置項設置爲accept_mutex on。
@上面所說的是客戶端請求在多個nginx進程之間的負載均衡
@下面所說的是客戶端請求在多個後端服務器之間的負載均衡(反向代理)
負載均衡模塊簡介
負載均衡模塊Load-balance是輔助模塊,主要爲Upstream模塊服務,目標明確且單一:如何從多臺後端服務器中選擇出一臺合適的服務器來處理當前的請求。
Load-blance模塊中4個關鍵回調函數:
回調指針:uscf->peer.init_upstream
函數功能:解析配置文件過程中調用,根據upstream裏各個server配置項做初始準備工作,另外的核心工作是設置回調指針us->peer.init。配置文件解析完後不再被調用。
round_robin模塊:ngx_http_upstream_init_round_robin 設置us->peer.init=ngx_http_upstream_init_round_robin_peer;
IP_hash模塊: ngx_http_upstream_init_ip_hash 設置us->peer.init=ngx_http_upstream_init_ip_hash_peer;
回調指針:us->peer.init
函數功能:在每一次Nginx準備轉發客戶請求道後端服務器前都會調用該函數。該函數爲本次轉發選擇合適的後端服務器做初始準備工作,另外的核心工作就是設置回調指針 r->upstream->peer.get和r->upstream->peer.free等
round_robin模塊: ngx_http_upstream_init_round_robin_peer 設置 r->upstream->peer.get=ngx_http_upstream_get_round_robin_peer; r->upstream->peer.free=ngx_http_upstream_free_round_robin_peer
IP_hash模塊: ngx_http_upstream_init_ip_hash_peer 設置 r->upstream->peer.get=ngx_http_upstream_get_ip_hash_peer; r->upstream->peer.free爲空
回調指針: r->upstream->peer.get
函數功能:在每一次Nginx準備轉發客戶端請求到後端服務器前都會調用該函數,該函數實現具體的爲本次轉發選擇合適的後端服務器的算法邏輯,即完成選擇獲取合適後端服務器的功能。
round_robin模塊: ngx_http_upstream_get_round_robin_peer 加權選擇當前權值最高的後端服務器。
IP_hash模塊: ngx_http_upstream_get_ip_hash_peer 根據IP哈希值選擇後端服務器
回調指針: r->upstream->peer.free
函數功能:在每一次Nginx完成於後端服務器之間的交互後都會調用該函數
round_robin模塊:ngx_http_upstream_free_round_robin_peer更新相關數值,比如rrp->current
IP_hash模塊:空
@負載均衡配置指令
nginx負載均衡模塊ngx_http_upstream_module允許定義一組服務器,這組服務器可以被proxy_pass,fastcgi_pass和memcached_pass這些指令引用
proxy代理服務,
配置例子:
upstream backend {
server backend1.example.com weight=5;
server backend2.example.com:8080;
server unix:/tmp/backend3;
server backup1.example.com:8080 backup;
server backup2.example.com:8080 backup;
}
server {
location / {
proxy_pass http://backend;
}
}
語法:upstream name {...}
所屬指令:http
定義一組用於實現nginx負載均衡的服務器,它們可以偵聽在不同的端口,另外,可以混合使用偵聽TCP與UNIX-domain套接文件
例子:
upstream backend {
server backend1.example.com weight=5;
server 127.0.0.1:8080 max_fails=3 fail_timeout=30s;
server unix:/tmp/backend3;
}
默認情況下,請求被分散在使用加權輪詢的nginx負載 均衡服務器上。在上面的例子中,每七個請求按下面分配:五個請求發送給backend1.example.com,127.0.0.1:8080和unix:/tmp/backend3各自分配一個。如果在與服務器通信時發生了一個錯誤,這個請求就會被傳遞到下一個服務器,一次類推直到所有的服務器都嘗試過,如果不能從所有的這些nginx負載均衡服務器上獲得迴應,客戶端將會獲得最後一個鏈接的服務器的處理結果。
語法:server地址[參數]
所屬指令:upstream
設置一個nginx負載均衡服務器的地址和其他參數。一個地址可以被指定爲域名或IP地址,和一個可選的端口,或者被定義爲UNIX-domain套接字文件的路徑,使用“unix:”作爲前綴。如果端口沒指定,則使用80端口。一個被解析到多個IP地址的域名本質指定了多少服務器
可以定義下面的參數:
weight=number
設置服務器的權重,默認是1
max_fails=number
設置在fail_timeout參數設置的時間內(注意內)最大失敗次數,如果在這個時間內,所有針對該服務器的請求都失敗了,那麼認爲該服務器是停機了,停機時間是fail_timeout設置的時間。默認情況下,不成功連接數被設置爲1。被設置爲零則表示不進行鏈接數統計。那些連接被認爲是不成功的可以通過proxy_next_upstream、fastcgi_next_upstream和memcached_next_upstream指令配置。http_404狀態不會被認爲是不成功的嘗試。
fail_timeout=time
設置多長時間內失敗次數達到最大失敗次數會被認爲服務器停機了,服務器會被認爲停機的時間長度,默認情況下,超時時間被設置爲10s。
backup
標記該服務器爲備用服務器,當主服務器停止時,請求會被髮送到這裏。
down
標記服務器永久停機了;與指令ip_hash一起使用。
例子:
upstream backend {
server backend1.example.com weight=5;
server 127.0.0.1:8080 max_fails=3 fail_timeout=30s;
server unix:/tmp/backend3;
server backup1.example.com:8080 backup;
}
語法:ip_hash
所屬指令:upstream
指定nginx負載均衡器組使用基於客戶端ip的負載均衡算法,ipv4的前3個八進制和所有的ipv6地址唄用作一個hash key。這個方法確保了相同客戶端的請求一直髮送到相同的服務器上除非這個服務器被認爲是停機了,這種情況下,請求會被髮送到其他主機上,然後可能會一直髮送到這個主機上。
如果nginx負載均衡器組裏面的一個服務器要臨時移除,它應該用參數down標記,來防止之前的客戶端ip還往這個服務器上發請求。
例子:
upstream backend {
ip_hash;
server backend1.example.com;
server backend2.example.com;
server backend3.example.com down;
server backend4.example.com;
}
注意!ip_hash不能使用權重weight
語法:keepalive連接數
所屬模塊:upstream
nginx負載均衡器的活動鏈接數緩存
連接數(keepalive的值)指定了每個工作進程中保留的持續連接到nginx負載均衡器緩存的最大值,如果超過這個設置值的閒置進程想鏈接到nginx負載均衡器組,最先連接的將被關閉
注意!keepalive指令不限制nginx工作進程可以連接到nginx負載均衡器可以開啓的最大工作進程,如果有需要的話,新進程還是會被髮起,連接數應該被設置最後低來滿足nginx負載均衡器處理新進的連接。
帶有持續連接的memcached upstream配置例子
upstream memcached_backend {
server 127.0.0.1:11211;
server 10.0.0.2:11211;
keepalive 32;
}
server {
...
location /memcached/ {
set $memcached_key $uri;
memcached_pass memcached_backend;
}
}
nginx負載均衡器內置變量
nginx負載均衡模塊ngx_http_upstream_module支持下列內置變量:
$upstream_addr
保存一個服務器的ip地址和端口號或者unix-domain套接字文件的路徑。如果在處理請求過程中使用了多個服務器,那麼他們的地址將以逗號分割,例如:“192.168.1.1:80,192.168.1.2:80,unix:/tmp/sock”。如果一個內置的從一個服務器組到另一個服務器組的重定向使用X-Accel-Redirect or error_page,那麼這些服務器組以冒號隔開,例如“192.168.1.1:80,192.168.1.2:80,unix:/tmp/sock:192.168.10.1:80,192.138.10.2:80”。
$upstream_response_time
保存nginx負載均衡服務器響應時間,以毫秒計。多個響應也以逗號和冒號隔開。
$upstream_status
保存nginx負載均衡服務器響應代碼。多個響應代碼也以逗號或冒號隔開。
$upstream_http_...
保存nginx負載均衡服務器響應頭字段。注意!只有最後一個服務器響應頭字段被保存
nginx負載均衡策略可分爲兩大類:內置策略和擴展策略
內置策略包含加權輪詢和ip hash
擴展策略包含fair、通用hash、consistent hash等
nginx的upstream目前支持4中方式的分配:
1)輪詢默認
每個請求按時間順序逐一分配到不同的後端服務器,如果後端服務器down掉,能自動剔除
2)weight
指定輪詢機率,weight和訪問比率成正比,用於後端服務器性能不均的情況。
3)ip_hash
每個請求按訪問ip的hash結果分配,這樣每個訪客固定訪問一個後端服務器,可以解決session的問題
4)fair(第三方)
按後端服務器的響應時間來分配請求,響應時間短的優先分配
5)url_hash(第三方)
Nginx默認採用round_robin加權算法,如果要選擇其他的負載均衡算法,必須在upstream的配置上下文中通過配置指令ip_hash明確指定(該配置項最好放在其他server指令等的前面,以便檢查server配置項是否合理)比如ip_hash的upstream配置如下:
upstream load_balance{
ip_hash;
server localhost:8001;
server localhost:8002;
}
當整個http配置塊被Nginx解析完畢後,會調用各個http模塊對應的初始函數。對於模塊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;
}
}
默認採用加權輪詢策略的原因就在於上述代碼中的init賦值一行。如果用戶沒有做任何策略選擇,那麼執行的策略初始函數爲ngx_http_upstream_init_round_robin,也就是加權輪詢策略,否則的話執行的是uscfp[i]->peer.init_upstream指針函數,如果有配置執行ip_hash,那麼就是ngx_http_upstream_init_ip_hash()。
加權輪詢策略
全局準備工作
需要注意的是,配置文件中出現的參數只能和某些策略配合使用,所以如果發現默寫參數沒有生效,則應該檢查這一點。在配置解析的過程中,這些選項設置都別轉換爲nginx內對於的變量值,對應的結構體ngx_http_upstream_server_t如下(ngx_http_upstream.h):
typedef struct {
ngx_addr_t *addrs;//指向存儲IP地址的數組的指針,host信息(對應的是 ngx_url_t->addrs )
ngx_uint_t naddrs;//與第一個參數配合使用,數組元素個數(對應的是 ngx_url_t->naddrs )
ngx_uint_t weight;
ngx_uint_t max_fails;
time_t fail_timeout;
unsigned down:1;
unsigned backup:1;
} ngx_http_upstream_server_t;
這個階段的函數是ngx_http_upstream_init_round_robin()
首先是設置了一個回調指針,這個函數用來針對每個請求選擇後端服務器之前做一些初始化工作:
us->peer.init = ngx_http_upstream_init_round_robin_peer;
us類型是ngx_http_upstream_srv_conf_t:
typedef struct ngx_http_upstream_srv_conf_s ngx_http_upstream_srv_conf_t;
struct ngx_http_upstream_srv_conf_s {
ngx_http_upstream_peer_t peer;
void **srv_conf;//在 ngx_http_upstream()函數中被設置,指向的是本層的srv_conf
ngx_array_t *servers; /*array of ngx_http_upstream_server_t */
ngx_uint_t flags;//調用函數時ngx_http_upstream_add() 指定的標記
ngx_str_t host;//在函數 ngx_http_upstream_add() 中設置(e.g. upstream backend中的backend)
u_char *file_name;//"/usr/local/nginx/conf/nginx.conf"
ngx_uint_t line;//proxy在配置文件中的行號
in_port_t port;//使用的端口號(ngx_http_upstream_add()函數中添加, 指向ngx_url_t-->port,通常在函數ngx_parse_inet_url()中解析)
in_port_t default_port;//默認使用的端口號(ngx_http_upstream_add()函數中添加, 指向ngx_url_t-->default_port)
ngx_uint_t no_port; /* unsigned no_port:1 */
};
而ngx_http_upstream_peer_t
typedef struct {
//使用負載均衡的類型,默認採用 ngx_http_upstream_init_round_robin()
ngx_http_upstream_init_pt init_upstream;
//使用的負載均衡類型的初始化函數
ngx_http_upstream_init_peer_pt init;
//us->peer.data = peers; 指向的是 ngx_http_upstream_rr_peers_t(函數 ngx_http_upstream_init_round_robin()中設置)
void *data;
} ngx_http_upstream_peer_t;
ngx_http_upstream_init_peer_pt是函數指針類型
typedef ngx_int_t (*ngx_http_upstream_init_peer_pt)(ngx_http_request_t *r,
ngx_http_upstream_srv_conf_t *us);
服務器類型ngx_http_upstream_server_t見前面的解釋。
如果upstream中服務器爲空,那麼默認使用proxy_pass。將利用函數ngx_inet_resolve_host依據us參數中的host和port進行解析。將結果保存在一個ngx_url_t類型的變量中:
typedef struct {
ngx_str_t url; //保存IP地址+端口信息(e.g. 192.168.124.129:8011 或 money.163.com)
ngx_str_t host; //保存IP地址信息
ngx_str_t port_text; //保存port字符串
ngx_str_t uri; //uri部分,在函數ngx_parse_inet_url()中設置
in_port_t port; //端口,e.g. listen指令中指定的端口(listen 192.168.124.129:8011)
in_port_t default_port; //默認端口(當no_port字段爲真時,將默認端口賦值給port字段, 默認端口通常是80)
int family; //address family, AF_xxx
unsigned listen:1; //是否爲指監聽類的設置
unsigned uri_part:1;
unsigned no_resolve:1; //根據情況決定是否解析域名(將域名解析到IP地址)
unsigned one_addr:1; //等於1時,僅有一個IP地址
unsigned no_port:1; //標識url中沒有顯示指定端口(爲1時沒有指定)
unsigned wildcard:1; //標識是否使用通配符(e.g. listen *:8000;)
socklen_t socklen; //sizeof(struct sockaddr_in)
u_char sockaddr[NGX_SOCKADDRLEN]; //sockaddr_in結構指向它
ngx_addr_t *addrs; //數組大小是naddrs字段;每個元素對應域名的IP地址信息(struct sockaddr_in),在函數中賦值(ngx_inet_resolve_host())
ngx_uint_t naddrs; //url對應的IP地址個數,IP格式的地址將默認爲1
char *err; //錯誤信息字符串
} ngx_url_t;
此函數會創建後端服務器列表,並且將非後備服務器與後備服務器分開進行各自單獨的鏈表。每一個後端服務器用一個結構體ngx_http_upstream_rr_peer_t與之對應(ngx_http_upstream_round_robin.h):
typedef struct {
struct sockaddr *sockaddr;//後端服務器地址
socklen_t socklen;//後端服務器地址長度
ngx_str_t name;//後端名稱
ngx_int_t current_weight;//當前權重,nginx會在運行過程中調整此權重
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;//多長時間內出現max_fails次失敗便認爲後端down掉了
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;
列表最前面需要帶有一些head信息,用結構體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;//所有服務器總權重
unsigned single:1;//爲1表示後端服務器總共只有一臺,用於優化,此時不需要再做選擇
unsigned weighted:1;//爲1表示總的權重值等於服務器數量
ngx_str_t *name;
ngx_http_upstream_rr_peers_t *next;//後備服務器列表掛載在這個字段下
ngx_http_upstream_rr_peer_t peer[1];
};
函數的完整代碼如下(ngx_http_upstream_round_robin.c):
//函數:初始化服務器負載均衡表
//參數:
//us:ngx_http_upstream_main_conf_t結構體中upstreams數組元素
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;
//回調指針設置
us->peer.init = ngx_http_upstream_init_round_robin_peer;
//服務器數組指針不爲空
if (us->servers) {
server = us->servers->elts;
n = 0;
w = 0;
//遍歷所有服務器
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) {
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;
}
//非後備服務器列表頭中各屬性設置
peers->single = (n == 1);
peers->number = n;
peers->weighted = (w != n);
peers->total_weight = w;
peers->name = &us->host;
n = 0;
//後備服務器列表中各服務器項設置
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;
peers->peer[n].socklen = server[i].addrs[j].socklen;
peers->peer[n].name = server[i].addrs[j].name;
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;
peers->peer[n].weight = server[i].weight;
peers->peer[n].effective_weight = server[i].weight;
peers->peer[n].current_weight = 0;
n++;
}
}
//非後備服務器列表掛載的位置
us->peer.data = peers;
/* backup servers */
//後備服務器
n = 0;
w = 0;
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_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;
//後備服務器列表頭中各屬性設置
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++) {
for (j = 0; j < server[i].naddrs; j++) {
if (!server[i].backup) {
continue;
}
backup->peer[n].sockaddr = server[i].addrs[j].sockaddr;
backup->peer[n].socklen = server[i].addrs[j].socklen;
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;
n++;
}
}
//後備服務器掛載
peers->next = backup;
return NGX_OK;
}
//us參數中服務器指針爲空,例如用戶直接在proxy_pass等指令後配置後端服務器地址
/* an upstream implicitly defined by proxy_pass, etc. */
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_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還要單獨爲本輪選擇做一些初始化(針對一個客戶請求,nginx會進行多次嘗試選擇,嘗試全部失敗後才返回502錯誤,所以注意一輪選擇與一次選擇的區別)。
在前面的函數ngx_http_upstream_init_round_robin()中設置的回調函數us->peer.init,它的調用位置是函數ngx_http_upstream_init_request中(ngx_http_upstream.c):
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_init_round_robin_peer()完成了那些工作。
它除了完成初始化工作之外,另外的核心工作是設置回調指針。
函數ngx_http_upstream_init_round_robin_peer的完整代碼(ngx_http_upstream_round_robin.c):
//函數:
//功能:針對每個請求選擇後端服務器前做一些初始化工作
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;
rrp->current = 0;
//n取值爲:非後備服務器和後備服務器列表中個數較大的那個值
n = rrp->peers->number;
if (rrp->peers->next && rrp->peers->next->number > n) {
n = rrp->peers->next->number;
}
//如果n小於一個指針變量所能表示的範圍
if (n <= 8 * sizeof(uintptr_t)) {
//直接使用已有的指針類型的data變量做位圖(tried是位圖,用來標識在一輪選擇中,各個後端服務器是否已經被選擇過)
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;
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_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;
//如果只有一臺後端服務器,Nginx直接選擇並返回
if (rrp->peers->single) {
peer = &rrp->peers->peer[0];
if (peer->down) {
goto failed;
}
} else {
//有多臺後端服務器
/* there are several peers */
//按照各臺服務器的當前權值進行選擇
peer = ngx_http_upstream_get_peer(rrp);
if (peer == NULL) {
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); */
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;
pc->tries = rrp->peers->number;
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;
}
/* ngx_lock_mutex(peers->mutex); */
}
/* 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;
//如果後備服務器也選擇失敗,則返回NGX_BUSY
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;
ngx_http_upstream_rr_peer_t *peer, *best;
now = ngx_time();
best = NULL;
total = 0;
for (i = 0; i < rrp->peers->number; i++) {
//計算當前服務器的標記位在位圖中的位置
n = i / (8 * sizeof(uintptr_t));
m = (uintptr_t) 1 << i % (8 * sizeof(uintptr_t));
//已經選擇過,跳過
if (rrp->tried[n] & m) {
continue;
}
//當前服務器對象
peer = &rrp->peers->peer[i];
//當前服務器已宕機,排除
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;
if (peer->effective_weight < peer->weight) {
peer->effective_weight++;
}
if (best == NULL || peer->current_weight > best->current_weight) {
best = peer;
}
}
if (best == NULL) {
return NULL;
}
//所選擇的服務器在服務器列表中的位置
i = best - &rrp->peers->peer[0];
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;
best->checked = now;
return best;
}
要理解這個函數的工作原理,先要區分下表示服務的ngx_http_upstream_rr_peer_t結構體中的以下三個成員變量:
ngx_int_t current_weight
ngx_int_t effective_weight
ngx_int_t weight
effective_weight相當於質量(來源於配置的weight),current_weight相當於重量。前者反應本質,一般是不變的。current_weight是運行時的動態權值,它的變化基於effective_weight。但是effective_weight在其對應的peer服務異常,會被調低,當服務恢復正常時,effective_weight會逐漸恢復到實際值(配置的weight)
下面我們結合具體的代碼來看
它們在函數ngx_http_upstream_init_round_robin中被初始化:
for (i = 0; i < us->servers->nelts; i++) {
for (j = 0; j < server[i].naddrs; j++) {
if (server[i].backup) {
continue;
}
peers->peer[n].weight = server[i].weight;
peers->peer[n].effective_weight = server[i].weight;
peers->peer[n].current_weight = 0;
n++;
}
}
/* backup servers */
for (i = 0; i < us->servers->nelts; i++) {
for (j = 0; j < server[i].naddrs; j++) {
if (!server[i].backup) {
continue;
}
backup->peer[n].weight = server[i].weight;
backup->peer[n].effective_weight = server[i].weight;
backup->peer[n].current_weight = 0;
n++;
}
}
/* an upstream implicitly defined by proxy_pass, etc. */
for (i = 0; i < u.naddrs; i++) {
peers->peer[i].weight = 1;
peers->peer[i].effective_weight = 1;
peers->peer[i].current_weight = 0;
}
可以看到weight、effective_weight都是初始化爲配置項中的weight值。current_weight初始化爲0。
weight的值在整個運行過程中不發生變化
total變量記錄了針對一個服務器列表的一次輪詢過程中輪詢到的所有服務器的effective_weight總和。在每一次針對服務器列表的輪詢之前會置爲0。
遍歷服務列表的過程中,每遍歷到一個服務,會在該服務的current_weight上加上其對應的effective_weight。這是累加。如果對統一的服務列表進行另一次輪詢,那麼會在前面計算的current_weight的基礎上再加上effective_weight。
輪詢策略是取current_weight最大的服務器。每次取到後端服務(用best表示)後,都會把該對象的peer的current_weight減去total的值。因爲該服務剛被選中過,因此要降低權值。
關於effective_weight的變化,有兩處,一個是在函數ngx_http_upstream_get_peer中:
//服務正常,effective_weight 逐漸恢復正常
if (peer->effective_weight < peer->weight) {
peer->effective_weight++;
}
另一處是在釋放後端服務器的函數ngx_http_upstream_free_round_robin_peer中:
if (peer->max_fails) {
//服務發生異常時,調低effective_weight
peer->effective_weight -= peer->weight / peer->max_fails;
}
權重高的會優先被選中,而且被選中的頻率也更高。權重低的也會由於權重逐漸增長獲得被選中的機會。
下圖是一個加權輪詢實例:
解釋一下:每行都有3臺服務器,分爲a,b,c,第一行選擇了a服務器(a的權重最高 5),選中之後就current_weight-total即5-8=-3。
釋放後端服務器
連接後端服務器並且正常處理當前客戶端請求後需釋放後端服務器。如果在某一輪選擇裏,某次選擇的服務器因連接失敗或請求處理失敗而需要重新進行選擇,那麼這時候就需要做一些額外的處理。
//函數:
//功能:釋放後端服務器
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 */
//後端服務只有一個
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 {
/* mark peer live if check passed */
if (peer->accessed < peer->checked) {
peer->fails = 0;
}
}
//ngx_peer_connection_t結構體中tries字段:
//表示在連接一個遠端服務器時,當前連接出現異常失敗後可以重試的次數,也就是允許失敗的次數
if (pc->tries) {
pc->tries--;
}
/* ngx_unlock_mutex(rrp->peers->mutex); */
}
整個加權輪詢的流程
流程圖如下:
說明:
首先是全局初始化,由函數ngx_http_upstream_init_round_robin完成,它在函數ngx_http_upstream_init_main_conf中被調用,代碼:
static char *
ngx_http_upstream_init_main_conf(ngx_conf_t *cf, void *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_init_round_robin_peer,它在函數ngx_http_upstream_init_request中被調用:
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中被調用:
//函數:連接後端upstream
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被調用:
//函數:發送數據到後端upstream
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函數,這個函數可能再次調用peer.get調用別的連接。
static void
ngx_http_upstream_next(ngx_http_request_t *r, ngx_http_upstream_t *u,
ngx_uint_t ft_type)
{
...
if (u->peer.sockaddr) {
if (ft_type == NGX_HTTP_UPSTREAM_FT_HTTP_404) {
state = NGX_PEER_NEXT;
} else {
state = NGX_PEER_FAILED;
}
//釋放後端服務器
u->peer.free(&u->peer, u->peer.data, state);
u->peer.sockaddr = NULL;
}
...
if (status) {
u->state->status = status;
if (u->peer.tries == 0 || !(u->conf->next_upstream & ft_type)) {
#if (NGX_HTTP_CACHE)
if (u->cache_status == NGX_HTTP_CACHE_EXPIRED
&& (u->conf->cache_use_stale & ft_type))
{
ngx_int_t rc;
rc = u->reinit_request(r);
if (rc == NGX_OK) {
u->cache_status = NGX_HTTP_CACHE_STALE;
rc = ngx_http_upstream_cache_send(r, u);
}
ngx_http_upstream_finalize_request(r, u, rc);
return;
}
#endif
//結束請求
ngx_http_upstream_finalize_request(r, u, status);
return;
}
}
...
//再次發起連接
ngx_http_upstream_connect(r, u);
}
函數ngx_http_upstream_connect中會調用ngx_event_connect_peer,進而調用ngx_http_upstream_get_round_robin_peer再次選擇後端服務器。