NGINX基於Cookie和Header的負載均衡會話保持

Nginx是一個很高效穩定的軟負載均衡器,最新的版本可以負載均衡HTTP(s),TCP,UDP等多種協議的鏈接。一般訪問量比較大一點的Web站點都會用NGINX做HTTP協議的Web負載均衡,其後端一般是多個PHP或者JAVA中間件。另外NGINX還可以和Keepalived配合防止均衡器的單點故障,這一點要強於F5,A10這一類的硬件負載均衡設備。

但是F5,A10等硬件負載均衡器雖然價格昂貴但是仍然很有市場,其中原因之一就是硬件負載均衡器比Nginx配置簡單,具備圖形化界面,有圖形化的實時監測界面(收費版的Nginx Plux也有這個功能,但是價格更加昂貴)。但是最重要的一點,就是硬件負載均衡器有成熟的會話保持措施,這一點是Nginx的弱點。

一般來說,我們在java中都通過如下代碼進行用戶登錄後的服務端註冊,並且在用戶下次請求時無需再登陸一遍,這就是Servlet的Session

HttpSession session = request.getSession(false); 
session.setAttribute("data", data);
session.getAttribute("data"); 

使用了這種Session策略,那麼Web容器比如tomcat就爲當前用戶生成一個SessionID,並且以這個SessionID爲索引,存儲這個用戶相關的鍵值對,比如用戶名,登陸時間一類的。存儲在服務器的內存中。同時再response裏向用戶瀏覽器中設置一個cookie,這個cookie的名字爲jsessionid,內容爲服務器生成的隨機數SessionID。在用戶第二次請求時,將這個cookie發給服務器,服務器根據這個SessionID到內存中尋找相關數據,把用戶名什麼的提取出來,服務器就可以在本來無狀態的HTTP連接中識別出這是哪個客戶發出的請求,然後繪製相關頁面。

這中Session機制使用簡單方便,被使用了很長時間。但是一旦做成集羣,這種方式就不靈了。以NGINX默認的輪詢方式爲例,用戶在A服務器上登陸成功,SessionID和用戶名等相關信息寫入了A服務器的內存中,該用戶第二次請求時被NGINX分發到了B服務器,而B服務器沒用該用戶的SessionID和用戶名等相關信息,於是要求用戶再登陸一遍。用戶第二次登陸之後發送第三次請求,被NGINX分配到了A或者C服務器,於是用戶又必須登陸一遍,總之這個用戶一直沒法登陸成功。

所以使用NGINX默認的輪詢(round-robin)方式是沒法做到會話保持的,如果你硬要再這種情況下做會話保持,那麼就不能使用Servlet中HttpSession這個方案了。一般有如下兩種方式可以選擇

1、數據庫存儲Session。

與Servlet Session不同的是,現在SessionID和用戶信息不放到服務器內存中,而放到數據庫中讓所有節點都可以訪問。setAttribute()變成iSQL nsert語句,getAttribute()變成SQL select語句,主鍵就是jsessionid這個cookie的值,這樣就實現了Session的共享。但是一般不推薦這樣做,因爲這樣會給數據庫帶來大量的讀寫請求,應用服務器是負載均衡了,可是數據庫這樣搞就炸了,所以避免這樣搞。

2、Redis,Memcached存儲Session

這個的原理和數據庫存儲是一樣的,因爲你完全可以把Redis和Memcached看成一種數據庫,只不過由於Redis和Memcached是把數據存放到內存中,一般不做持久化,所以IO速度要快於普通數據庫,並且Redis比較容易做集羣,可以防止單點故障。是目前比較流行的一種方法。同上面一樣,setAttribute()變成了put語句,getAttribute()變成了get語句。另外Redis對於JAVA支持比較好,更推薦使用Redis,Memcached在PHP中有官方的支持,比JAVA中好用。

上面的方式雖然解決了集羣的問題,但是如果要從F5遷移到NGINX,那麼成本是很高的,因爲要重寫相關的代碼,還要搭建Redis服務器等等,對於一開始沒有考慮到集羣的項目是很難做的。

那麼NGINX官方對於這種情況的解決方案是什麼呢?請看下面

Session persistence
Please note that with round-robin or least-connected load balancing, each subsequent client’s request can be potentially distributed to a different server. There is no guarantee that the same client will be always directed to the same server. 
If there is the need to tie a client to a particular application server — in other words, make the client’s session “sticky” or “persistent” in terms of always trying to select a particular server — the ip-hash load balancing mechanism can be used. 
With ip-hash, the client’s IP address is used as a hashing key to determine what server in a server group should be selected for the client’s requests. This method ensures that the requests from the same client will always be directed to the same server except when this server is unavailable. 
To configure ip-hash load balancing, just add the ip_hash directive to the server (upstream) group configuration: 
upstream myapp1 {
    ip_hash;
    server srv1.example.com;
    server srv2.example.com;
    server srv3.example.com;
}
以上出自:http://nginx.org/en/docs/http/load_balancing.html
方法很簡單,就是NGINX根據請求源的IP,固定的分配到某個地址上去,這樣保證同一個IP多次分配都是同一臺服務器,這樣就不用考慮共享Session的問題了。可是這種解決方案是一種非常不負責任的方案。首先這樣做必須確保NGINX是放在公網上的,且NGINX前面不能有其他的代理服務器,這樣才能保證NGINX能夠獲得用戶真實的IP。但是如果NGINX前面還有squid這種代理或者前面還有一個NGINX的話,那麼當前NGINX收到的就是上一級代理的IP,所有IP都一個樣,所以最後只有1臺後端服務器被利用,根本沒有做到負載均衡的要求。

即使退一步,NGINX確實是放在公網上的第一級代理,那麼根據我國目前的網絡狀況,有很多學校,公司企業他們公網出口就一個IP。也就是近千人共用一個IP訪問公網的情況非常普遍。而這正好又是一個學生選課系統或者辦公系統的話,那麼NGINX還是沒有起到負載均衡的作用,頂多能發揮高可用的功能。

那麼對於這種實際生產中碰到的問題,用NGINX應該如何解決呢。我們首先可以看一下F5是如何解決這樣問題的。、

F5支持什麼樣的會話保持方法?
    F5 Big-IP支持多種的會話保持方法,其中包括:簡單會話保持(源地址會話保持)、HTTP Header的會話保持,基於SSL Session ID的會話保持,i-Rules會話保持以及基於HTTP Cookie的會話保持,此外還有基於SIP ID以及Cache設備的會話保持等,但常用的是簡單會話保持,HTTP Header的會話保持以及 HTTP Cookie會話保持以及基於i-Rules的會話保持。

2.1 簡單會話保持
    簡單會話保持也被稱爲基於源地址的會話保持,是指負載均衡器在作負載均衡時是根據訪問請求的源地址作爲判斷關連會話的依據。對來自同一IP地址的所有訪問 請求在作負載均時都會被保持到一臺服務器上去。在BIG-IP設備上可以爲“同一IP地址”通過網絡掩碼進行區分,比如可以通過對IP地址 192.168.1.1進行255.255.255.0的網絡掩碼,這樣只要是來自於192.168.1.0/24這個網段的流量BIGIP都可以認爲他 們是來自於同一個用戶,這樣就將把來自於192.168.1.0/24網段的流量會話保持到特定的一臺服務器上。


    簡單會話保持裏另外一個很重要的參數就是連接超時值,BIGIP會爲每一個進行會話保持的會話設定一個時間值,當一個會話上一次完成到這個會話下次再來之 前的間隔如果小於這個超時值,BIGIP將會將新的連接進行會話保持,但如果這個間隔大於該超時值,BIGIP將會將新來的連接認爲是新的會話然後進行負 載平衡。


    基於原地址的會話保持實現起來簡單,只需要根據數據包三、四層的信息就可以實現,效率也比較高。存在的問題就在於當多個客戶是通過代理或地址轉換的方式來 訪問服務器時,由於都分配到同一臺服務器上,會導致服務器之間的負載嚴重失衡。另外一種情況上客戶機數量很少,但每個客戶機都會產生多個併發訪問,對這些 併發訪問也要求通過負載均衡器分配到多個服器上,這時基於客戶端源地址的會話保持方法也會導致負載均衡失效。


2.2 基於Cookie的會話保持
2.2.1 Cookie插入模式:
    在Cookie插入模式下,Big-IP將負責插入cookie,後端服務器無需作出任何修改


    當客戶進行第一次請求時,客戶HTTP請求(不帶cookie)進入BIG-IP, BIG-IP根據負載平衡算法策略選擇後端一臺服務器,並將請求發送至該服務器,後端服務器進行HTTP回覆(不帶cookie)被髮回BIGIP,然後 BIG-IP插入cookie,將HTTP回覆返回到客戶端。當客戶請求再次發生時,客戶HTTP請求(帶有上次BIGIP插入的cookie)進入 BIGIP,然後BIGIP讀出cookie裏的會話保持數值,將HTTP請求(帶有與上面同樣的cookie)發到指定的服務器,然後後端服務器進行請 求回覆,由於服務器並不寫入cookie,HTTP回覆將不帶有cookie,恢復流量再次經過進入BIG-IP時,BIG-IP再次寫入更新後的會話保 持 cookie。

2.2.2 Cookie 重寫模式
    當客戶進行第一次請求時,客戶HTTP請求(不帶cookie)進入BIGIP, BIGIP根據負載均衡算法策略選擇後端一臺服務器,並將請求發送至該服務器,後端服務器進行HTTP回覆一個空白的cookie併發回BIGIP,然後 BIGIP重新在cookie裏寫入會話保持數值,將HTTP回覆返回到客戶端。當客戶請求再次發生時,客戶HTTP請求(帶有上次BIGIP重寫的 cookie)進入BIGIP,然後BIGIP讀出cookie裏的會話保持數值,將HTTP請求(帶有與上面同樣的cookie)發到指定的服務器,然 後後端服務器進行請求回覆,HTTP回覆裏又將帶有空的cookie,恢復流量再次經過進入BIGIP時,BIGIP再次寫入更新後會話保持數值到該 cookie。

2.2.3 Passive Cookie 模式,服務器使用特定信息來設置cookie。
    當客戶進行第一次請求時,客戶HTTP請求(不帶cookie)進入BIGIP, BIGIP根據負載平衡算法策略選擇後端一臺服務器,並將請求發送至該服務器,後端服務器進行HTTP回覆一個cookie併發回BIGIP,然後 BIGIP將帶有服務器寫的cookie值的HTTP回覆返回到客戶端。當客戶請求再次發生時,客戶HTTP請求(帶有上次服務器寫的cookie)進入 BIGIP,然後BIGIP根據cookie裏的會話保持數值,將HTTP請求(帶有與上面同樣的cookie)發到指定的服務器,然後後端服務器進行請 求回覆,HTTP回覆裏又將帶有更新的會話保持cookie,恢復流量再次經過進入BIGIP時,BIGIP將帶有該cookie的請求回覆給客戶端。

2.2.4 Cookie Hash模式:
    當客戶進行第一次請求時,客戶HTTP請求(不帶cookie)進入BIGIP, BIGIP根據負載均衡算法策略選擇後端一臺服務器,並將請求發送至該服務器,後端服務器進行HTTP回覆一個cookie併發回BIGIP,然後 BIGIP將帶有服務器寫的cookie值的HTTP回覆返回到客戶端。當客戶請求再次發生時,客戶HTTP請求(帶有上次服務器寫的cookie)進入 BIGIP,然後BIGIP根據cookie裏的一定的某個字節的字節數來決定後臺服務器接受請求,將HTTP請求(帶有與上面同樣的cookie)發到 指定的服務器,然後後端服務器進行請求回覆,HTTP回覆裏又將帶有更新後的cookie,恢復流量再次經過進入BIGIP時,BIGIP將帶有該 cookie的請求回覆給客戶端。

2.3 SSL Session ID會話保持
    在用戶的SSL訪問系統的環境裏,當SSL對話首次建立時,用戶與服務器進行首次信息交換以:1}交換安全證書,2)商議加密和壓縮方法,3)爲每條對話 建立Session ID。由於該Session ID在系統中是一個唯一數值,由此,BIGIP可以應用該數值來進行會話保持。當用戶想與該服務器再次建立連接時,BIGIP可以通過會話中的 SSL Session ID識別該用戶並進行會話保持。


    基於SSL Session ID的會話保持就需要客戶瀏覽器在進行會話的過程中始終保持其SSL Session ID不變,但實際上,微軟Internet Explorer被發現在經過特定一段時間後將主動改變SSL Session ID,這就使基於SSL Session ID的會話保持實際應用範圍大大縮小。


從F5的會話保持方法中獲得啓發,2.2.1的Cookie插入模式是使用最普遍也最簡單的一種方式。如果NGINX也能實現相同的功能,爲每個用戶插入一個Cookie,再根據這個Cookie選擇相應的負載均衡服務器的話,哪怕再多人公用一個IP也不害怕了。

可惜,NGINX官方並沒有給出這樣一個功能。那麼目前有下面三種方案:

1、使用第三方模塊nginx-sticky-module-ng

在這裏你可以看到相關的說明:https://bitbucket.org/nginx-goodies/nginx-sticky-module-ng/overview

由於是第三方模塊,所以需要重新編譯你的NGINX,首先下載nginx-sticky-module-ng的源碼,下載地址:https://bitbucket.org/nginx-goodies/nginx-sticky-module-ng/downloads/

然後解壓出源碼,使用--add-module 加源碼路徑添加這個模塊,如下

./configure ... --add-module=/absolute/path/to/nginx-sticky-module-ng
make
make install

然後在你nginx配置文件的upstream片段中,去掉ip_hash等策略,加上sticky,如下

upstream {
  sticky;
  server 127.0.0.1:9000;
  server 127.0.0.1:9001;
  server 127.0.0.1:9002;
}

其中sticky後面還可以跟參數,比如

sticky expires=1h domain=toxingwang.com path=/; 

參數的意義如下
sticky [name=route] [domain=.foo.bar] [path=/] [expires=1h] [hash=index|md5|sha1] [no_fallback];  
name: 可以爲任何的string字符,默認是route  
domain:哪些域名下可以使用這個cookie  
path:哪些路徑對啓用sticky,例如path/test,那麼只有test這個目錄纔會使用sticky做負載均衡  
expires:cookie過期時間,默認瀏覽器關閉就過期,也就是會話方式。  
no_fallbackup:如果設置了這個,cookie對應的服務器宕機了,那麼將會返回502(bad gateway 或者 proxy error),建議不啓用
配置好之後,當upstream模塊中有一個一上的server時,就可以在chrome中看到nginx添加的這個cookie(如果upstream裏只有一個服務器是不行的)

2、使用第三方NGINX——tengine

tengine是騰訊在nginx基礎上開發的一個nginx分支,開發很活躍,更新頻繁,可以作爲替代NGINX的很好的方案,目前最新版本2.2.1。sticky模塊的說明地址是:

http://tengine.taobao.org/document_cn/http_upstream_session_sticky_cn.html

用法和NGINX有一些區別,如下

# 默認配置:cookie=route mode=insert fallback=on
upstream foo {
server 192.168.0.1;
server 192.168.0.2;
session_sticky;
}
server {
location / {
proxy_pass http://foo;
}
}

在tengine中,sticky變成了session_sticky,並且後面的參數也有了一些變化,具體請看上面的鏈接。

3、使用cookie的HASH來區分同一個用戶的不同鏈接。

上面兩個方案都用到了第三方的模塊,那麼如果不想用第三方模塊可不可以實現呢,也是可以的,但是隻有對登陸用戶有效。

前面我們已經知道了如果使用Servlet Session的話,Web容器會自動的在用戶瀏覽器上建立名爲jsessionid的cookie,並且值就是服務器端的SessionID。另一方面,新版的NGINX不光可以通過IP的hash來分發流量,也可以通過url的hash,cookie的hash,header的hash等等進行鏈接的固定分配。由於用戶登陸成功以後名爲jsessionid的cookie就有了一個短期固定的值,而且每個用戶都不一樣,那麼我們就可以根據這個sessionid的hash值爲它分配一個服務器。在當前sessionID起作用的時候那麼分配的服務器也是同一個,並且不需要安裝第三方的插件,方法如下

upstream backend {
    ...
    hash        $cookie_jsessionid;

}


在NGINX 1.7.2版本之前,這也是一個第三方模塊,名爲nginx_upstream_hash,它目前的源碼地址:https://github.com/evanmiller/nginx_upstream_hash

在NGINX 1.7.2及之後,這成爲了一個內置功能,而且默認就被編譯進去了,所以我們可以很方便的使用它了,官方說明地址:http://nginx.org/en/docs/http/ngx_http_upstream_module.html#hash,我把它摘出來放在下面

Syntax:    hash key [consistent];
Default:    —
Context:    upstream
This directive appeared in version 1.7.2.

Specifies a load balancing method for a server group where the client-server mapping is based on the hashed key value. The key can contain text, variables, and their combinations. Note that adding or removing a server from the group may result in remapping most of the keys to different servers. The method is compatible with the Cache::Memcached Perl library.

If the consistent parameter is specified the ketama consistent hashing method will be used instead. The method ensures that only a few keys will be remapped to different servers when a server is added to or removed from the group. This helps to achieve a higher cache hit ratio for caching servers. The method is compatible with the Cache::Memcached::Fast Perl library with the ketama_points parameter set to 160.

4、使用Http Header區分不同用戶。

上面的方法中,不論F5還是NGINX的sticky模塊都是根據cookie區分用戶的。但是有些情況下無法使用cookie,比如客戶端瀏覽器禁用的cookie,Android,IOS等移動端調用的HTTP API接口等。現在比較常用的做法是把SessionID,有的也喜歡叫做token,放在請求的URL或者請求參數中,比如下面這樣

http://example.com/user.jsp?token=nv3e0n382nv83sk2

那麼沒有cookie如何區分用戶呢,這種情況下雖然不能使用cookie,但是header是可以使用的,我們可以把token或者sessionID放到header中,然後對該header的值進行hash,並固定分配一個服務器。配置文件的寫法如下

hash $http_你設置的header名稱;

大多數的反向代理軟件都會把它收到的請求源的IP記錄在x_forwarded_for這個header中,所以一個客戶總是擁有一個唯一的x_forwarded_for頭不會變化,所以我們也可以對這個Header進行hash,效果就是根據IP地址進行分流是一樣的。
hash $http_x_forwarded_for;


如果你的上級也是NGINX,那麼應該按照如下配置
location / {
proxy_pass http://localhost:8000;
 
proxy_set_header X-Real-IP $remote_addr;
# needed for HTTPS
# proxy_set_header X_FORWARDED_PROTO https;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_redirect off;
}


這種通過header的hash來分配服務器是Caddy官方主推的,在caddy中配置如下
proxy / web1.local:80 web2.local:90 web3.local:100
{
    policy header X-My-Header
}

官網文檔地址:
https://caddyserver.com/docs/proxy

參考文章:http://jingpin.jikexueyuan.com/article/57613.html
--------------------- 
作者:lvshaorong 
來源:CSDN 
原文:https://blog.csdn.net/lvshaorong/article/details/78309514 
版權聲明:本文爲博主原創文章,轉載請附上博文鏈接!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章