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

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

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的示例。

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冲突。

posted @ 2018-03-13 01:18  骏马金龙  阅读(11632)  评论(8编辑  收藏  举报