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

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


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

1.stickiness和stick table簡介

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

stick table中使用key/value的方式映射客戶端和後端服務器,key是客戶端的標識符,可使用客戶端的源ip(50字節)、cookie以及從報文中過濾出來的部分String。value部分是服務端的標識符。git

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

除了存儲key/value實現最基本的粘性,stick table還能夠額外存儲每一個stickiness記錄對應的狀態統計數據。好比stickiness記錄1目前創建了多少和客戶端的鏈接、平均創建鏈接的速度是多少、流入流出了多少字節的數據、創建會話的數量等等。web

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表示,分別表示102四、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進行測試,發現全部請求都將引導到同義後端服務器上。

[root@xuexi ~]# 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)
相關文章
相關標籤/搜索