主题
第 5 章 · 会话与粘性
生活类比:你去银行办按揭,第一天接待你的是 王经理。第二天你带着材料再来,如果前台随机分配了 李经理,你得从头解释一遍情况——体验极差。
会话粘性(Session Affinity / Sticky Session) 就是尽量让「同一客户」找到「同一个接待者」。
但更现代的做法是:任何经理都能看你的档案柜(Redis)——不需要固定王经理。
5.1 什么是 Session?为什么 LB 会在乎它?
HTTP 默认 无状态:服务器不记得你上一秒是谁。
网站用 Session 记住登录态:
1. 用户登录成功
2. 服务器生成 session_id=abc123,存 {user: 张三}
3. 通过 Set-Cookie 把 session_id 给浏览器
4. 下次请求带上 Cookie: session_id=abc123
5. 服务器查 session 存储,知道是张三问题来了:如果有 3 台服务器,Session 存在 各自内存 里:
请求 1 登录 → 落到 A,session 在 A 内存
请求 2 刷新 → 落到 B,B 内存里没有 abc123 → 未登录!这就是 会话不一致,负载均衡必须处理。
5.2 三种解决方案(由差到好)
方案 A:会话粘性(Sticky Session)
强制同一客户端总是打到同一后端。
| 实现方式 | 说明 |
|---|---|
| IP Hash | 按客户端 IP 哈希(见第 3 章缺点) |
| Cookie 粘性 | LB 注入 SERVERID=backend2 Cookie |
| Header 路由 | 按 X-User-Id 一致性哈希 |
Nginx 商业版 / 部分网关支持:
nginx
upstream backend {
sticky cookie srv_id expires=1h domain=.example.com path=/;
server 10.0.0.1;
server 10.0.0.2;
}优点:不改应用代码(老系统救急)
缺点:节点挂了会话丢、扩缩容漂移、分布不均
方案 B:Session 集中存储(推荐)
Session 不存本机,存 Redis / Memcached:
任意后端收到请求
│
▼
用 session_id 查 Redis → 拿到用户信息请求 → LB(任意算法)→ A 或 B 或 C
│
└── 都读同一个 Redis生活类比:不再固定王经理,而是所有经理都能查 你的客户档案柜。
优点:任意算法、随意扩缩容、节点挂了不丢登录
缺点:多一个 Redis 依赖(但这是标准做法)
方案 C:无状态 Token(JWT 等)
登录后下发 自包含 Token(JWT),服务端不存 Session:
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...任意后端用 同一套密钥 验证 Token 即可识别用户。
优点:完全无状态,最适合微服务
缺点:Token 难主动失效(要等过期)、Payload 不宜过大
5.3 选型建议
| 场景 | 推荐 |
|---|---|
| 新项目 | JWT 或 Redis Session |
| 老 PHP/Java 单体,session 在内存 | 短期 Cookie 粘性 + 长期迁 Redis |
| 强登录安全(要能立刻踢人下线) | Redis Session + 黑名单 |
| 纯静态 / 无登录 | 不需要考虑,随便轮询 |
5.4 Cookie 粘性的工作原理
第一次请求:
客户端 ──► LB ──► 选中 B
LB 在响应里 Set-Cookie: SERVERID=B
之后请求:
客户端带上 SERVERID=B ──► LB 读到 Cookie ──► 强制去 B坑
| 坑 | 说明 |
|---|---|
| 用户清 Cookie | 粘性丢失 |
| 多域名 | Cookie domain 要配对 |
| HTTPS 安全 | 设 Secure; HttpOnly |
| B 挂了 | 要带 Cookie 的节点挂了,用户要重新登录或重新粘性 |
5.5 WebSocket 与长连接
WebSocket 建立后 连接一直挂在某一台 上,天然「粘性」。
客户端 ──WS──► LB ──► 服务器 A(连接一直保持)如果 A 挂了,连接断开,客户端要 重连——可能落到 B,服务端要处理「断线重连」逻辑。
生活类比:电话客服,通话中不能换接线员;断了只能重拨,可能换一个人。
5.6 有状态服务 vs 无状态服务
| 有状态 | 无状态 | |
|---|---|---|
| 例子 | 游戏房间、WebSocket 房间、内存 Session | REST API、JWT 鉴权 |
| LB 要求 | 粘性或房间路由 | 任意算法 |
| 扩缩容 | 难(要等连接迁移) | 易 |
架构原则:能无状态就无状态。有状态是复杂度之源。
5.7 改造实战:内存 Session → Redis
以典型 Web 应用为例(伪代码):
python
# 改造前
session_store = {} # 进程内存 dict
# 改造后
import redis
r = redis.Redis(host='redis.internal', port=6379)
def get_session(session_id):
data = r.get(f"session:{session_id}")
return json.loads(data) if data else None
def set_session(session_id, user_data, ttl=3600):
r.setex(f"session:{session_id}", ttl, json.dumps(user_data))改造后 LB 可放心用 least_conn 或轮询。
5.8 面试题
问:负载均衡怎么保证 Session 不丢?
三种思路:① 会话粘性(IP Hash / Cookie),适合遗留系统;② Session 外置到 Redis,推荐;③ JWT 无状态。现代架构优先 ② 或 ③,避免 IP Hash。
问:粘性会话有什么问题?
节点故障丢会话、扩缩容导致哈希漂移、NAT 下分布不均、弹性伸缩困难。
5.9 一句话总结
固定柜员(粘性)是权宜之计;共享档案柜(Redis)或带工牌(JWT)才是正道。
下一章 → 06 · 软件负载均衡实战