1. 程式人生 > >haproxy實現會話保持 haproxy實現會話保持(2):stick table

haproxy實現會話保持 haproxy實現會話保持(2):stick table

HAProxy系列文章:http://www.cnblogs.com/f-ck-need-u/p/7576137.html


1.反向代理為什麼需要設定cookie

任何一個七層的http負載均衡器,都應該具備一個功能:會話保持。會話保持是保證客戶端對動態應用程式正確請求的基本要求。

還是那個被舉爛了卻最有說服力的例子:客戶端A向服務端B請求將C商品加入它的賬戶購物車,加入成功後,服務端B會在某個快取區域中記錄下客戶端A和它的商品C,這個快取的內容就是session上下文環境。而識別客戶端的方式一般是設定session ID(如PHPSESSID、JSESSIONID),並將其作為cookie的內容交給客戶端。客戶端A再次請求的時候(比如將購物車中的商品下訂單)只要攜帶這個cookie,服務端B就可以從中獲取到session ID並找到屬於客戶端A的快取內容(商品C),也就可以繼續執行下訂單部分的程式碼。

假如這時使用負載均衡軟體對客戶端的請求進行負載,就必須要保證能將客戶端A的請求再次引導到服務端B,而不能引導到服務端X、服務端Y,因為X、Y上並沒有快取和客戶端A對應的session內容,也就無法為客戶端A下訂單。

因此,反向代理軟體必須具備將客戶端和服務端"繫結"的功能,也就是所謂的提供會話保持,讓客戶端A後續的請求一定轉發到服務端B上。

這裡討論的物件是http的動態應用請求,它要求會話保持。更通用地,只要負載均衡軟體負載的不是"無狀態"的協議或服務,就應該提供會話保持能力,除非它是四層負載軟體。

haproxy提供了3種實現會話保持的方式:

  • (1).源地址hash;
  • (2).設定cookie;
  • (3).會話粘性表stick-table;

本文只討論haproxy在設定cookie上實現會話保持的方式,stick-table會話粘性的方式則在下一篇文章中單獨討論。而源地址hash是一種負載排程演算法,沒什麼可討論的,而且除非實在沒辦法,不建議使用這種排程演算法。

2.haproxy設定cookie的幾種方式

設定cookie的方式是通過在配置檔案中使用cookie指令進行配置的。由於haproxy設定cookie的目的是為了將某客戶端引導到之前為其服務過的後端伺服器上,簡單地說,就是和後端某伺服器保持聯絡,因此cookie指令不能設定在frontend段落。

首先看一個設定cookie的示例。

backend dynamic_servers
    cookie app_cook  insert  nocache
    server app1      192.168.100.22:80 cookie server1 server app2 192.168.100.23:80 cookie server2 

這個示例配置中,cookie指令中指定的是insert命令,表示在將響應報文交給客戶端之前,先插入一個屬性名為"app_cook"的cookie,這個cookie在響應報文的頭部將獨佔一個"Set-Cookie"欄位(因為是插入新cookie),而"app_cook"只是cookie名稱,它的值是由server指令中的cookie選項指定的,這裡是"server1"或"server2"。

因此,如果這個請求報文分配給後端app2時,響應給客戶端的響應報文中haproxy設定的"Set-Cookie"欄位的樣式為:

Set-Cookie:app_cook=server2; path=/

除了insert命令,cookie指令中還支援rewrite和prefix兩種設定cookie的方式,這三種cookie的操作方式只能三選一。此外,還提供一些額外對cookie的功能設定。

首先看看指令的語法:

cookie <name> [ rewrite | insert | prefix ] [ indirect ] [ nocache ]
              [ postonly ] [ preserve ] [ httponly ] [ secure ] [ domain <domain> ]* [ maxidle <idle> ] [ maxlife <life> ] 

本文詳細分節討論rewrite、insert、prefix的行為,並在討論它們的時候會穿插說明indirect、nocache和preserve的行為,如果需要了解其他選項,請自翻官方手冊。

下圖是後文實驗時使用的環境:

其中在後端提供的index.php內容大致如下,主要部分是設定了名為PHPSESSID的cookie

<h1>response from webapp 192.168.100.61</h1>
<?php session_start(); echo "Server IP: "."<font color=red>".$_SERVER['SERVER_ADDR']."</font>"."<br>"; echo "Server Name: "."<font color=red>".$_SERVER['SERVER_NAME']."</font>"."<br>"; echo "SESSIONNAME: "."<font color=red>".session_name()."</font>"."<br>"; echo "SESSIONID: "."<font color=red>".session_id()."</font>"."<br>"; ?> 

2.1 cookie insert

insert    This keyword indicates that the persistence cookie will have to
          be inserted by haproxy in server responses if the client did not already have a cookie that would have permitted it to access this server. When used without the "preserve" option, if the server emits a cookie with the same name, it will be remove before processing. For this reason, this mode can be used to upgrade existing configurations running in the "rewrite" mode. The cookie will only be a session cookie and will not be stored on the client's disk. By default, unless the "indirect" option is added, the server will see the cookies emitted by the client. Due to caching effects, it is generally wise to add the "nocache" or "postonly" keywords (see below). The "insert" keyword is not compatible with "rewrite" and "prefix". 

其中大致說明了以下幾個意思:

  1. 該關鍵詞表示,haproxy將在客戶端沒有cookie時(比如第一次請求),在響應報文中插入一個cookie。
  2. 當沒有使用關鍵詞"preserve"選項時,如果後端伺服器設定了一個和此處名稱相同的cookie,則首先刪除服務端設定的cookie。
  3. 該cookie只能作為會話保持使用,無法持久化到客戶端的磁碟上(因為haproxy設定的cookie沒有maxAge屬性,無法持久儲存,只能儲存在瀏覽器快取中)。
  4. 預設情況下,除非使用了"indirect"選項,否則服務端可以看到客戶端請求時的所有cookie資訊。
  5. 由於快取的影響,建議加上"nocache"或"postonly"選項。

下面使用例子來解釋insert的各種行為。

在haproxy如下配置後端。

backend dynamic_group
    cookie app_cook insert nocache
    server app1 192.168.100.60:80 cookie app_server1 server app2 192.168.100.61:80 cookie app_server2 

當使用瀏覽器第一次訪問http://192.168.100.59/index.php時,響應結果和響應首部內容如下圖:

從圖中可以知道,這次瀏覽器的請求分配給了app2,而且響應首部中有兩個"Set-Cookie"欄位,其中帶有PHPSESSID的cookie是app2伺服器自身設定的,另一個是haproxy設定的,其名和其值為"app_cook=app_server2"。

如果客戶端再次訪問(不關閉瀏覽器,cookie快取還在),請求頭中將攜帶該cookie,haproxy發現了該cookie中"app_cook=app_server2"部分,知道這個請求要交給app_server2這個後端。如下圖:

這樣就實現了會話保持,保證被處理過的客戶端能被分配到同一個後端應用伺服器上。

注意,客戶端在第一次收到響應後就會把cookie快取下來,以後每次http://192.168.100.59/index.php(根據域名進行判斷)都會從快取中取出該cookie放進請求首部。這樣haproxy一定會將其分配給app_server2,除非app_server2下線了。但即使如此,客戶端還是會攜帶該cookie,只不過haproxy判斷app_server2下線後,就為客戶端重新分配app_server1,並設定"app_cook=app_server1",該cookie會替換客戶端中的"app_cook=app_server2"。下圖是app2下線後分配給app1的結果:

但注意,即使分配給了app1,PHPSESSID也不會改變(即app1設定的PHPSESSID無效),因為haproxy判斷出這個重名cookie,會刪除app1設定的PHPSESSID。因此上圖中的PHPSESSID值和之前分配給app2時的PHPSESSID是一樣的。

這樣一來,app1不是就無法處理該客戶端的請求了嗎?確實如此,但沒辦法,除非後端設定了session共享。

如果將配置檔案中的cookie名稱也設定為PHPSESSID,即後端應用伺服器和此處設定的cookie名稱相同,那麼haproxy將首先將後端的PHPSESSID刪除,然後使用自己的值傳送給客戶端。也就是說,此時將只有一個"Set-Cookie"欄位響應給客戶端。

backend dynamic_group
    cookie PHPSESSID insert nocache
    server app1 192.168.100.60:80 cookie app_server1 server app2 192.168.100.61:80 cookie app_server2 

因此,在cookie指令中絕對不能設定cookie名稱和後端的cookie名稱相同,否則後端就相當於"盲人"。例如此處的PHPSESSID,此時後端雖然認識PHPSESSID是自己傳送出去的cookie名稱,但是無法獲取ID為"app_server1"的session上下文。

如果不配合"indirect"選項,服務端可以看到客戶端請求時的所有cookie資訊。如果配合"indirect"選項,則haproxy在將請求轉發給後端時,將刪除自己設定的cookie,使得後端只能看到它自己的cookie,這樣對後端來說,整個過程是完全透明的,它不知道前面有負載均衡軟體。

重新修改haproxy的cookie指令,並修改nginx配置檔案中日誌格式,在其中加上"$http_cookie"變數,它表示請求報文中的cookie資訊。

# haproxy
cookie app_cook insert nocache

# nginx
log_format  main  '$http_cookie $remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for"'; 

客戶端再次訪問時,nginx的日誌中將記錄以下資訊(只貼出了前幾個欄位)。

PHPSESSID=47d0ina2m14gg67ovdf1d972d1; app_cook=app_server1 192.168.100.59 

加上"indirect"選項,再測試。

cookie app_cook insert indirect nocache

結果如下:

PHPSESSID=bge3bh6sksu2ie91lsp8ep9oi2 192.168.100.59

如果insert關鍵字配合"preserve"關鍵字,那麼當後端設定了cookie時,haproxy將強制保留該cookie,不做任何修改。也就是說,如果將haproxy的cookie名稱也設定為PHPSESSID,那麼客戶端第一次請求時收到的響應報文中將只有一個"Set-Cookie"欄位,且這個欄位的值是後端伺服器設定的,和haproxy無關。

當客戶端和HAProxy之間存在快取時,建議將insert配合nocache一起使用,因為nocache確保如果需要插入cookie,則可快取頁面將被標記為不可快取。這一點很重要,因為如果所有cookie都新增到可快取的頁面上,則所有客戶都將從中間的快取層(如cdn端的快取層)獲取頁面,並且將共享同一個Cookie,從而導致某臺後端伺服器接收的流量遠遠超過其他後端伺服器。

2.2 cookie prefix

prefix    This keyword indicates that instead of relying on a dedicated
          cookie for the persistence, an existing one will be completed.
          This may be needed in some specific environments where the client
          does not support more than one single cookie and the application
          already needs it. In this case, whenever the server sets a cookie
          named <name>, it will be prefixed with the server's identifier and a delimiter. The prefix will be removed from all client requests so that the server still finds the cookie it emitted. Since all requests and responses are subject to being modified, this mode doesn't work with tunnel mode. The "prefix" keyword is not compatible with "rewrite" and "insert". Note: it is highly recommended not to use "indirect" with "prefix", otherwise server cookie updates would not be sent to clients. 

大致意思是:haproxy將在已存在的cookie(例如後端應用伺服器設定的)上新增字首cookie值,這個字首部分是server指令中的cookie設定的,代表的是服務端識別符號。在客戶端再次訪問時,haproxy將會自動移除這部分字首,使得服務端只能看到它自己發出的cookie。在一些特殊環境下,客戶端不支援多個"Set-Cookie"欄位,這時可以使用prefix。

使用prefix的時候,cookie指令設定的cookie名必須和後端設定的cookie一樣(在本文的環境中是PHPSESSID),否則prefix模式下的haproxy不會對響應報文做任何改變。

backend dynamic_group
    cookie PHPSESSID prefix
    server app1 192.168.100.60:80 cookie app_server1
    server app2 192.168.100.61:80 cookie app_server2

如下圖:

從後端nginx上的日誌上檢視haproxy轉發過來的請求,可以看到字首已經被haproxy去掉了。

PHPSESSID=oses71hjr64dl6lputpkmdpg12 192.168.100.59 - -

2.3 cookie rewrite

rewrite   This keyword indicates that the cookie will be provided by the
          server and that haproxy will have to modify its value to set the
          server's identifier in it. This mode is handy when the management
          of complex combinations of "Set-cookie" and "Cache-control"
          headers is left to the application. The application can then
          decide whether or not it is appropriate to emit a persistence
          cookie. Since all responses should be monitored, this mode
          doesn't work in HTTP tunnel mode. Unless the application
          behaviour is very complex and/or broken, it is advised not to start with this mode for new deployments. This keyword is incompatible with "insert" and "prefix". 

當後端伺服器設定了cookie時,使用rewrite模式時,haproxy將重寫該cookie的為後端伺服器的識別符號。當應用程式需要同時考慮"Set-Cookie"和"Cache-control"欄位時,該模式非常方便,因為應用程式可以決定是否應該設定一個為了保持會話的cookie。除非後端應用程式的環境非常複雜,否則不建議使用該模式。

同樣,rewrite模式下的haproxy設定的cookie必須和後端伺服器設定的cookie名稱一致,否則不會做任何改變。

backend dynamic_group
    cookie PHPSESSID rewrite
    server app1 192.168.100.60:80 cookie app_server1
    server app2 192.168.100.61:80 cookie app_server2

結果如下圖:

但是,當客戶端持著"PHPSESSID=app_server1"再去請求伺服器時,haproxy將其分配給app1,app1此時收到的cookie將是重寫後的,但是app1根本就不認識這個cookie,後面的程式碼可能因此而失去邏輯無法進行正確處理。

3.haproxy如何使用cookie實現會話保持以及如何忽略會話保持

在haproxy中,haproxy會監控、修改、增加cookie,這都是通過記憶體中的cookie表實現的。

cookie表中記錄了它自己增、改的cookie記錄,包括cookie名和對應server的cookie值,通過這個cookie記錄,haproxy就能知道請求該交給哪個後端。

例如,當haproxy插入一個cookie的時候。即在haproxy配置如下後端。

backend dynamic_group
    cookie app_cook insert nocache
    server app1 192.168.100.60:80 cookie app_server1 server app2 192.168.100.61:80 cookie app_server2 

那麼,從客戶端第一次請求到第二次請求被處理的整個過程,大致如下:

當haproxy成功修改了響應報文中的cookie時,將在cookie表中插入一條記錄,這條記錄是維持會話的依據。

其實,通過cookie表保持和後端的會話只是預設情況,haproxy允許"即使使用了cookie也不進行會話繫結"的功能。這可以通過ignore-persist指令來實現。當滿足該指令的要求時,表示不將該cookie插入到cookie表中,因此無法實現會話保持,即使haproxy設定了cookie也沒用。

例如,在backend中指定如下配置:

backend dynamic_group
    acl  url_dynamic   path_end  -i .php
    ignore-persist if  url_dynamic
    cookie app_cook insert nocache
    server app1 192.168.100.60:80 cookie app_server1 server app2 192.168.100.61:80 cookie app_server2 

這表示當請求uri以".php"結尾時,將忽略會話保持功能。這表示,對於php結尾的請求,app_cook這個cookie從頭到尾都是擺設。

當然,上面的設定是不合理的,更合理的應該是這樣的。

acl url_static  path_beg         /static /images /img /css
acl url_static  path_end         .gif .png .jpg .css .js
ignore-persist  if url_static

ignore-persist相對的是force-persist,但不建議使用該選項,因為它和option redispatch衝突。

haproxy實現會話保持(2):stick table

  分類:  網站架構  

HAProxy系列文章:http://www.cnblogs.com/f-ck-need-u/p/7576137.html


在上一篇文章中,分析了haproxy如何通過cookie實現會話保持,本文討論haproxy另一種實現會話保持的方式:stick table。

1.stickiness和stick table簡介

stick table是haproxy的一個非常優秀的特性,這個表裡面儲存的是stickiness記錄,stickiness記錄了客戶端和服務端1:1對應的引用關係。通過這個關係,haproxy可以將客戶端的請求引導到之前為它服務過的後端伺服器上,也就是實現了會話保持的功能。這種記錄方式,俗稱會話粘性(stickiness),即將客戶端和服務端粘連起來。

stick table中使用key/value的方式對映客戶端和後端伺服器,key是客戶端的識別符號,可以使用客戶端的源ip(50位元組)、cookie以及從報文中過濾出來的部分String。value部分是服務端的識別符號。

stick table實現會話粘性的過程如下圖:

除了儲存key/value實現最基本的粘性,stick table還可以額外儲存每個stickiness記錄對應的狀態統計資料。比如stickiness記錄1目前建立了多少和客戶端的連線、平均建立連線的速度是多少、流入流出了多少位元組的資料、建立會話的數量等等。

stick table可以在"雙主模型"下進行復制(replication)。只要設定好對端haproxy節點,haproxy就會自動將新插入的、剛更新的記錄通過TCP連線推送到對端節點上。這樣一來,粘性記錄不會丟失,即使某haproxy節點出現了故障,其他節點也能將客戶端按照粘性對映關係引導到正確的後端伺服器上。而且每條stickiness記錄佔用空間都很小(平均最小50位元組,最大166位元組,由是否記錄額外統計資料以及記錄多少來決定佔用空間大小),使得即使在非常繁忙的環境下在幾十個節點之間推送都不會出現壓力瓶頸和網路阻塞(可以按節點數量、stickiness記錄的大小和平均併發量來計算每秒在網路間推送的資料流量)。

此外,stick table還可以在haproxy重啟時,在新舊兩個程序間進行復制,這是本地複製。當haproxy重啟時,舊haproxy程序會和新haproxy程序建立TCP連線,將其維護的stick table推送給新程序。這樣新程序不會丟失粘性資訊,和其他節點也能最大程度地保持同步,使得其他節點只需要推送該節點重啟過程中新增加的stickiness記錄就能完全保持同步。

2.使用stick table

下圖是本文測試時的環境:

2.1 建立stick table

首先看建立stick table的語法:

stick-table type {ip | integer | string [len <length>] | binary [len <length>]}
            size <size> [expire <expire>] [nopurge] [peers <peersect>] [store <data_type>]* 

其中

  • type ip | integer | string:使用什麼型別的key作為客戶端識別符號。可以是客戶端的源IP,可以是一個整數ID值,也可以是一段從請求報文或響應報文中匹配出來的字串。
  • size:表中允許的最大stickiness記錄數量。單位使用k、m和g表示,分別表示1024、2^20和2^30條記錄。
  • expire:stickiness記錄的過期時長。當某記錄被操作後,過了一段時間就會過期,過期的記錄會自動從stick table中移除,釋放表空間。
  • nopurge:預設情況下,當表滿後,如果還有新的stickiness記錄要插入進來,haproxy會自動將一部分老舊的stickiness記錄flush掉,以釋放空間儲存新紀錄。指定nopurge後,將不進行flush,只能通過記錄過期來釋放表空間,因此該選項必須配合expire選項同時使用。
  • peers:指定要將stick table中的記錄replication到對端haproxy節點。
  • store:指定要儲存在stick table中的額外狀態統計資料。其中代表後端伺服器的識別符號server ID(即key/value的value部分)會自動插入,無需顯式指定。

注意,每個後端組只能建立一張stick table,每個stick table的id或名稱等於後端組名。例如在backend static_group後端建立stick table,則該表的id為"static_group"。也有特殊方法建立多張,但無必要,可翻官方手冊找方法。

例如,建立一個以源IP地址為key的stick table,該表允許100W條記錄,5分鐘的記錄過期時長,並且不記錄任何額外資料。

stick-table type ip size 1m expire 5m

這張表由於沒有記錄額外的統計資料,每條stickiness記錄在記憶體中只佔用50位元組左右的空間,表滿後整張表在記憶體中佔用50MB(2^20*50/1024/1024=50MB)。看上去很大,但檢索速度是極快的,完全不用擔心效能問題。

如果還要儲存和客戶端建立的連線數量計數器(conn_cnt),則:

stick-table type ip size 1m expire 5m store conn_cnt

conn_cnt佔用32個bit位,即4位元組,因此每條stickiness記錄佔用54位元組,100W條記錄佔用54M記憶體空間。

2.2 檢視stick table

haproxy沒有直接的介面可以顯示stick table的相關資訊,只能通過stats socket進行檢視。該指令表示開啟一個本地unix套接字監聽haproxy的資訊,通過這個套接字可以檢視haproxy的很多資訊,且能動態調整haproxy配置。

首先在haproxy的配置檔案中開啟"stats socket"狀態資訊,如下:

global
    stats socket /var/run/haproxy.sock mode 600 level admin
    stats timeout 2m

預設stats timeout的過期時長為10s,建議設定長一點。上面還設定了socket的許可權級別,表示能訪問(600)這個套接字的人具有所有許可權(admin)。level還有兩種許可權級別更低一點的值"read"和"operator"(預設),前者表示只有讀取資訊的許可權,不能設定或刪除、清空某些資訊,後者表示具備讀和某些設定許可權。

本地套接字監聽haproxy後,可以通過"socat"工具(socket cat,很強大的工具,在epel源中提供)從套接字來操作haproxy。

# 方式一:直接傳遞要執行的操作給套接字
echo "help" | socat unix:/var/run/haproxy.sock -

# 方式二:進入互動式模式,然後在互動式模式下執行相關操作 socat readline unix:/var/run/haproxy.sock 

如果要監控某些狀態資訊的實時變化,可以使用watch命令。

watch -n 1 '"echo show table" | socat unix:/var/run/haproxy.sock -'

haproxy支援以下列出的所有操作命令:

[root@xuexi ~]# echo "help" | socat unix:/var/run/haproxy.sock -
  help           : this message
  prompt         : toggle interactive mode with prompt
  quit           : disconnect
  show tls-keys [id|*]: show tls keys references or dump tls ticket keys when id specified set ssl tls-key [id|keyfile] <tlskey>: set the next TLS key for the <id> or <keyfile> listener to <tlskey> set maxconn global : change the per-process maxconn setting set rate-limit : change a rate limiting value set timeout : change a timeout setting show env [var] : dump environment variables known to the process show resolvers [id]: dumps counters from all resolvers section and associated name servers add acl : add acl entry clear acl <id> : clear the content of this acl del acl : delete acl entry get acl : report the patterns matching a sample for an ACL show acl [id] : report available acls or dump an acl's contents add map : add map entry clear map <id> : clear the content of this map del map : delete map entry get map : report the keys and values matching a sample for a map set map : modify map entry show map [id] : report available maps or dump a map's contents show pools : report information about the memory pools usage show sess [id] : report the list of current sessions or dump this session shutdown session : kill a specific session shutdown sessions server : kill sessions on a server clear counters : clear max statistics counters (add 'all' for all counters) show info : report information about the running process show stat : report counters for each proxy and server show errors : report last request and response errors for each proxy clear table : remove an entry from a table set table [id] : update or create a table entry's data show table [id]: report table usage stats or dump this table's contents disable frontend : temporarily disable specific frontend enable frontend : re-enable specific frontend set maxconn frontend : change a frontend's maxconn setting show servers state [id]: dump volatile server information (for backend <id>) show backend : list backends in the current running config shutdown frontend : stop a specific frontend disable agent : disable agent checks (use 'set server' instead) disable health : disable health checks (use 'set server' instead) disable server : disable a server for maintenance (use 'set server' instead) enable agent : enable agent checks (use 'set server' instead) enable health : enable health checks (use 'set server' instead) enable server : enable a disabled server (use 'set server' instead) set maxconn server : change a server's maxconn setting set server : change a server's state, weight or address get weight : report a server's current weight set weight : change a server's weight (deprecated) 

其中和stick table相關的命令有:

  clear table    : remove an entry from a table
  set table [id] : update or create a table entry's data show table [id]: report table usage stats or dump this table's contents 

例如:

# on haproxy
backend static_group
    stick-table type ip size 5k expire 1m

backend dynamic_group
    stick-table type ip size 5k expire 1m [root@xuexi ~]# echo "show table" | socat unix:/var/run/haproxy.sock - # table: static_group, type: ip, size:5120, used:0 # table: dynamic_group, type: ip, size:5120, used:0 

本文只是引入stats socket的操作方式,至於各命令的作用,參見官方手冊:http://cbonte.github.io/haproxy-dconv/1.7/management.html#9.3

2.3 使用客戶端源IP作為客戶端識別符號

配置檔案部分內容如下:

frontend http-in
    bind             *:80
    mode             http
    log              global

    acl url_static   path_beg  -i /static /images /stylesheets
    acl url_static   path_end  -i .jpg .jpeg .gif .png .ico .bmp .html

    use_backend      static_group   if url_static
    default_backend  dynamic_group

backend dynamic_group
    stick-table type ip size 5k expire 1m
    stick on src
    balance roundrobin
    option http-server-close
    option httpchk  GET /index.php
    http-check expect  status 200
    server app1 192.168.100.60:80 check rise 1 maxconn 3000 server app2 192.168.100.61:80 check rise 1 maxconn 3000 backend static_group stick-table type ip size 5k expire 1m stick on src balance roundrobin option http-keep-alive http-reuse safe option httpchk GET /index.html http-check expect status 200 server staticsrv1 192.168.100.62:80 check rise 1 maxconn 5000 server staticsrv2 192.168.100.63:80 check rise 1 maxconn 5000 

上面的配置中,設定了acl,當滿足靜態訪問時,使用static_group後端組,否則使用dynamic_group後端組。在兩個後端組中,都設定了stick-tablestick on,其中stick on是儲存指定內容,並在請求到達時匹配該內容,它的具體用法見後文。只有配置了stick on後,haproxy才能根據匹配的結果決定是否儲存到stick table中,以及如何篩選待分派的後端。

總之,上面的兩個後端組都已經指定了要向stick table中儲存源ip地址作為key。當客戶端請求到達時,haproxy根據排程演算法分配一個後端,但請求交給後端成功後,Haproxy立即向stick table表中插入一條stickiness記錄。當客戶端請求再次到達時,haproxy發現能匹配源ip,於是按照該stickiness記錄,將請求分配給對應的後端。

以下是分別使用兩臺機器測試192.168.100.59/index.html192.168.100.59/index.php後,stick table記錄的資料。

[root@xuexi ~]# echo "show table static_group" | socat unix:/var/run/haproxy.sock - 
# table: static_group, type: ip, size:5120, used:2
0x1bc0024: key=192.168.100.1 use=0 exp=48013 server_id=2 0x1bbec14: key=192.168.100.59 use=0 exp=27994 server_id=1 [root@xuexi ~]# echo "show table dynamic_group" | socat unix:/var/run/haproxy.sock - # table: dynamic_group, type: ip, size:5120, used:2 0x1bc00c4: key=192.168.100.1 use=0 exp=53686 server_id=2 0x1bbeb04: key=192.168.100.59 use=0 exp=34309 server_id=1 

其中server_id預設是從1自增的,它可以在server指令中用"id"選項進行顯式指定。例如:

server staticsrv1 192.168.100.62:80 id 111 check rise 1 max conn 6500 

如果,在使用stickiness的同時,haproxy還設定了cookie,誰的優先順序高呢?

2.4 使用cookie作為客戶端識別符號

一般會話保持考慮的物件是應用程式伺服器,因此此處我們忽略後端的靜態伺服器,只考慮php應用伺服器。在dynamic_group兩個後端server app1和app2的index.php中分別設定好PHPSESSID作為測試。例如:

<h1>response from webapp 192.168.100.60</h1>
<?php session_start(); echo "Server IP: "."<font color=red>".$_SERVER['SERVER_ADDR']."</font>"."<br>"; echo "Server Name: "."<font color=red>".$_SERVER['SERVER_NAME']."</font>"."<br>"; echo "SESSIONNAME: "."<font color=red>".session_name()."</font>"."<br>"; echo "SESSIONID: "."<font color=red>".session_id()."</font>"."<br>"; ?> 

cookie是string的一種特殊情況,因此建立stick table時,指定type為string。以下是在haproxy上的配置:

backend dynamic_group
    stick-table type string len 32 size 5k expire 2m
    stick on req.cook(PHPSESSID)
    stick store-response res.cook(PHPSESSID)
    balance roundrobin
    option http-server-close
    option httpchk  GET /index.php
    http-check expect  status 200
    server app1 192.168.100.60:80 check rise 1 maxconn 3000 server app2 192.168.100.61:80 check rise 1 maxconn 3000 

stick store-response指令表示從響應報文中匹配某些資料出來,然後儲存到stick table中,此處表示擷取響應報文中"Set-Cookie"欄位中名為"PHPSESSID"的cookie名進行儲存。stick on req.cook(PHPSESSID)表示從請求報文的"Cookie"欄位中匹配名為PHPSESSID的cookie。如果能和儲存在stick table中的PHPSESSID匹配成功,則表示該客戶端被處理過,於是將其引導到對應的後端伺服器上。嚴格地說,這裡不是識別客戶端,而是通過PHPSESSID來識別後端。

某次瀏覽器的請求得到如下結果:之後每次請求也都是分配到192.168.100.61上。注意,不要使用curl命令來測試,因為這裡是根據PHPSESSID匹配的,curl每次接收到響應後進程就直接退出了,無法快取cookie,因此curl每次請求都相當於一次新請求。

在haproxy上檢視stick table。

[root@xuexi ~]# echo "show table dynamic_group" | socat unix:/var/run/haproxy.sock - 
# table: dynamic_group, type: string, size:5120, used:1
0x12163d4: key=g5ossskspc96aecp4hvmsehoh4 use=0 exp=50770 server_id=2 

2.5 使用string作為客戶端識別符號

上面的cookie是string的一種特殊用法。使用string篩選內容進行儲存,靈活性非常大,可以通過它實現某些複雜、特殊的需求。

例如,從請求報文中擷取Host欄位的值作為key儲存起來。

backend dynamic_group
    stick-table type string size 5k expire 2m
    stick on req.hdr(Host)
    balance roundrobin
    option http-server-close
    option httpchk  GET /index.php
    http-check expect  status 200
    server app1 192.168.100.60:80 check rise 1 maxconn 3000 server app2 192.168.100.61:80 check rise 1 maxconn 3000 

找一臺linux客戶端使用curl進行測試,發現所有請求都將引導到同義後端伺服器上。

[[email protected] ~]# for i in `seq 1 5`;do grep "response" <(curl 192.168.100.59/index.php 2>/dev/null);done <h1>response from webapp 192.168.100.60</h1> <h1>response from webapp 192.168.100.60</h1> <h1>response from webapp 192.168.100.60</h1> <h1>response from webapp 192.168.100.60</h1> <h1>response from webapp 192.168.100.60</h1> 

檢視stick table也只能看到一條記錄,而且其key部分正是捕獲到的Host欄位的值。

[root@xuexi ~]# echo "show table dynamic_group" | socat unix:/var/run/haproxy.sock -       
# table: dynamic_group, type: string, size:5120, used:1
0xf0d904: key=192.168.100.19 use=0 exp=46308 server_id=1 

2.6 stick on、stick match、stick store

在前面haproxy的配置中出現過stick onstick store-response,除此之外,還有兩個指令stick matchstick store-request。語法如下:

stick store-request <pattern> [table <table>] [{if | unless} <condition>] stick store-response <pattern> [table <table>] [{if | unless} <condition>] stick match <pattern> [table <table>] [{if | unless} <cond>] stick on <pattern> [table <table>] [{if | unless} <condition>] 

其中stick store指令是從請求或響應報文中擷取一部分字元串出來,並將其作為stickiness的key儲存到stick table中。例如:

# 擷取響應報文中名為PHPSESSID的cookie作為key
stick store-response res.cook(PHPSESSID)

# 擷取請求報文中Host欄位的值作為key
stick store-request req.hdr(Host)

# 對請求的源ip地址進行匹配,若不是兄弟網路中的主機時,就寫入stick table中,且該table名為dynamic_group
stick store-request src table dynamic_group if !my_brother

stick match是將請求報文中的指定部分和stick table中的記錄進行匹配。例如:

# 擷取請求報文中名為PHPSESSID的cookie,去stick table中搜索是否存在對應的記錄
stick match req.cook(PHPSESSID)

# 當源IP不是本機時,去dynamic_group表中搜索是否有能匹配到源IP地址的記錄
stick match src table dynamic_group if !localhost

stick on等價於stick store+stick match,是它們的簡化寫法。例如:

# 儲存並匹配源IP地址
stick on src               #1 = #2 + #3
stick match src            #2
stick store-request src    #3 # 儲存並匹配源IP地址 stick on src table dynamic_group if !localhost #1 = #2 + #3 stick match src table dynamic_group if !localhost #2 stick store-request src table dynamic_group if !localhost #3 # 儲存並匹配後端伺服器設定的PHPSESSID stick on req.cook(PHPSESSID) #1 +#2 = #3 + #4 stick store-response res.cook(PHPSESSID) #2 stick match req.cook(PHPSESSID) #3 stick store-response res.cook(PHPSESSID) #4 

2.7 使用stick table統計狀態資訊

stick table除了儲存基本的粘性資訊,還能儲存額外的統計資料,這其實是haproxy提供的一種"取樣調查"功能。它能採集的資料種類有以下幾種:

每個stickiness記錄中可以同時儲存多個記錄型別,使用逗號分隔或多次使用store關鍵字即可。但注意,後端伺服器的server id會自動記錄,其它所有額外資訊都需要顯式指定。

需要注意,每個haproxy後端組只能有一張stick table,但卻不建議統計太多額外的狀態資訊,因為每多存一個型別,意味著使用更多的記憶體。

如果儲存所有上述列出的資料型別,需要116位元組,100W條記錄要用116M,這不是可以忽略的大小。此外還有50M的key,共166M。

例如下面的示例中,使用了通用計數器累計,並記錄了每30秒內的平均連線速率。

stick-table type ip size 1m expire 5m store gpc0,conn_rate(30s)

 

轉載請註明出處:http://www.cnblogs.com/f-ck-need-u/p/8558514.html

HAProxy系列文章:http://www.cnblogs.com/f-ck-need-u/p/7576137.html


在上一篇文章中,分析了haproxy如何通過cookie實現會話保持,本文討論haproxy另一種實現會話保持的方式:stick table。

1.stickiness和stick table簡介

stick table是haproxy的一個非常優秀的特性,這個表裡面儲存的是stickiness記錄,stickiness記錄了客戶端和服務端1:1對應的引用關係。通過這個關係,haproxy可以將客戶端的請求引導到之前為它服務過的後端伺服器上,也就是實現了會話保持的功能。這種記錄方式,俗稱會話粘性(stickiness),即將客戶端和服務端粘連起來。

stick table中使用key/value的方式對映客戶端和後端伺服器,key是客戶端的識別符號,可以使用客戶端的源ip(50位元組)、cookie以及從報文中過濾出來的部分String。value部分是服務端的識別符號。

stick table實現會話粘性的過程如下圖:

除了儲存key/value實現最基本的粘性,stick table還可以額外儲存每個stickiness記錄對應的狀態統計資料。比如stickiness記錄1目前建立了多少和客戶端的連線、平均建立連線的速度是多少、流入流出了多少位元組的資料、建立會話的數量等等。

stick table可以在"雙主模型"下進行復制(replication)。只要設定好對端haproxy節點,haproxy就會自動將新插入的、剛更新的記錄通過TCP連線推送到對端節點上。這樣一來,粘性記錄不會丟失,即使某haproxy節點出現了故障,其他節點也能將客戶端按照粘性對映關係引導到正確的後端伺服器上。而且每條stickiness記錄佔用空間都很小(平均最小50位元組,最大166位元組,由是否記錄額外統計資料以及記錄多少來決定佔用空間大小),使得即使在非常繁忙的環境下在幾十個節點之間推送都不會出現壓力瓶頸和網路阻塞(可以按節點數量、stickiness記錄的大小和平均併發量來計算每秒在網路間推送的資料流量)。

此外,stick table還可以在haproxy重啟時,在新舊兩個程序間進行復制,這是本地複製。當haproxy重啟時,舊haproxy程序會和新haproxy程序建立TCP連線,將其維護的stick table推送給新程序。這樣新程序不會丟失粘性資訊,和其他節點也能最大程度地保持同步,使得其他節點只需要推送該節點重啟過程中新增加的stickiness記錄就能完全保持同步。

2.使用stick table

下圖是本文測試時的環境:

2.1 建立stick table

首先看建立stick table的語法:

stick-table type {ip | integer | string [len <length>] | binary [len <length>]}
            size <size> [expire <expire>] [nopurge] [peers <peersect>] [store <data_type>]* 

其中

  • type ip | integer | string:使用什麼型別的key作為客戶端識別符號。可以是客戶端的源IP,可以是一個整數ID值,也可以是一段從請求報文或響應報文中匹配出來的字串。
  • size:表中允許的最大stickiness記錄數量。單位使用k、m和g表示,分別表示1024、2^20和2^30條記錄。
  • expire:stickiness記錄的過期時長。當某記錄被操作後,過了一段時間就會過期,過期的記錄會自動從stick table中移除,釋放表空間。
  • nopurge:預設情況下,當表滿後,如果還有新的stickiness記錄要插入進來,haproxy會自動將一部分老舊的stickiness