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".
其中大致说明了以下几个意思:
- 该关键词表示,haproxy将在客户端没有cookie时(比如第一次请求),在响应报文中插入一个cookie。
- 当没有使用关键词"preserve"选项时,如果后端服务器设置了一个和此处名称相同的cookie,则首先删除服务端设置的cookie。
- 该cookie只能作为会话保持使用,无法持久化到客户端的磁盘上(因为haproxy设置的cookie没有maxAge属性,无法持久保存,只能保存在浏览器缓存中)。
- 默认情况下,除非使用了"indirect"选项,否则服务端可以看到客户端请求时的所有cookie信息。
- 由于缓存的影响,建议加上"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-table
和stick 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.html
和192.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 on
和stick store-response
,除此之外,还有两个指令stick match
、stick 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)