负载均衡的核心价值是把请求分散到多台后端服务器上,从而提升系统的吞吐量和可用性。但HTTP本身是无状态协议,一旦用户的请求被转发到不同的服务器,就会出现登录态丢失、验证码反复刷新、购物车数据消失等问题。解决这个问题的技术手段统称为会话保持(Session Persistence,也叫粘性会话),本文将详细分析各类实现方案及其实际配置方法。

一、什么是会话保持,为什么需要它
会话保持指的是让同一个客户端的后续请求始终路由到同一台后端服务器处理,或者让会话数据在多台服务器之间可共享访问。它的必要性源于两点:第一,应用服务器通常把用户状态保存在本地内存中,比如Tomcat的Session默认存放在JVM堆里;第二,负载均衡算法大多是无状态的轮询或最少连接,转发时并不关心这个用户之前连过谁。
举个例子:用户在Server A上登录成功,Session写入了A的内存,下一个请求被轮询到Server B,B的内存里没有这个Session,用户就会被踢回登录页。在小规模集群里这表现为偶发的重复登录,在高并发场景下则可能演变成大面积的用户投诉。因此,会话保持既是功能问题,也是体验问题。
需要注意,会话保持和会话共享是两个不同的思路:前者是让请求粘在某台机器上,后者是让数据脱离单机存储。两者的实现代价和可靠性差异很大,下面分别展开。
二、基于负载均衡层的会话保持方案
1. 源地址哈希(ip_hash)
源地址哈希是最简单的方案,负载均衡器根据客户端IP计算哈希值,把同一个IP的请求固定转发到同一台后端。Nginx中的配置如下:
upstream backend {
ip_hash;
server 192.168.1.10:8080;
server 192.168.1.11:8080;
}
server {
listen 80;
location / {
proxy_pass http://backend;
}
}
这种方式的优点是配置简单、后端应用无需任何改动。缺点也很明显:大型企业内网用户往往共享同一个出口IP,会导致流量严重不均;一旦某台后端故障,其上的会话会全部丢失;客户端IP变化(比如移动网络切换、代理)也会导致会话中断。因此ip_hash只适合规模较小、对可用性要求不高的场景。
2. Cookie植入(Cookie Insert)
由负载均衡器在第一次响应时给客户端植入一个Cookie,后续请求中负载均衡器读取该Cookie决定转发目标。F5、HAProxy和Nginx的sticky模块都支持这种方式。HAProxy的配置示例:
backend web_servers
balance roundrobin
cookie SERVERID insert indirect nocache
server web1 192.168.1.10:8080 cookie web1 check
server web2 192.168.1.11:8080 cookie web2 check
相比ip_hash,Cookie植入解决了IP共享导致的分布不均问题,识别的是浏览器实例而不是IP地址。但它依然没有解决根本问题:会话数据仍在单台服务器上,后端节点宕机或扩容缩容时,粘在该节点上的用户会话照样丢失。另外要注意,如果客户端禁用Cookie,该方案会失效。
3. URL重写
在URL后面附加会话标识参数,比如将请求改写为带有服务器路由信息的地址。这种方式不依赖Cookie,但会泄露后端拓扑、破坏URL的整洁性,对SEO不友好,而且需要应用配合改写所有链接,现在已经很少使用。
三、基于应用层的会话共享方案
1. Session集中存储到Redis
这是目前生产环境的主流做法:把Session从应用服务器的JVM内存中抽离出来,统一存放到Redis等集中式缓存中,任何一台后端都能读取同一份会话数据。以Spring Boot为例,引入spring-session-data-redis后只需少量配置:
// 引入依赖后,在配置文件中设置
// spring.session.store-type=redis
// spring.redis.host=192.168.1.100
// spring.redis.port=6379
@Configuration
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 1800)
public class SessionConfig {
// 开启后,HttpSession由Redis托管
// 应用服务器本地不再保存Session
}
这种方案的优势是后端节点完全对等,任何一台宕机都不影响用户会话,天然支持弹性扩缩容,也方便灰度发布。代价是每次请求都要访问Redis,多了一次网络开销,同时引入了Redis这个单点依赖,必须做好Redis的高可用(哨兵或集群模式)和持久化策略。
2. Session复制
Session复制是指服务器之间同步广播Session数据,每台节点都保存全量会话。Tomcat内置了这种能力,只需在server.xml中配置<Cluster>元素即可开启。它的优点是应用零改造,缺点是随着节点数增加,网络广播开销急剧上升,每台机器都要承载全部会话内存,扩展性很差。一般只适合三五个节点的小集群,超过这个规模就不建议使用了。
3. 使用JWT等令牌方案
更进一步的做法是干脆不使用服务端Session,把用户状态编码成签名后的Token返回给客户端,服务端只做验签而不存储状态。这就是JWT(JSON Web Token)的无状态认证思路。它彻底摆脱了会话保持问题,负载均衡可以随意转发。但代价是令牌无法主动失效(登出、封号需要额外的黑名单机制),且令牌体积比SessionID大,每次请求都要携带完整用户信息。实践中常常采用短生命周期AccessToken加RefreshToken的组合来缓解这个问题。
四、方案对比与选型建议
| 方案 | 应用改造 | 容灾能力 | 扩展性 | 适用场景 |
|---|---|---|---|---|
| ip_hash | 无 | 差 | 差 | 小型集群、内网系统 |
| Cookie植入 | 无 | 一般 | 一般 | 传统Web应用、硬件负载均衡环境 |
| Session复制 | 无 | 好 | 差 | 3到5节点的小集群 |
| Redis集中存储 | 少量 | 好 | 好 | 大多数互联网应用 |
| JWT无状态 | 较大 | 好 | 好 | 前后端分离、开放API、微服务 |
从整体趋势来看,Session粘性方案正在被集中式存储和无状态令牌逐步取代,因为它与弹性伸缩、滚动发布等现代部署方式天然冲突。但如果系统历史悠久、无法改造应用代码,Cookie植入仍然是性价比最高的过渡方案。
五、常见问题与注意事项
第一,粘性会话与节点故障的矛盾。无论ip_hash还是Cookie植入,一旦某个后端节点宕机,其承载的所有会话都会丢失。补救办法是把Session外置到Redis,粘性只作为性能优化手段(让会话读取尽量命中同一节点以减少缓存网络访问),而不是唯一依赖。
第二,健康检查与故障摘除。配置粘性会话时务必开启健康检查,比如Nginx商业版的health_check或第三方nginx_upstream_check模块。否则请求会被持续转发到已经故障的节点上,粘性反而放大了故障影响面。
第三,扩容缩容引发的会话漂移。哈希算法在后端节点数量变化时,大量客户端会被重新映射到不同节点。如果使用一致性哈希(如Nginx的hash指令配合consistent参数)可以显著减少重映射的比例。
第四,Cookie的属性设置。植入的会话Cookie要合理设置Path、HttpOnly、Secure和SameSite属性,避免跨域场景下Cookie无法携带,也要防止会话标识被脚本读取造成会话劫持风险。
第五,Redis会话的内存与过期管理。集中存储Session时要设置合理的过期时间,防止长期不登录的会话占用内存;同时注意缓存雪崩问题,过期时间可以加随机抖动。序列化方式建议选择紧凑的JSON或Kryo,减少网络传输量。
总结一下,会话保持没有银弹,选择方案时要综合考虑应用架构、团队能力和运维成本。对于新项目,推荐直接采用Redis集中存储Session或JWT令牌方案,让负载均衡回归纯粹的流量分发角色;对于存量系统,可以先通过Cookie植入快速止血,再逐步演进到集中式会话管理。