nginx 是優秀的反向代理服務器,這里主要講它的健康檢查和負載均衡機制,以及這種機制帶來的問題。所謂健康檢查,就是當后端出現問題(具體什么叫出現問題,依賴於具體實現,各個實現定義不一樣),不再往這個后端分發請求,並且做后續的檢查,直到這個后端恢復正常。所謂負載均衡,就是選擇后端的方式,如何(根據后端的能力)將請求均衡的分發到后端。此外,當請求某個后端失敗時,要將該請求分發到其它后端(redispatch)。這里以ngx_http_upstream_round_robin(簡稱RR)做為負載均衡模塊,以ngx_http_proxy_module(檢查proxy)作為后端代理模塊。
nginx 的健康檢查和負載均衡是密切相關的,它沒有獨立的健康檢查模塊,而是使用業務請求作為健康檢查,這省去了獨立健康檢查線程,這是好處。壞處是,當業務復雜時,可能出現誤判,例如后端響應超時,這是可能是后端宕機,也可能是某個業務請求自身出現問題,跟后端無關。如果后端宕機,nginx還要在將它標記為不可用之后,仍不時的將業務請求分發給它,以檢查后端是否恢復。
nginx 完成客戶端請求Header部分的解析,upstream 調用RR模塊的peer.get 選擇具體的后端。當請求結束,upstream 調用RR模塊的peer.free,向RR反饋后端的健康情況。當upstream和后端通信時,出現錯誤會調用 ngx_http_upstream_next,
void ngx_http_upstream_next(ngx_http_request_t *r, ngx_http_upstream_t *u, ngx_uint_t ft_type); 第三個參數指明錯誤類型,共有如下錯誤類型
#define NGX_HTTP_UPSTREAM_FT_ERROR 0x00000002
#define NGX_HTTP_UPSTREAM_FT_TIMEOUT 0x00000004
#define NGX_HTTP_UPSTREAM_FT_INVALID_HEADER 0x00000008
#define NGX_HTTP_UPSTREAM_FT_HTTP_500 0x00000010
#define NGX_HTTP_UPSTREAM_FT_HTTP_502 0x00000020
#define NGX_HTTP_UPSTREAM_FT_HTTP_503 0x00000040
#define NGX_HTTP_UPSTREAM_FT_HTTP_504 0x00000080
#define NGX_HTTP_UPSTREAM_FT_HTTP_404 0x00000100
#define NGX_HTTP_UPSTREAM_FT_UPDATING 0x00000200
#define NGX_HTTP_UPSTREAM_FT_BUSY_LOCK 0x00000400
#define NGX_HTTP_UPSTREAM_FT_MAX_WAITING 0x00000800
#define NGX_HTTP_UPSTREAM_FT_NOLIVE 0x40000000
ngx_http_upstream_next,只要錯誤類型不是 NGX_HTTP_UPSTREAM_FT_HTTP_404,都認為后端有問題(NGX_PEER_FAILED)
if (ft_type == NGX_HTTP_UPSTREAM_FT_HTTP_404) {
state = NGX_PEER_NEXT;
} else {
state = NGX_PEER_FAILED;
}
ngx_http_upstream_next 調用RR的peer.free,RR根據state判斷剛才接受請求的后端是否健康。
if (ft_type != NGX_HTTP_UPSTREAM_FT_NOLIVE) {
u->peer.free(&u->peer, u->peer.data, state);
}
ngx_http_upstream_next 如果超過最大重試次數(默認為后端的個數,每試過一個,就減1),或者proxy設置不允許redispatch,則向客戶端返回響應status。
if (u->peer.tries == 0 || !(u->conf->next_upstream & ft_type)) {
ngx_http_upstream_finalize_request(r, u, status);
}
proxy 模塊的 proxy_next_upstream 配置,在何種情況下將請求redispatch到下一個后端。
剛剛談到,只要錯誤類型不是 NGX_HTTP_UPSTREAM_FT_HTTP_404,都認為后端有問題。這里的錯誤類型包括,連接后端失敗,連接,讀寫后端超時,后端返回了500,502,504等。這個策略是有待商榷的,尤其是讀寫后端超時也判斷為后端不可用。因為某個業務請求,可能因為自身的原因而導致讀寫超時。注意,在proxy_next_upstream 中指定timeout,http_504 是不同的,前者表示upstream連接,讀寫后端超時,后者表示后端返回的http code 是504。
實際上健康檢查不是必須的,因為redispatch的存在保證了,就算有后端宕機,客戶端仍將收到正確的響應。那么我們考慮關掉健康檢查。通過upstream 的server配置的max_fails 參數
RR 的peer.get,如果max_fails 為0,則該后端總是可用的(就算它真有問題)。
if (peer->max_fails == 0
|| peer->fails < peer->max_fails)
{
break;
}
因為redispatch的次數,取決於后端的個數,所以后端的個數稍微多一點是有好處的。
下面是一些佐證分析的測試。
upstream test {
server 127.0.0.1:8060 max_fails=0;
server 127.0.0.1:8070 max_fails=0;
server 127.0.0.1:8080 max_fails=0;
server 127.0.0.1:8090 max_fails=0;
}
只有8060,8070是存活的,8080,8090處於不可用狀態,這里max_fails=0,關閉了健康檢查。
proxy_read_timeout 2;
讀超時設為2S。
proxy_next_upstream error timeout;
默認當 error 和 timeout發生時,redispatch。
測試請求的sleep參數指定后端的sleep時間,code參數指定后端返回的http code。根據time和sleep時間的對比,判斷重試了幾個后端。
time curl "http://127.0.0.1:8099/index.php?sleep=3" -vv
real 0m4.014s
sleep=3,讀超時,重試了2個后端。
修改配置 proxy_next_upstream error;
time curl "http://127.0.0.1:8099/index.php?sleep=3" -vv
real 0m2.018s
讀超時,不再redispatch,重試了1個后端。
修改配置 proxy_next_upstream error http_504;
time curl "http://127.0.0.1:8099/index.php?sleep=1" -vv
real 0m1.022s
這個是正常請求。
time curl "http://127.0.0.1:8099/index.php?sleep=1&code=504" -vv
real 0m2.023s
讓后端返回504,此時nginx會做redispatch,重試了2個后端
但是nginx返回給客戶端的是502,不是504,因為所有的后端都返回504,nginx認為后端不可用,返回502.
測試健康檢查,關掉redispatch。proxy_next_upstream off;
curl "http://127.0.0.1:8099/index.php?sleep=3" -vv
返回了兩次502,兩次504。存活的后端返回504,有問題的返回502。
修改 max_fails server 127.0.0.1:8060 max_fails=1; 對8060開啟健康檢查。
curl "http://127.0.0.1:8099/index.php?sleep=3" -vv
第一輪4次請求,返回兩次502,兩次504
8080和8090有問題,返回502,8060和8070響應超時,返回504,因為8060開啟了健康檢查,並且返回了504,所以被標記為不可用。
第二輪4次請求,返回三次502,一次504。8070沒有開啟健康檢查,所以仍然返回504。
根據測試分析,業務請求(sleep 3s,或者 輸出 http 504)可以讓nginx誤以為后端宕了,而這時后端活得好好的。在私有雲平台,這個通常不是問題,把超時設大點,不返回5XX錯誤,可以避免這個問題。但是在公有雲平台,這是致命的,因為業務可以編程輸出5XX錯誤。有兩種方法應對,一種是關閉健康檢查,一種是修改nginx的代碼,僅對 NGX_HTTP_UPSTREAM_FT_ERROR 判定為后端有問題。