在多台后端服务器之间做负载均衡时,最让人头疼的问题之一就是会话丢失。用户明明已经登录成功,下一次请求却被转发到了另一台没有该会话数据的服务器上,直接被踢回登录页。解决这个问题的常见手段就是会话亲和性(Session Affinity),也叫会话保持或粘滞会话。Apache通过mod_proxy_balancer模块提供了Cookie插入机制,让代理层自动完成粘滞调度,不需要修改应用代码。本文将详细讲解它的原理、配置与踩坑要点。

一、什么是Cookie插入会话亲和性
会话亲和性的核心思想是:让同一个客户端的后续请求始终落在同一台后端服务器上。Apache的mod_proxy_balancer支持两种粘滞方式:一种是应用原生Cookie,即应用服务器自己下发包含路由标识的Cookie(比如Tomcat的JSESSIONID末尾附带的.server1路由后缀);另一种就是本文重点讨论的Cookie插入模式。
Cookie插入(Cookie Insertion)的含义是:Apache在第一次收到某个客户端请求时,先按照负载均衡算法挑选一台后端服务器处理请求,然后在响应头中“插入”一个由Apache自己生成的Cookie,Cookie的值就是被选中那台服务器的路由标识(route)。客户端后续请求会自动带上这个Cookie,Apache解析其中的路由值,直接把请求转发给对应的服务器,从而实现粘滞。
这种模式最大的优势在于对应用透明。应用代码完全不需要感知负载均衡的存在,也不用修改任何会话处理逻辑,所有工作都由代理层完成。这对于那些无法修改源码的老旧系统,或者不方便在应用中改造会话机制的团队来说,是非常实用的方案。
二、核心配置详解与完整示例
实现Cookie插入依赖几个模块,分别是mod_proxy、mod_proxy_balancer和mod_proxy_http。确认模块已加载后,就可以在Apache配置文件或虚拟主机中编写负载均衡配置。下面是一个可直接使用的完整示例:
LoadModule proxy_module modules/mod_proxy.so
LoadModule proxy_balancer_module modules/mod_proxy_balancer.so
LoadModule proxy_http_module modules/mod_proxy_http.so
LoadModule lbmethod_byrequests_module modules/mod_lbmethod_byrequests.so
LoadModule slotmem_shm_module modules/mod_slotmem_shm.so
LoadModule headers_module modules/mod_headers.so
<VirtualHost *:80>
ServerName www.ipipp.com
ProxyRequests Off
Header add Set-Cookie "BalancerRoute=.%{BALANCER_WORKER_ROUTE}e; path=/; HttpOnly" env=BALANCER_ROUTE_CHANGED
<Proxy balancer://mycluster>
BalancerMember http://192.168.1.101:8080 route=web1
BalancerMember http://192.168.1.102:8080 route=web2
BalancerMember http://192.168.1.103:8080 route=web3
ProxySet stickysession=BalancerRoute
ProxySet lbmethod=byrequests
</Proxy>
ProxyPass / balancer://mycluster/
ProxyPassReverse / balancer://mycluster/
</VirtualHost>
配置中有几个关键点需要理解。每个<BalancerMember>通过route参数定义了路由标识,这个标识就是Cookie值中携带的服务器身份。stickysession指定了Apache要读取的Cookie名称,这里是BalancerRoute。那条Header add Set-Cookie指令则是Cookie插入的关键:BALANCER_WORKER_ROUTE是Apache内置的环境变量,保存了本次请求实际使用的路由值;BALANCER_ROUTE_CHANGED表示路由发生了变化(首次请求或切换服务器时该变量会被设置),只有此时才需要下发新Cookie,避免每个响应都重复发送。
另外要注意ProxyRequests Off这一行不能省略。关闭正向代理,只启用反向代理能力,既是功能上的要求,也是安全上的考虑,否则Apache可能被外部滥用为开放代理。
三、Cookie插入与应用原生Cookie粘滞的对比
应用原生Cookie方式要求后端应用在生成会话Cookie时附加路由信息。以Tomcat为例,需要在server.xml的Connector或Engine节点配置jvmRoute="web1",这样下发的JSESSIONID就会变成abc123.web1的形式。Apache端则配置stickysession=JSESSIONID,并用stickysessionsep=. 指定分隔符。
两种方式各有优劣。原生Cookie方式下,路由信息由应用自己管理,即使会话被序列化同步到其他节点,路由仍然准确;而Cookie插入方式下,Apache下发的Cookie与应用会话是两套独立机制,如果粘滞的那台服务器宕机,请求会被转发到其他服务器,但新服务器上并没有原来的会话数据,用户登录态仍然会丢失,Apache只能保证“粘人”,不能保证“粘会话”。所以Cookie插入更像是请求分发层面的粘滞,真正的会话容灾还需要配合会话复制或集中式会话存储(如Redis)来实现。
从运维成本看,Cookie插入完全不需要碰应用配置,扩容时只需在Apache端新增一个BalancerMember并分配路由标识即可,对Java、PHP、Python等各类后端技术栈一视同仁,通用性明显更好。这也是很多混合技术栈团队选择它的原因。
四、常见问题排查与生产环境建议
排查粘滞是否生效,最直接的方法是curl观察Cookie。使用类似下面的命令,多次请求并携带返回的Cookie,检查响应是否始终来自同一台后端:
# 首次请求,观察Set-Cookie响应头 curl -v http://www.ipipp.com/ 2>&1 | grep -i set-cookie # 携带Cookie再次请求,验证是否粘滞到web1 curl -v -H "Cookie: BalancerRoute=.web1" http://www.ipipp.com/ 2>&1 | grep -i set-cookie
如果发现每次请求都重新下发Cookie,常见原因有几个:一是route参数忘记配置或拼写不一致,导致Apache无法匹配;二是Cookie的path设置与应用实际路径不匹配,浏览器没有回传Cookie;三是客户端禁用了Cookie,此时粘滞自然失效,可以考虑结合IP哈希作为兜底。还有一种情况是响应中出现了Set-Cookie里的BalancerRoute值与后端route不符,通常是多台Apache前端又叠了一层负载均衡,把响应头弄乱了,需要理清代理链路。
生产环境建议给每个BalancerMember加上故障转移参数,例如retry=30 connectiontimeout=5 timeout=30,这样某台后端异常时Apache能快速切换,恢复探测的时间也可控。同时开启ProxyPass / balancer://mycluster/ failonstatus=502,503可以让特定错误码触发故障转移。另外别忘了通过balancer-manager页面实时观察各节点的状态和请求分布,但生产环境务必用IP白名单或认证保护该页面,避免暴露内部拓扑。
最后要提醒一点:Cookie插入的粘滞粒度是基于浏览器Cookie的,如果用户清理Cookie、换浏览器或者使用无痕模式,粘滞关系就会重置。因此它适合作为会话保持的第一道防线,而不是唯一手段。对登录态敏感的系统,建议同时落地会话共享机制,两层配合才能真正兼顾体验与高可用。