一款中重度多人在线游戏在春节活动第一天遇到一个很常见的问题:登录量没有压垮网关,真正造成投诉的是玩家在切场景、进副本、重连时被分配到不同节点,旧连接里的临时状态没有跟过去,新节点又认为玩家还在上一局。技术群里看起来像网络抖动,客服侧看到的是“刚买的礼包没到账”“排队成功却进不去房间”。这类事故的根源通常不是机器不够,而是会话粘性没有被当成一等架构来设计。
核心判断
- 会话不是连接本身,而是玩家从认证通过到离开在线系统期间的一组可路由上下文
- 粘性应该围绕状态所有权建立,而不是围绕某台机器的 IP 建立
- 任何粘性都必须有迁移、过期、驱逐和观测策略,否则它会变成隐形单点
架构示意
flowchart LR
Client["客户端"] --> Edge["接入层"]
Edge --> Auth["认证服务"]
Auth --> Directory["会话目录"]
Directory --> GatewayA["网关 A"]
Directory --> GatewayB["网关 B"]
GatewayA --> Room["房间/场景服务"]
GatewayB --> Room
Room --> Store["玩家运行态存储"]
Directory --> Drain["迁移与摘流控制"]
先把会话拆开看
很多团队把会话粘性理解成负载均衡上的 sticky session,这在 Web 后台里可能够用,但在游戏服务器里明显不够。游戏会话至少包含四层含义:接入连接、认证身份、在线路由、玩法归属。接入连接关心 TCP 或 WebSocket 长连接在哪个网关上;认证身份关心 token 是否仍然有效;在线路由关心这个玩家当前应该由谁收包和发包;玩法归属关心角色是否在某个房间、战斗、队伍或交易上下文中。把这四件事混在一个内存 map 里,短期开发会很快,长期故障会很难解释。更稳的做法是让网关只拥有连接,会话目录拥有路由,玩法服务拥有业务状态,存储拥有可恢复事实。
粘什么,不粘什么
会话粘性的第一条原则是只粘必须粘的状态。连接必须粘在接入节点上,因为操作系统 socket 不会自动漂移;房间内帧同步状态通常必须粘在房间进程上,因为每一帧都跨网络取状态会把延迟打爆;但玩家昵称、头像、背包摘要、活动入口红点就不应该粘在网关内存里。它们可以缓存,却不能成为路由正确性的前提。实际落地时,我们会给每类状态标注 ownership、ttl、recoverable、handoff 四个属性。ownership 决定谁说了算,ttl 决定脏状态多久自然消失,recoverable 决定是否可以从数据库或事件日志重建,handoff 决定发布或迁移时能否转交。
会话目录的关键字段
会话目录不要贪心,它不是玩家总表。一个可靠的目录一般只需要 playerId、sessionId、gatewayId、roomId、stateVersion、expireAt、lastSeenAt、migrationToken。sessionId 用来区分同一玩家的两次登录,避免旧连接踢掉新连接;stateVersion 用来处理并发刷新,防止重连请求把新路由覆盖成旧路由;expireAt 给脏会话兜底;migrationToken 用于摘流期间把玩家从旧网关平滑引到新网关。目录写入要带条件更新,例如只有当前 sessionId 匹配时才能续租,这比事后查日志找“谁覆盖了谁”有用得多。
断线重连不是登录重试
很多线上问题来自把断线重连当成普通登录。普通登录可以重新挑网关,重连应该优先回到原来的玩法归属。如果玩家正在战斗,重连流程要先查会话目录,再向房间服确认该 session 是否仍在房间里,最后才建立新网关连接。若原网关还活着,新连接要通过 session fencing 让旧连接进入只读或直接断开;若原网关已失联,目录需要触发连接租约过期,而不是让两个网关同时代发消息。这个流程写起来麻烦,但它能避免“玩家重连后自己看见自己还活着,别人看见他已经离线”的分裂状态。
发布与扩容时的摘流
会话粘性最怕发布。没有摘流机制时,滚动发布看似成功,实际上每一台被停的网关都在制造短线。一个可用的方案是把网关状态分成 accepting、draining、closed。accepting 接收新连接;draining 不接新连接,但继续服务老连接,并主动把可迁移的玩家发迁移提示;closed 只保留极短时间的关闭握手。房间服也类似,长局可以等待自然结束,短局可以拒绝新房间,非关键玩法可以保存快照后迁移。摘流不是运维脚本的小细节,而是会话架构的一部分。
观测指标要能解释玩家体验
会话粘性的问题如果只看 CPU、内存、连接数,很容易误判。更应该观察 session create、renew、fence、handoff、expire、reconnect success、route miss、duplicate login 等指标。每条客户端关键请求都带 sessionId 和 routeVersion,日志里能从登录一路串到房间。告警也要贴近体验,比如五分钟内 route miss 超过阈值、重连成功率下降、draining 网关上连接滞留时间过长。这样事故发生时,团队知道是目录更新失败、房间确认失败,还是网关摘流不彻底。
落地检查清单
上线前可以用一组朴素但有效的检查压住风险:同一账号双端登录时旧连接是否被 fencing;网关进程 kill 后玩家能否在规定时间内重连;目录缓存丢失后是否能从权威存储恢复;发布摘流期间新玩家是否不会被分配到旧节点;房间服拒绝重连时客户端能否得到明确原因;会话过期任务是否有速率限制,避免清理任务把存储打满。会话粘性不是为了让玩家永远不掉线,而是让每一次掉线都可解释、可恢复、可度量。
工程落地表
| 关注点 | 推荐做法 | 常见风险 |
|---|---|---|
| 状态边界 | 明确权威服务、缓存副本和可恢复事实 | 把运行态散落在多个服务里,故障时无法判断谁说了算 |
| 版本控制 | 给协议、配置、策略和数据结构都记录版本 | 发布后新旧逻辑交错,排查时无法复现 |
| 失败补偿 | 每个跨服务步骤都设计超时、重试和幂等结果 | 成功路径能跑通,异常路径留下脏状态 |
| 观测指标 | 指标贴近玩家体验,同时保留技术细分维度 | 只有机器指标,事故发生时不知道玩家卡在哪 |
| 演练方式 | 用脚本制造重试、掉线、超时、重启和版本不一致 | 只在测试服点几次正常流程,线上第一次遇到边界 |
一个可执行的落地步骤
第一步,不急着重构所有代码,而是把 会话粘性 的关键事件和状态列出来,形成一张状态表。表里至少要有事件来源、状态 owner、是否可重试、是否需要持久化、失败后谁补偿。很多团队会在这一步发现,线上所谓的随机故障其实是状态没有 owner。
第二步,先在边界处加版本和审计。即使内部实现暂时没改,只要每次请求、每次状态转换、每次跨服务调用都能留下版本、原因和结果,后续迭代就有依据。不要等事故后再补日志,那时最关键的上下文已经丢了。
第三步,挑一条高价值路径做闭环,例如登录进房、领取奖励、切换场景或活动开启。闭环要包含成功、重复、超时、失败、回滚和人工处理。只要一条路径跑通,团队就能把模式复制到其他路径。
第四步,把演练自动化。会话粘性 的风险大多不会在正常点击里出现,而是在进程重启、网络抖动、配置切换、客户端重试、下游超时的组合里出现。自动化演练不需要一开始很复杂,能稳定复现三五个最危险场景,就已经比靠人工记忆可靠。
复盘问题清单
- 玩家在最差网络条件下,是否仍然能得到明确结果,而不是一直转圈?
- 服务重启或发布时,是否有清晰的进入、等待、迁移和退出策略?
- 重复请求、延迟响应和旧会话消息是否会污染新状态?
- 关键决策是否能通过日志复现,包括输入、版本、策略和输出?
- 如果下游服务短暂不可用,当前架构是保护玩家体验,还是把错误直接扩散到客户端?
- 运维或客服是否有安全的人工介入入口,还是只能直接改数据库?
在实际落地 会话粘性 时,团队还需要把责任边界写进代码和文档。第 1 个容易被忽略的点,是不要让临时判断散落在调用方。调用方只表达意图,平台层给出明确结果,业务层再根据结果决定是否继续。这样做看似多了一层接口,后续排查却非常省时间:日志能说明哪个版本的策略参与了决策,指标能看到哪个阶段开始变慢,回滚时也能只回滚策略而不是重启整组服务。对于游戏服务器来说,很多架构问题最终都会落到玩家体验上,稳定的边界比聪明的捷径更重要。
在实际落地 会话粘性 时,团队还需要把责任边界写进代码和文档。第 2 个容易被忽略的点,是不要让临时判断散落在调用方。调用方只表达意图,平台层给出明确结果,业务层再根据结果决定是否继续。这样做看似多了一层接口,后续排查却非常省时间:日志能说明哪个版本的策略参与了决策,指标能看到哪个阶段开始变慢,回滚时也能只回滚策略而不是重启整组服务。对于游戏服务器来说,很多架构问题最终都会落到玩家体验上,稳定的边界比聪明的捷径更重要。
在实际落地 会话粘性 时,团队还需要把责任边界写进代码和文档。第 3 个容易被忽略的点,是不要让临时判断散落在调用方。调用方只表达意图,平台层给出明确结果,业务层再根据结果决定是否继续。这样做看似多了一层接口,后续排查却非常省时间:日志能说明哪个版本的策略参与了决策,指标能看到哪个阶段开始变慢,回滚时也能只回滚策略而不是重启整组服务。对于游戏服务器来说,很多架构问题最终都会落到玩家体验上,稳定的边界比聪明的捷径更重要。
在实际落地 会话粘性 时,团队还需要把责任边界写进代码和文档。第 4 个容易被忽略的点,是不要让临时判断散落在调用方。调用方只表达意图,平台层给出明确结果,业务层再根据结果决定是否继续。这样做看似多了一层接口,后续排查却非常省时间:日志能说明哪个版本的策略参与了决策,指标能看到哪个阶段开始变慢,回滚时也能只回滚策略而不是重启整组服务。对于游戏服务器来说,很多架构问题最终都会落到玩家体验上,稳定的边界比聪明的捷径更重要。
在实际落地 会话粘性 时,团队还需要把责任边界写进代码和文档。第 5 个容易被忽略的点,是不要让临时判断散落在调用方。调用方只表达意图,平台层给出明确结果,业务层再根据结果决定是否继续。这样做看似多了一层接口,后续排查却非常省时间:日志能说明哪个版本的策略参与了决策,指标能看到哪个阶段开始变慢,回滚时也能只回滚策略而不是重启整组服务。对于游戏服务器来说,很多架构问题最终都会落到玩家体验上,稳定的边界比聪明的捷径更重要。
总结
游戏服务器会话粘性架构设计 的重点不在于堆更多组件,而在于把状态、时间、版本和失败路径讲清楚。游戏服务器的复杂度通常不是来自单个算法,而是来自玩家行为、网络环境、运营动作和服务故障同时发生。一个可信的架构,应该让正常路径足够顺,让异常路径有边界,让每一次自动处理和人工介入都有证据可查。做到这一点,系统即使不能避免所有问题,也能把问题限制在可理解、可恢复、可继续迭代的范围内。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。