搞懂分散式技術9:Nginx負載均衡原理與實踐

程式設計師江湖發表於2018-06-22

Nginx負載均衡與反向代理—《億級流量網站架構核心技術》

原創: 張開濤 開濤的部落格 2017-03-24

本篇摘自《億級流量網站架構核心技術》第二章 Nginx負載均衡與反向代理 部分內容。

當我們的應用單例項不能支撐使用者請求時,此時就需要擴容,從一臺伺服器擴容到兩臺、幾十臺、幾百臺。然而,使用者訪問時是通過如的方式訪問,在請求時,瀏覽器首先會查詢DNS伺服器獲取對應的IP,然後通過此IP訪問對應的服務。

因此,一種方式是域名對映多個IP,但是,存在一個最簡單的問題,假設某臺伺服器重啟或者出現故障,DNS會有一定的快取時間,故障後切換時間長,而且沒有對後端服務進行心跳檢查和失敗重試的機制。

因此,外網DNS應該用來實現用GSLB(全域性負載均衡)進行流量排程,如將使用者分配到離他最近的伺服器上以提升體驗。而且當某一區域的機房出現問題時(如被挖斷了光纜),可以通過DNS指向其他區域的IP來使服務可用。

可以在站長之家使用“DNS查詢”,查詢c.3.cn可以看到類似如下的結果。

即不同的運營商返回的公網IP是不一樣的。

對於內網DNS,可以實現簡單的輪詢負載均衡。但是,還是那句話,會有一定的快取時間並且沒有失敗重試機制。因此,我們可以考慮選擇如HaProxy和Nginx。

而對於一般應用來說,有Nginx就可以了。但Nginx一般用於七層負載均衡,其吞吐量是有一定限制的。為了提升整體吞吐量,會在DNS和Nginx之間引入接入層,如使用LVS(軟體負載均衡器)、F5(硬負載均衡器)可以做四層負載均衡,即首先DNS解析到LVS/F5,然後LVS/F5轉發給Nginx,再由Nginx轉發給後端Real Server。

對於一般業務開發人員來說,我們只需要關心到Nginx層面就夠了,LVS/F5一般由系統/運維工程師來維護。Nginx目前提供了HTTP(ngx_http_upstream_module)七層負載均衡,而1.9.0版本也開始支援TCP(ngx_stream_upstream_module)四層負載均衡。

此處再澄清幾個概念。二層負載均衡是通過改寫報文的目標MAC地址為上游伺服器MAC地址,源IP地址和目標IP地址是沒有變的,負載均衡伺服器和真實伺服器共享同一個VIP,如LVS DR工作模式。四層負載均衡是根據埠將報文轉發到上游伺服器(不同的IP地址+埠),如LVS NAT模式、HaProxy,七層負載均衡是根據埠號和應用層協議如HTTP協議的主機名、URL,轉發報文到上游伺服器(不同的IP地址+埠),如HaProxy、Nginx。

這裡再介紹一下LVS DR工作模式,其工作在資料鏈路層,LVS和上游伺服器共享同一個VIP,通過改寫報文的目標MAC地址為上游伺服器MAC地址實現負載均衡,上游伺服器直接響應報文到客戶端,不經過LVS,從而提升效能。但因為LVS和上游伺服器必須在同一個子網,為了解決跨子網問題而又不影響負載效能,可以選擇在LVS後邊掛HaProxy,通過四到七層負載均衡器HaProxy叢集來解決跨網和效能問題。這兩個“半成品”的東西相互取長補短,組合起來就變成了一個“完整”的負載均衡器。現在Nginx的stream也支援TCP,所以Nginx也算是一個四到七層的負載均衡器,一般場景下可以用Nginx取代HaProxy。

在繼續講解之前,首先統一幾個術語。接入層、反向代理伺服器、負載均衡伺服器,在本文中如無特殊說明則指的是Nginx。upstream server即上游伺服器,指Nginx負載均衡到的處理業務的伺服器,也可以稱之為real server,即真實處理業務的伺服器。

對於負載均衡我們要關心的幾個方面如下。

上游伺服器配置:使用upstream server配置上游伺服器。

負載均衡演算法:配置多個上游伺服器時的負載均衡機制。

失敗重試機制:配置當超時或上游伺服器不存活時,是否需要重試其他上游伺服器。

伺服器心跳檢查:上游伺服器的健康檢查/心跳檢查。

Nginx提供的負載均衡可以實現上游伺服器的負載均衡、故障轉移、失敗重試、容錯、健康檢查等,當某些上游伺服器出現問題時可以將請求轉到其他上游伺服器以保障高可用,並可以通過OpenResty實現更智慧的負載均衡,如將熱點與非熱點流量分離、正常流量與爬蟲流量分離等。Nginx負載均衡器本身也是一臺反向代理伺服器,將使用者請求通過Nginx代理到內網中的某臺上遊伺服器處理,反向代理伺服器可以對響應結果進行快取、壓縮等處理以提升效能。Nginx作為負載均衡器/反向代理伺服器如下圖所示。

本章首先會講解Nginx HTTP負載均衡,最後會講解使用Nginx實現四層負載均衡。

 

第一步我們需要給Nginx配置上游伺服器,即負載均衡到的真實處理業務的伺服器,通過在http指令下配置upstream即可。

upstream backend {

server 192.168.61.1:9080 weight=1;

server 192.168.61.1:9090 weight=2;

}

upstream server主要配置。

IP地址和埠:配置上游伺服器的IP地址和埠。

權重:weight用來配置權重,預設都是1,權重越高分配給這臺伺服器的請求就越多(如上配置為每三次請求中一個請求轉發給9080,其餘兩個請求轉發給9090),需要根據伺服器的實際處理能力設定權重(比如,物理伺服器和虛擬機器就需要不同的權重)。

然後,我們可以配置如下proxy_pass來處理使用者請求。

location / {

proxy_pass http://backend;

}

當訪問Nginx時,會將請求反向代理到backend配置的Upstream Server。接下來我們看一下負載均衡演算法。

 

負載均衡用來解決使用者請求到來時如何選擇Upstream Server進行處理,預設採用的是round-robin(輪詢),同時支援其他幾種演算法。

round-robin:輪詢,預設負載均衡演算法,即以輪詢的方式將請求轉發到上游伺服器,通過配合weight配置可以實現基於權重的輪詢。

ip_hash:根據客戶IP進行負載均衡,即相同的IP將負載均衡到同一個Upstream Server。

upstream backend {

ip_hash;

server 192.168.61.1:9080 weight=1;

server 192.168.61.1:9090 weight=2;

}

hash key [consistent]:對某一個key進行雜湊或者使用一致性雜湊演算法進行負載均衡。使用Hash演算法存在的問題是,當新增/刪除一臺伺服器時,將導致很多key被重新負載均衡到不同的伺服器(從而導致後端可能出現問題);因此,建議考慮使用一致性雜湊演算法,這樣當新增/刪除一臺伺服器時,只有少數key將被重新負載均衡到不同的伺服器。

雜湊演算法:此處是根據請求uri進行負載均衡,可以使用Nginx變數,因此,可以實現複雜的演算法。

upstream backend {

hash $uri;

server 192.168.61.1:9080 weight=1;

server 192.168.61.1:9090 weight=2;

}

一致性雜湊演算法:consistent_key動態指定。

upstream nginx_local_server {

hash $consistent_key consistent;

server 192.168.61.1:9080 weight=1;

server 192.168.61.1:9090 weight=2;

}

如下location指定了一致性雜湊key,此處會優先考慮求引數cat(類目),如果沒有,則再根據請求uri進行負載均衡。

location / {

set $consistent_key $arg_cat;

if($consistent_key = “”) {

set $consistent_key $request_uri;

}

}

而實際我們是通過lua設定一致性雜湊key。

set_by_lua_file $consistent_key”lua_balancing.lua”;

lua_balancing.lua程式碼。

local consistent_key = args.cat

if not consistent_key or consistent_key == “ then

consistent_key = ngx_var.request_uri

end

local value = balancing_cache:get(consistent_key)

if not value then

success,err = balancing_cache:set(consistent_key, 1, 60)

else

newval,err = balancing_cache:incr(consistent_key, 1)

end

如果某一個分類請求量太大,上游伺服器可能處理不了這麼多的請求,此時可以在一致性雜湊key後加上遞增的計數以實現類似輪詢的演算法。

if newval > 5000 then

consistent_key = consistent_key .. `_` .. newval

end

least_conn:將請求負載均衡到最少活躍連線的上游伺服器。如果配置的伺服器較少,則將轉而使用基於權重的輪詢演算法。

Nginx商業版還提供了least_time,即基於最小平均響應時間進行負載均衡。

 

主要有兩部分配置:upstream server和proxy_pass。

upstream backend {

server 192.168.61.1:9080 max_fails=2 fail_timeout=10s weight=1;

server 192.168.61.1:9090 max_fails=2 fail_timeout=10s weight=1;

}

通過配置上游伺服器的max_fails和fail_timeout,來指定每個上游伺服器,當fail_timeout時間內失敗了max_fails次請求,則認為該上游伺服器不可用/不存活,然後將摘掉該上游伺服器,fail_timeout時間後會再次將該伺服器加入到存活上游伺服器列表進行重試。

location /test {

proxy_connect_timeout 5s;

proxy_read_timeout 5s;

proxy_send_timeout 5s;

proxy_next_upstreamerror timeout;

proxy_next_upstream_timeout 10s;

proxy_next_upstream_tries 2;

proxy_pass http://backend;

add_header upstream_addr $upstream_addr;

}

}

然後進行proxy_next_upstream相關配置,當遇到配置的錯誤時,會重試下一臺上游伺服器。

詳細配置請參考“代理層超時與重試機制”中的Nginx部分。

 

Nginx對上游伺服器的健康檢查預設採用的是惰性策略,Nginx商業版提供了health_check進行主動健康檢查。當然也可以整合nginx_upstream_check_module(https://github.com/yaoweibin/nginx_upstream_check_module)模組來進行主動健康檢查。

nginx_upstream_check_module支援TCP心跳和HTTP心跳來實現健康檢查。

心跳檢查

upstream backend {

server 192.168.61.1:9080 weight=1;

server 192.168.61.1:9090 weight=2;

check interval=3000 rise=1 fall=3 timeout=2000 type=tcp;

}

此處配置使用TCP進行心跳檢測。

interval:檢測間隔時間,此處配置了每隔3s檢測一次。

fall:檢測失敗多少次後,上游伺服器被標識為不存活。

rise:檢測成功多少次後,上游伺服器被標識為存活,並可以處理請求。

timeout:檢測請求超時時間配置。

心跳檢查

upstream backend {

server 192.168.61.1:9080 weight=1;

server 192.168.61.1:9090 weight=2;

check interval=3000 rise=1 fall=3 timeout=2000 type=http;

check_http_send “HEAD /status HTTP/1.0rnrn”;

check_http_expect_alive http_2xx http_3xx;

}

HTTP心跳檢查有如下兩個需要額外配置。

check_http_send:即檢查時發的HTTP請求內容。

check_http_expect_alive:當上遊伺服器返回匹配的響應狀態碼時,則認為上游伺服器存活。

此處需要注意,檢查間隔時間不能太短,否則可能因為心跳檢查包太多造成上游伺服器掛掉,同時要設定合理的超時時間。

本文使用的是openresty/1.11.2.1(對應nginx-1.11.2),安裝Nginx之前需要先打nginx_upstream_check_module補丁(check_1.9.2+.patch),到Nginx目錄下執行如下shell:

patch -p0 < /usr/servers/nginx_upstream_check_module-master/check_1.9.2+.patch。

如果不安裝補丁,那麼nginx_upstream_check_module模組是不工作的,建議使用wireshark抓包檢視其是否工作。

 

域名上游伺服器

upstream backend {

server c0.3.cn;

server c1.3.cn;

}

Nginx社群版,是在Nginx解析配置檔案的階段將域名解析成IP地址並記錄到upstream上,當這兩個域名對應的IP地址發生變化時,該upstream不會更新。Nginx商業版才支援動態更新。

不過,proxy_pass http://c0.3.cn是支援動態域名解析的。

備份上游伺服器

upstream backend {

server 192.168.61.1:9080 weight=1;

server 192.168.61.1:9090 weight=2 backup;

}

9090埠上游伺服器配置為備上游伺服器,當所有主上游伺服器都不存活時,請求會轉發給備上游伺服器。

如通過縮容上游伺服器進行壓測時,要摘掉一些上游伺服器進行壓測,但為了保險起見會配置一些備上游伺服器,當壓測的上游伺服器都掛掉時,流量可以轉發到備上游伺服器,從而不影響使用者請求處理。

不可用上游伺服器

upstream backend {

server 192.168.61.1:9080 weight=1;

server 192.168.61.1:9090 weight=2 down;

}

9090埠上游伺服器配置為永久不可用,當測試或者機器出現故障時,暫時通過該配置臨時摘掉機器。

 

配置Nginx與上游伺服器的長連線,客戶端與Nginx之間的長連線可以參考位置“超時與重試”的相應部分。

通過keepalive指令配置長連線數量。

upstream backend {

server 192.168.61.1:9080 weight=1;

server 192.168.61.1:9090 weight=2 backup;

keepalive 100;

}

通過該指令配置了每個Worker程式與上游伺服器可快取的空閒連線的最大數量。當超出這個數量時,最近最少使用的連線將被關閉。keepalive指令不限制Worker程式與上游伺服器的總連線。

如果想要跟上游伺服器建立長連線,則一定別忘了以下配置。

location / {

#支援keep-alive

proxy_http_version 1.1;

proxy_set_header Connection “”;

proxy_pass http://backend;

}

如果是http/1.0,則需要配置傳送“Connection: Keep-Alive”請求頭。

上游伺服器不要忘記開啟長連線支援。

接下來,我們看一下Nginx是如何實現keepalive的(ngx_http_upstream_keepalive _module),獲取連線時的部分程式碼。

ngx_http_upstream_get_keepalive_peer(ngx_peer_connection_t*pc, void *data) {

//1.首先詢問負載均衡使用哪臺伺服器(IP和埠)

rc =kp->original_get_peer(pc, kp->data);

cache =&kp->conf->cache;

//2.輪詢 “空閒連線池”

for (q =ngx_queue_head(cache);

q!= ngx_queue_sentinel(cache);

q =ngx_queue_next(q))

{

item = ngx_queue_data(q,ngx_http_upstream_keepalive_cache_t, queue);

c =item->connection;

//2.1.如果“空閒連線池”快取的連線IP和埠與負載均衡到的IP和埠相同,則使用此連線

if (ngx_memn2cmp((u_char *)&item->sockaddr, (u_char *) pc->sockaddr,

item->socklen,pc->socklen)

== 0)

{

//2.2.從“空閒連線池”移除此連線並壓入“釋放連線池”棧頂

ngx_queue_remove(q);

ngx_queue_insert_head(&kp->conf->free, q);

goto found;

}

}

//3.如果 “空閒連線池”沒有可用的長連線,將建立短連線

return NGX_OK;

釋放連線時的部分程式碼。

ngx_http_upstream_free_keepalive_peer(ngx_peer_connection_t*pc, void *data, ngx_uint_t state) {

c = pc->connection;//當前要釋放的連線

//1.如果“釋放連線池”沒有待釋放連線,那麼需要從“空閒連線池”騰出一個空間給新的連線使用(這種情況存在於建立連線數超出了連線池大小時,這就會出現震盪)

if(ngx_queue_empty(&kp->conf->free)) {

q =ngx_queue_last(&kp->conf->cache);

ngx_queue_remove(q);

item= ngx_queue_data(q, ngx_http_upstream_keepalive_cache_t, queue);

ngx_http_upstream_keepalive_close(item->connection);

} else {//2.從“釋放連線池”釋放一個連線

q =ngx_queue_head(&kp->conf->free);

ngx_queue_remove(q);

item= ngx_queue_data(q, ngx_http_upstream_keepalive_cache_t, queue);

}

//3.將當前連線壓入“空閒連線池”棧頂供下次使用

ngx_queue_insert_head(&kp->conf->cache, q);

item->connection = c;

總長連線數是“空閒連線池”+“釋放連線池”的長連線總數。首先,長連線配置不會限制Worker程式可以開啟的總連線數(超了的作為短連線)。另外,連線池一定要根據實際場景合理進行設定。

1.空閒連線池太小,連線不夠用,需要不斷建連線。

2.空閒連線池太大,空閒連線太多,還沒使用就超時。

另外,建議只對小報文開啟長連線。

 

反向代理除了實現負載均衡之外,還提供如快取來減少上游伺服器的壓力。

1.全域性配置(proxy cache)

proxy_buffering on;

proxy_buffer_size 4k;

proxy_buffers 512 4k;

proxy_busy_buffers_size 64k;

proxy_temp_file_write_size 256k;

proxy_cache_lock on;

proxy_cache_lock_timeout 200ms;

proxy_temp_path /tmpfs/proxy_temp;

proxy_cache_path /tmpfs/proxy_cache levels=1:2keys_zone =cache:512m inactive=5m max_size=8g;

proxy_connect_timeout 3s;

proxy_read_timeout 5s;

proxy_send_timeout 5s;

開啟proxy buffer,快取內容將存放在tmpfs(記憶體檔案系統)以提升效能,設定超時時間。

2.location配置

location ~ ^/backend/(.*)$ {

#設定一致性雜湊負載均衡key

set_by_lua_file $consistent_key “/export/App/c.3.cn/lua/lua_ balancing_backend.properties”;

#失敗重試配置

proxy_next_upstream error timeout http_500 http_502 http_504;

proxy_next_upstream_timeout 2s;

proxy_next_upstream_tries 2;

#請求上游伺服器使用GET方法(不管請求是什麼方法)

proxy_method GET;

#不給上游伺服器傳遞請求體

proxy_pass_request_body off;

#不給上游伺服器傳遞請求頭

proxy_pass_request_headers off;

#設定上游伺服器的哪些響應頭不傳送給客戶端

proxy_hide_header Vary;

#支援keep-alive

proxy_http_version 1.1;

proxy_set_header Connection “”;

#給上游伺服器傳遞Referer、Cookie和Host(按需傳遞)

proxy_set_header Referer $http_referer;

proxy_set_header Cookie $http_cookie;

proxy_set_header Host web.c.3.local;

proxy_pass http://backend /$1$is_args$args;

}

我們開啟了proxy_pass_request_body和proxy_pass_request_headers,禁止向上遊伺服器傳遞請求頭和內容體,從而使得上游伺服器不受請求頭攻擊,也不需要解析;如果需要傳遞,則使用proxy_set_header按需傳遞即可。

我們還可以通過如下配置來開啟gzip支援,減少網路傳輸的資料包大小。

gzip on;

gzip_min_length 1k;

gzip_buffers 16 16k;

gzip_http_version 1.0;

gzip_proxied any;

gzip_comp_level 2;

gzip_types text/plainapplication/x-java text/css application/xml;

gzip_vary on;

對於內容型響應建議開啟gzip壓縮,gzip_comp_level壓縮級別要根據實際壓測來決定(頻寬和吞吐量之間的抉擇)。

未完。

完。 


相關文章