会话保持(session persistence),也称为粘性会话(sticky sessions),是代理使用的一种负载均衡技术,用于确保来自某个特定客户端的所有请求,在整个会话期间都被一致地路由到同一台后端服务器。
该机制解决的是有状态应用在负载均衡环境中面临的难题。许多 Web 应用会把会话相关数据(例如购物车内容、用户认证状态、个性化设置)保存在服务器上。如果没有会话保持,客户端的后续请求可能被负载均衡器分发到另一台后端服务器。若该服务器没有这个客户端的会话数据,会话状态就会丢失,从而导致报错、要求重新认证,或迫使客户端重新开始整个操作流程。粘性会话通过在初次建立连接后把客户端"粘"在某台服务器上来避免这一问题。
会话保持的工作原理
代理通过多种方法实现会话保持,核心思路都是先识别客户端,再把该客户端映射到某台特定的后端服务器。
客户端识别方法
IP Hash(源 IP 哈希)
该方法把客户端的 IP 地址作为决定后端服务器的键。代理对客户端 IP 地址做哈希运算,并用结果值从可用服务器池中挑选一台服务器。这样可以保证来自同一 IP 地址的所有请求都被送往同一台服务器。
- 机制:
hash(client_ip) % number_of_servers - 优点: 实现简单,不需要客户端 Cookie,也不需要修改应用。
- 缺点:
- 负载不均: 如果许多客户端共用同一个公网 IP(例如位于 NAT 网关或企业网络之后),那一台服务器就可能过载。
- 服务器故障: 如果被分配的服务器发生故障,客户端会话就会丢失,后续请求将被路由到新的服务器(若哈希计算指向另一台服务器,或代理重新评估分配,粘性可能就此失效)。
- 动态 IP: 使用动态 IP 地址的客户端,如果在会话中途 IP 发生变化,可能会丢失会话。
基于 Cookie 的会话保持
这是应用广泛且更灵活的方法。代理或应用会在客户端浏览器中写入一个 Cookie,其中包含代理用来把后续请求路由到正确后端服务器的信息。
代理生成的 Cookie
代理本身在把 HTTP 响应发给客户端之前,向响应中注入一个 Cookie。该 Cookie 通常包含处理了首次请求的后端服务器的标识。在后续请求中,代理读取这个 Cookie,并把客户端引导到对应的服务器。
- 机制: 代理拦截响应并添加
Set-Cookie: PROXY_SESSION_ID=server_id头。在后续请求中,代理读取Cookie: PROXY_SESSION_ID=server_id并转发到server_id。 - 优点: 无需修改应用,对应用完全透明。
- 缺点:
- 服务器故障: 如果被标识的服务器发生故障,客户端会话会丢失。代理必须把请求路由到新的服务器并更新 Cookie,否则客户端会遇到错误。
- Cookie 管理: 要求客户端浏览器接受并回送 Cookie。
应用生成的 Cookie
后端应用自行创建并管理会话 Cookie(例如 Java 应用的 JSESSIONID、PHP 的 PHPSESSID)。代理被配置为检查这个特定的应用 Cookie,并用它的值(或其中一部分)来确定目标后端服务器。代理可以对 Cookie 值做哈希,或从中提取内嵌的服务器标识。
- 机制: 应用设置
Set-Cookie: APP_SESSION_ID=unique_session_id。代理读取Cookie: APP_SESSION_ID=unique_session_id并用该值映射到一台服务器。 - 优点: 复用应用已有的会话管理,在识别唯一会话方面可能更可靠。
- 缺点: 需要配置代理以理解应用特定的 Cookie 格式。服务器故障仍会导致会话丢失。
基于请求头的会话保持
这种方式不如 IP 哈希或基于 Cookie 的方法常见,它使用自定义 HTTP 头来携带会话或服务器标识信息。该请求头可以由客户端或中间代理插入。
- 机制: 代理读取
X-Server-ID: server_123或X-Client-Session: abcdef并据此路由。 - 优点: 在特定的 API 网关或微服务架构中可能很有用。
- 缺点: 需要客户端或上游代理配合注入请求头。在没有客户端脚本的情况下,不适用于标准 Web 浏览器。
粘性会话方法对比
| 特性 | IP Hash | 代理生成的 Cookie | 应用生成的 Cookie |
|---|---|---|---|
| 客户端标识来源 | 客户端 IP 地址 | 代理注入的 Cookie | 应用生成的 Cookie |
| 是否需改应用 | 不需要 | 不需要 | 不需要(代理读取已有的应用 Cookie) |
| 负载分布 | 可能不均(NAT/代理) | 通常良好 | 通常良好 |
| 服务器故障 | 会话丢失,重新路由到新服务器 | 会话丢失,重新路由到新服务器 | 会话丢失,重新路由到新服务器 |
| 保持粒度 | 按 IP 地址 | 按浏览器会话(Cookie 过期) | 按应用会话(Cookie 过期) |
| 客户端要求 | 无 | 支持 Cookie | 支持 Cookie |
| 复杂度 | 低 | 中(代理负责 Cookie 管理) | 中(需配置代理读取应用 Cookie) |
会话保持为什么重要
- 支持有状态应用: 对于把会话数据保存在服务器本地的应用(例如电商购物车、登录会话、多步骤表单)来说必不可少。
- 用户体验: 避免会话丢失、重复登录或进度丢失,使体验更流畅。
- 简化应用设计: 减少对复杂分布式会话管理方案(例如 Redis 或 Memcached 等外部会话存储)的需求,让应用保持更简单。
缺点与注意事项
- 负载不均: 主要缺点。如果某台服务器承接了过多"粘住"的客户端,它可能过载,而其他服务器却处于闲置状态,这会抵消负载均衡带来的好处。
- 服务器故障的影响: 如果一台持有活跃粘性会话的服务器发生故障,当前"粘"在该服务器上的所有客户端都会丢失会话数据,导致这些客户端体验下降或服务中断。
- 扩展性挑战: 会让横向扩展更复杂。增删后端服务器可能打断已有的粘性会话,扩缩容时需要谨慎管理。
- 资源消耗: 维护粘性映射关系(例如保存在查找表中)会占用代理的资源。
- Cookie 管理开销: 对基于 Cookie 的方法而言,每个请求设置和读取 Cookie 都会带来少量开销。
配置示例
Nginx(开源版)
Nginx 可以通过 ip_hash 指令实现粘性会话,也可以借助商业模块或 nginx-sticky-module-ng 这类第三方模块,使用更高级的基于 Cookie 的方法。
IP Hash
http {
upstream backend {
ip_hash;
server backend1.example.com;
server backend2.example.com;
server backend3.example.com;
}
server {
listen 80;
location / {
proxy_pass http://backend;
}
}
}
在这个示例中,ip_hash; 确保来自同一客户端 IP 地址的请求,被一致地路由到 backend upstream 组中的同一台服务器。
基于 Cookie(使用 sticky 模块——通常需要 Nginx Plus 或第三方模块)
如果使用 Nginx Plus 或兼容的第三方模块:
http {
upstream backend {
# 使用名为 "route" 的、由代理生成的 Cookie
# 其值是 Nginx 分配给该服务器的路由 ID。
sticky route $cookie_route;
server backend1.example.com route=A;
server backend2.example.com route=B;
server backend3.example.com route=C;
}
server {
listen 80;
location / {
proxy_pass http://backend;
}
}
}
这里,sticky route $cookie_route; 让 Nginx 使用名为 route 的 Cookie 来实现会话保持。Nginx 会为该 Cookie 设置一个与最初选中的后端服务器相对应的值(A、B 或 C)。
此外,也可以用 sticky learn 来学习应用生成的 Cookie:
http {
upstream backend {
# 学习应用的 JSESSIONID Cookie
sticky learn
create=$upstream_cookie_JSESSIONID
lookup=$cookie_JSESSIONID
zone=client_sessions:1m; # 用于存放会话数据的共享内存区
server backend1.example.com;
server backend2.example.com;
server backend3.example.com;
}
server {
listen 80;
location / {
proxy_pass http://backend;
}
}
}
该配置会尝试学习后端应用设置的 JSESSIONID Cookie。如果该 Cookie 存在,Nginx 就用它的值把后续请求路由到最初设置它的那台服务器。
HAProxy
HAProxy 提供了强大的粘性会话能力,同时支持基于 IP 和基于 Cookie 的方法。
IP Hash(源 IP 哈希)
frontend http_front
bind *:80
default_backend http_back
backend http_back
balance source # 使用客户端的 IP 地址
server backend1 192.168.1.10:80 check
server backend2 192.168.1.11:80 check
server backend3 192.168.1.12:80 check
balance source 指令让 HAProxy 使用客户端的源 IP 地址进行负载均衡,从而实际提供粘性会话。
代理生成的 Cookie
frontend http_front
bind *:80
default_backend http_back
backend http_back
balance roundrobin
cookie SERVERID insert indirect nocache # 插入名为 SERVERID 的 Cookie
server backend1 192.168.1.10:80 cookie S1 check
server backend2 192.168.1.11:80 cookie S2 check
server backend3 192.168.1.12:80 cookie S3 check
这里,cookie SERVERID insert indirect nocache 告诉 HAProxy 在返回给客户端的响应中插入名为 SERVERID 的 Cookie。每台服务器行上的 cookie S1、cookie S2 等指定了 HAProxy 为该服务器使用的值。随后 HAProxy 就用这个 Cookie 路由后续请求。indirect 表示只有在客户端还没有该 Cookie 时才设置它,nocache 则阻止缓存代理缓存带有 Set-Cookie 头的响应。
应用生成的 Cookie
frontend http_front
bind *:80
default_backend http_back
backend http_back
balance roundrobin
appsession JSESSIONID len 52 timeout 3h # 使用 JSESSIONID Cookie
server backend1 192.168.1.10:80 check
server backend2 192.168.1.11:80 check
server backend3 192.168.1.12:80 check
appsession JSESSIONID len 52 timeout 3h 指令配置 HAProxy 去查找名为 JSESSIONID 的 Cookie(在 Java 应用中很常见)。len 52 指定要考虑的会话 ID 长度,timeout 3h 设定当请求中不含该 Cookie 时,HAProxy 记住会话与服务器映射关系的时长。该方法要求应用设置 JSESSIONID Cookie。
