Skip to content

第 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 + 黑名单
纯静态 / 无登录不需要考虑,随便轮询

第一次请求:
  客户端 ──► 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 房间、内存 SessionREST 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 · 软件负载均衡实战