骏马金龙 (新博客:www.junmajinlong.com)

网名骏马金龙,钟情于IT世界里的各种原理和实现机制,强迫症重症患者。爱研究、爱翻译、爱分享。特借此一亩三分田记录自己成长点滴!!!

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进行测试,发现所有请求都将引导到同义后端服务器上。

[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)
posted @ 2018-03-13 16:39  骏马金龙  阅读(7337)  评论(2编辑  收藏  举报