haproxy实现会话保持:cookie
haproxy实现会话保持(1):cookie
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的示例。
1 | backend dynamic_servers |
这个示例配置中,cookie
指令中指定的是insert命令,表示在将响应报文交给客户端之前,先插入一个属性名为”app_cook”的cookie,这个cookie在响应报文的头部将独占一个”Set-Cookie”字段(因为是插入新cookie),而”app_cook”只是cookie名称,它的值是由server指令中的cookie选项指定的,这里是”server1”或”server2”。
因此,如果这个请求报文分配给后端app2时,响应给客户端的响应报文中haproxy设置的“Set-Cookie”字段的样式为:
1 | Set-Cookie:app_cook=server2; path=/ |
除了insert命令,cookie指令中还支持rewrite和prefix两种设置cookie的方式,这三种cookie的操作方式只能三选一。此外,还提供一些额外对cookie的功能设置。
首先看看指令的语法:
1 | cookie <name> [ rewrite | insert | prefix ] [ indirect ] [ nocache ] |
本文详细分节讨论rewrite、insert、prefix的行为,并在讨论它们的时候会穿插说明indirect、nocache和preserve的行为,如果需要了解其他选项,请自翻官方手册。
下图是后文实验时使用的环境:
其中在后端提供的index.php内容大致如下,主要部分是设置了名为PHPSESSID
的cookie。
1 | <h1>response from webapp 192.168.100.61</h1> |
2.1 cookie insert
1 | insert This keyword indicates that the persistence cookie will have to |
其中大致说明了以下几个意思:
- 该关键词表示,haproxy将在客户端没有cookie时(比如第一次请求),在响应报文中插入一个cookie。
- 当没有使用关键词”preserve”选项时,如果后端服务器设置了一个和此处名称相同的cookie,则首先删除服务端设置的cookie。
- 该cookie只能作为会话保持使用,无法持久化到客户端的磁盘上(因为haproxy设置的cookie没有maxAge属性,无法持久保存,只能保存在浏览器缓存中)。
- 默认情况下,除非使用了”indirect”选项,否则服务端可以看到客户端请求时的所有cookie信息。
- 由于缓存的影响,建议加上”nocache”或”postonly”选项。
下面使用例子来解释insert的各种行为。
在haproxy如下配置后端。
1 | backend dynamic_group |
当使用浏览器第一次访问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”字段响应给客户端。
1 | backend dynamic_group |
因此,在cookie指令中绝对不能设置cookie名称和后端的cookie名称相同,否则后端就相当于”盲人”。例如此处的PHPSESSID,此时后端虽然认识PHPSESSID是自己发送出去的cookie名称,但是无法获取ID为”app_server1”的session上下文。
如果不配合”indirect”选项,服务端可以看到客户端请求时的所有cookie信息。如果配合”indirect”选项,则haproxy在将请求转发给后端时,将删除自己设置的cookie,使得后端只能看到它自己的cookie,这样对后端来说,整个过程是完全透明的,它不知道前面有负载均衡软件。
重新修改haproxy的cookie指令,并修改nginx配置文件中日志格式,在其中加上”$http_cookie”变量,它表示请求报文中的cookie信息。
1 | # haproxy |
客户端再次访问时,nginx的日志中将记录以下信息(只贴出了前几个字段)。
1 | PHPSESSID=47d0ina2m14gg67ovdf1d972d1; app_cook=app_server1 192.168.100.59 |
加上”indirect”选项,再测试。
1 | cookie app_cook insert indirect nocache |
结果如下:
1 | 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
1 | prefix This keyword indicates that instead of relying on a dedicated |
大致意思是:haproxy将在已存在的cookie(例如后端应用服务器设置的)上添加前缀cookie值,这个前缀部分是server指令中的cookie设置的,代表的是服务端标识符。在客户端再次访问时,haproxy将会自动移除这部分前缀,使得服务端只能看到它自己发出的cookie。在一些特殊环境下,客户端不支持多个”Set-Cookie”字段,这时可以使用prefix。
使用prefix的时候,**cookie指令设置的cookie名必须和后端设置的cookie一样(在本文的环境中是PHPSESSID)**,否则prefix模式下的haproxy不会对响应报文做任何改变。
1 | backend dynamic_group |
如下图:
从后端nginx上的日志上查看haproxy转发过来的请求,可以看到前缀已经被haproxy去掉了。
1 | PHPSESSID=oses71hjr64dl6lputpkmdpg12 192.168.100.59 - - |
2.3 cookie rewrite
1 | rewrite This keyword indicates that the cookie will be provided by the |
当后端服务器设置了cookie时,使用rewrite模式时,haproxy将重写该cookie的值为后端服务器的标识符。当应用程序需要同时考虑”Set-Cookie”和”Cache-control”字段时,该模式非常方便,因为应用程序可以决定是否应该设置一个为了保持会话的cookie。除非后端应用程序的环境非常复杂,否则不建议使用该模式。
同样,rewrite模式下的haproxy设置的cookie必须和后端服务器设置的cookie名称一致,否则不会做任何改变。
1 | backend dynamic_group |
结果如下图:
但是,当客户端持着”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配置如下后端。
1 | backend dynamic_group |
那么,从客户端第一次请求到第二次请求被处理的整个过程,大致如下:
当haproxy成功修改了响应报文中的cookie时,将在cookie表中插入一条记录,这条记录是维持会话的依据。
其实,通过cookie表保持和后端的会话只是默认情况,haproxy允许”即使使用了cookie也不进行会话绑定”的功能。这可以通过ignore-persist
指令来实现。当满足该指令的要求时,表示不将该cookie插入到cookie表中,因此无法实现会话保持,即使haproxy设置了cookie也没用。
例如,在backend中指定如下配置:
1 | backend dynamic_group |
这表示当请求uri以”.php”结尾时,将忽略会话保持功能。这表示,对于php结尾的请求,app_cook这个cookie从头到尾都是摆设。
当然,上面的设置是不合理的,更合理的应该是这样的。
1 | acl url_static path_beg /static /images /img /css |
与ignore-persist
相对的是force-persist
,但不建议使用该选项,因为它和option redispatch
冲突。