搞懂分布式技術9:Nginx負載均衡原理與實踐
本篇摘自《億級流量網站架構核心技術》第二章 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的配置
第一步我們需要給Nginx配置上遊服務器,即負載均衡到的真實處理業務的服務器,通過在http指令下配置upstream即可。
upstream backend {
//server ip:端口 weight=權重值;
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
round-robin:輪詢,默認負載均衡算法,即以輪詢的方式將請求轉發到上遊服務器,通過配合weight配置可以實現基於權重的輪詢。
ip_hash
ip_hash:根據客戶IP進行負載均衡,即相同的IP將負載均衡到同一個Upstream Server。
upstream backend {
?
ip_hash;//ip_hash
?
server 192.168.61.1:9080 weight=1;
?
server 192.168.61.1:9090 weight=2;
?
}
hash key [consistent]
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時間後會再次將該服務器加入到存活上遊服務器列表進行重試。
upstream server和proxy_pass
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與上遊服務器的長連接,客戶端與Nginx之間的長連接可以參考位置“超時與重試”的相應部分。
通過keepalive指令配置長連接數量。
upstream backend { server 192.168.61.1:9080 weight=1; server 192.168.61.1:9090 weight=2 backup; keepalive 100;//LRU算法 }
通過該指令配置了每個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壓縮級別要根據實際壓測來決定(帶寬和吞吐量之間的抉擇)。
搞懂分布式技術9:Nginx負載均衡原理與實踐