Games104学习笔记(25)
GAMES104:网络游戏的进阶架构(下)
课程:GAMES104《现代游戏引擎:从入门到实践》
第 19 讲(下):网络游戏的进阶架构
本地视频时长:约 1 小时 24 分钟
一、本讲概览
上一部分讨论的是一局实时游戏内部的问题:如何平滑复制角色移动,以及如何在延迟存在时完成相对公平的命中判定。本部分把视角提升到大型多人在线游戏(MMO)的整体后台。
MMO 不只是“同时在线人数更多”。它通常还意味着:
- 世界长期存在,玩家离线后数据仍需保存。
- 登录、聊天、邮件、好友、公会、交易和排行榜等服务长期运行。
- 玩家和数据规模超过单台服务器的承载能力。
- 服务会扩容、缩容、更新、故障和迁移。
- 服务器必须限制每个客户端接收的数据量。
- 客户端运行在不可信设备上,需要系统化反作弊。
- 大世界需要跨机器分区,同时尽量保持移动和交互连续。
因此,本讲的主线可以概括为:
- 把 MMO 后台拆成边缘接入、玩法、社交和数据服务。
- 用分布式系统技术管理服务实例与数据分片。
- 通过压缩、更新策略和 AOI 控制网络带宽。
- 从程序、协议、行为和数据多个层面治理作弊。
- 通过分区、边界复制与多实例组合扩展游戏世界。
二、学习目标
- 理解 MMO 与普通房间制多人游戏在生命周期和系统规模上的差异。
- 掌握登录服务、网关、玩法服务、社交服务和数据层的基本职责。
- 理解关系型数据库与内存键值数据库在游戏后台中的不同用途。
- 认识分布式系统中的互斥、幂等、部分故障、一致性和事务问题。
- 掌握一致性哈希、虚拟节点、服务发现和健康检查的基本思想。
- 理解同步数据的量化、差量、频率和优先级优化。
- 掌握 AOI 的目的,以及空间网格和正交链表两类实现思路。
- 建立“客户端不可信、服务器做最终裁决”的反作弊边界。
- 理解程序加固、通信加密、服务端验证、行为检测各自解决的问题。
- 理解世界分区、无缝边界、活动实体与影子实体如何协同。
三、MMO 的核心特征
MMO 是 Massively Multiplayer Online Game,即大型多人在线游戏。它并不局限于传统 MMORPG;只要一个游戏需要长期在线服务、海量账号数据和大规模玩家交互,就会遇到类似的后台问题。
1. 持久世界
房间制游戏的一场对局结束后,战斗进程通常可以销毁;MMO 的角色、物品、货币、任务、公会和市场却需要长期保存。
持久化意味着服务端要处理:
- 玩家登录前后的状态恢复。
- 多个服务同时访问同一份玩家数据。
- 更新失败后的重试与回滚。
- 版本升级时的数据兼容和迁移。
- 物品与货币操作的原子性。
2. 复杂社会关系
玩家不只与战斗场景交互,还会形成好友、公会、队伍、邮件、聊天、排行榜和交易关系。它们的生命周期和扩展方式并不相同,不适合全部塞进一个游戏进程。
3. 大规模并发
单个场景可能只容纳有限玩家,但整个产品可能同时运行大量场景、副本和后台服务。系统的可扩展性因此来自“多个可组合服务和实例”,而不是追求一台无限强的服务器。
4. 经济系统
持久世界中的货币与物品会形成经济循环:
- Source:任务、掉落、奖励等资源来源。
- Sink:维修、手续费、消耗品等资源回收口。
若资源只产生不回收,会导致通货膨胀;若回收过强,则会压制玩家成长。交易系统不仅是功能模块,也关系到整个虚拟经济的稳定性和安全性。
四、MMO 后台的分层思路
大型后台通常不会由一个巨型进程包办全部职责,而是按变化频率、数据归属、负载模式和故障边界拆分。
| 层次 | 典型组件 | 主要职责 |
|---|---|---|
| 接入层 | 登录服务、网关 | 身份校验、连接维护、协议入口、内外网隔离 |
| 玩法层 | 世界服、场景服、副本服、匹配服 | 运行权威玩法逻辑和实时状态 |
| 社交层 | 聊天、好友、邮件、公会、排行榜 | 维护跨场景和长期社会关系 |
| 数据层 | 关系型数据库、内存数据库、缓存 | 持久化、查询、会话与热点数据 |
| 基础设施层 | 服务发现、配置、监控、日志 | 管理实例、健康状态和运行环境 |
拆分的价值不是“服务越多越先进”,而是让不同负载可以独立伸缩,并限制故障影响范围。过度拆分也会把本地函数调用变成不可靠的网络调用,增加部署、监控和一致性成本。
五、接入层:登录服务与网关
1. 登录服务
登录服务负责验证客户端身份。典型流程是:
- 客户端提交账号凭据或平台票据。
- 登录服务访问账号数据或第三方认证服务。
- 验证成功后签发短期会话令牌。
- 客户端携带令牌连接游戏网关。
- 网关验证令牌并建立玩家会话。
令牌应具有身份、有效期、签发方和防篡改信息。它不能永久有效,也不应直接暴露长期凭据。
2. 网关
网关是公网客户端与内部服务之间的重要边界,常见职责包括:
- 维持客户端长连接。
- 协议解析、路由和转发。
- 限流、封禁和异常流量过滤。
- 隐藏内部服务地址。
- 在后端实例切换时保持统一入口。
- 收集连接层指标和审计信息。
网关不宜承载大量核心玩法逻辑。否则它会从稳定的接入层变成难以扩展的业务瓶颈。
3. 为什么要隔离内外网
内部服务不应全部直接暴露给玩家。统一经过网关可以缩小攻击面,并让认证、限流和协议升级集中处理。即便客户端发现了内部消息格式,也不应具备直接访问数据库或任意服务实例的路径。
六、玩法与社会服务
1. 场景和副本服务
场景服务维护开放区域中的权威状态;副本服务运行生命周期较短、参与者较少的独立玩法实例。
两者的共同特点是:
- 对延迟敏感。
- 大量状态保存在内存中。
- 按固定逻辑帧推进。
- 玩家离开后只需持久化关键结果。
副本天然适合横向扩展:新对局创建新实例,结束后释放实例。开放世界则需要进一步解决空间分区和跨区交互。
2. 匹配服务
匹配服务根据段位、延迟、队伍规模、等待时间和玩法偏好组合玩家。它更关注队列、排序和约束,而不是实时世界模拟。
匹配条件越严格,质量可能越高,但等待时间也越长。因此实际系统通常随等待时间逐步放宽搜索范围。
3. 聊天、好友、邮件与公会
这些服务跨越场景和服务器实例:
- 聊天需要频道、屏蔽、审核和消息扇出。
- 好友需要关系图、在线状态和隐私控制。
- 邮件需要可靠存储、附件领取和防重复。
- 公会需要成员、权限、事件和共享资产管理。
把它们从场景进程中拆出后,即使玩家切换地图或副本,社会关系也能保持连续。
4. 交易系统
交易可能包括:
- 市场挂单与购买。
- 玩家间赠送物品或货币。
- 邮件附件。
- 真实货币购买虚拟物品。
交易操作不能只追求低延迟,更要保证不重复、不丢失和可审计。以购买为例,扣款和发货必须形成一个可恢复的业务过程;任何一步失败,都要能判断应该重试、补偿还是人工处理。
七、游戏数据存储
1. 关系型数据库
关系型数据库要求预先定义结构,并通过表、键和约束组织数据。它适合:
- 玩家账号和角色数据。
- 物品、背包和订单。
- 商店与交易记录。
- 需要复杂条件组合的查询。
- 强调一致性和事务的数据。
优点是结构明确、查询灵活、事务能力成熟;代价是磁盘访问和复杂事务可能成为高并发路径上的瓶颈。
2. 内存键值数据库
以 Redis 为代表的内存存储具有低延迟、键值访问和高效范围查询等特点,常用于:
- 会话管理。
- 匹配队列。
- 排行榜。
- 在线状态。
- 热点缓存。
- 为持久数据库削峰。
内存数据库很快,但“快”不等于“天然可靠”。仍需考虑持久化模式、副本、故障切换、缓存过期和数据源一致性。
3. 数据分级
可以按价值和时效划分数据:
| 数据类型 | 示例 | 主要要求 |
|---|---|---|
| 权威持久数据 | 付费订单、货币、稀有物品 | 强一致、审计、可恢复 |
| 运行时状态 | 角色位置、技能冷却、怪物状态 | 低延迟、频繁更新 |
| 会话数据 | 登录令牌、连接映射 | 短生命周期、快速访问 |
| 派生数据 | 排行榜缓存、统计结果 | 可重建、允许短暂延迟 |
不是所有数据都需要同一种数据库和一致性等级。正确做法是先确定数据丢失或短暂不一致的后果,再选择存储方案。
八、分布式系统为何困难
服务拆分后,本地程序中的许多隐含保证都会消失。
1. 数据访问互斥
多个实例可能同时修改同一角色、同一物品或同一公会。仅靠单进程锁无法协调跨机器访问,需要使用:
- 数据库条件更新。
- 乐观版本号。
- 分布式锁。
- 单写者或按键分片。
- 事件串行化。
锁不是目的;真正目标是定义数据所有权,并减少多处同时写入。
2. 幂等性
网络请求超时并不代表服务端没有执行。客户端或上游服务重试时,可能造成重复扣款、重复发奖或重复创建角色。
幂等操作满足:
$$ f(f(x)) = f(x) $$
业务系统常通过请求唯一标识、去重表、状态机和条件更新实现幂等。每个重试接口都应明确:重复请求会返回原结果,还是会再次产生副作用。
3. 部分故障
分布式调用可能出现:
- 请求未到达。
- 请求到达但响应丢失。
- 下游服务超时。
- 某个副本失效而其他副本仍正常。
- 网络分区导致双方都无法确认对方状态。
这意味着“失败”不再是一个简单布尔值。调用方必须设置超时、重试上限、退避、熔断和降级策略。
4. 一致性与共识
数据存在多个副本后,需要决定:
- 谁可以写。
- 哪个版本更新。
- 读请求是否允许看到旧值。
- 网络分区时优先可用性还是一致性。
- 故障恢复后如何合并状态。
实时位置可以容忍短暂不一致,但货币、订单和所有权通常需要更强保证。
5. 分布式事务
跨多个服务的操作无法依赖单机事务自动完成。例如市场购买可能同时涉及订单、买方货币、卖方收益、物品所有权和邮件。
常见思路是:
- 缩小强事务边界。
- 让关键资源由单一服务拥有。
- 使用业务状态机记录每一步。
- 通过消息和补偿动作实现最终一致。
- 为所有步骤保留可审计的唯一编号。
九、数据分片与一致性哈希
1. 普通取模的迁移问题
最简单的分片方式是:
$$ server(k) = h(k) \bmod N $$
其中 $h(k)$ 是玩家或数据键的哈希,$N$ 是服务器数量。
当 $N$ 改变时,大量键的余数都会变化,造成大规模迁移和缓存失效。服务频繁扩缩容时,这种扰动不可接受。
2. 哈希环
一致性哈希把哈希空间首尾相接成环。假设哈希值范围为:
$$ h(k) \in [0, 2^m - 1] $$
服务器节点和数据键都映射到环上。一个键通常归属于沿顺时针方向遇到的第一个服务器节点。
添加或移除节点时,主要只影响相邻区间,而不是重新映射所有键。在理想均匀分布下,新增一个节点时迁移比例约为:
$$ \frac{1}{N+1} $$
3. 虚拟节点
物理服务器数量有限时,哈希环上的节点可能分布不均,导致负载倾斜。可以让每台物理服务器拥有多个虚拟节点:
$$ v_{i,j} = h(server_i, j) $$
其中 $i$ 表示物理服务器,$j$ 表示该服务器的虚拟节点编号。
虚拟节点的作用包括:
- 让负载在环上分布得更均匀。
- 通过虚拟节点数量表达不同机器的权重。
- 故障时把负载分散给多台其他服务器。
- 迁移时以较细粒度重新分配数据。
一致性哈希解决的是“键如何稳定映射到节点”,并不自动解决数据复制、强一致、热点键或故障恢复。
十、服务发现与健康检查
服务实例的地址和数量会不断变化,调用方不应把它们永久写死在配置里。
1. 服务注册
实例启动后向服务发现系统注册:
- 服务名称。
- 网络地址和端口。
- 版本、区域和标签。
- 当前容量或权重。
实例关闭时主动注销;异常退出时则依赖租约或心跳超时清理。
2. 服务发现
网关或其他服务查询“当前有哪些可用实例”,再按轮询、权重、最少连接或一致性哈希选择目标。
发现模式可以是:
- 客户端发现:调用方获取实例列表并自行负载均衡。
- 服务端发现:调用方访问统一代理,由代理选择后端实例。
3. 健康检查
只注册不检查会把流量继续发给已经失效的实例。健康检查可分为:
- 存活检查:进程是否还在运行。
- 就绪检查:实例是否已经加载完成并能接收新请求。
- 深度检查:关键依赖是否正常。
若实例 B 的心跳超时,服务发现系统应把它标记为不健康,并让网关停止路由新请求。但检查过于敏感会造成实例频繁上下线;过于宽松又会延长故障时间。
十一、带宽预算
网络优化首先要有预算,而不是事后随意压缩。
若每次更新平均包含 $n$ 个实体,每个实体状态大小为 $s$ 字节,更新频率为 $f$,则单个客户端的下行带宽近似为:
$$ B \approx fns $$
在玩家数量和世界对象数量增长时,不能简单提高带宽。可控制的三个量分别是:
- $f$:降低或动态调整更新频率。
- $n$:通过 AOI 只发送相关实体。
- $s$:通过量化、差量和编码减少单个状态大小。
带宽优化本质上是在“精度、时效和相关性”之间分配预算。
十二、数据压缩与量化
1. 选择合适精度
游戏同步数据中包含大量位置、旋转和速度。并非所有数据都需要 32 位或 64 位浮点精度。例如人的奔跑速度通常不需要双精度表示。
若一个数值已知范围为 $[x_{\min},x_{\max}]$,使用 $b$ 位无符号整数编码,可量化为:
$$ q = \operatorname{round} \left( \frac{x-x_{\min}}{x_{\max}-x_{\min}} \cdot (2^b-1) \right) $$
解码近似为:
$$ \hat{x}
x_{\min} + \frac{q}{2^b-1} \cdot (x_{\max}-x_{\min}) $$
这里的关键不是盲目降低位数,而是根据实际值域和可接受误差选择精度。
2. 差量更新
完整快照中许多字段没有变化。可以只发送相对基准状态的变化:
$$ \Delta S_t = S_t - S_{base} $$
接收端需要知道差量所依赖的基准。若基准包丢失,则后续差量可能无法恢复,因此协议还需提供确认、基准切换或周期性完整状态。
3. 其他常见手段
- 布尔值按位打包。
- 枚举使用尽可能少的位。
- 小整数使用变长编码。
- 旋转使用四元数压缩。
- 静止对象不重复发送位置。
- 低优先级对象降低更新频率。
- 多条小消息合并,减少协议头开销。
压缩会消耗 CPU,并提高编解码和版本兼容复杂度,应以测量结果为依据。
十三、更新频率和优先级
所有对象都按相同频率同步会浪费带宽。优先级可以综合考虑:
- 与玩家的距离。
- 是否处于视野内。
- 对象是否正在移动。
- 是否参与战斗。
- 是否即将影响玩家。
- 状态距离上次发送变化了多少。
- 已经多久没有发送更新。
可把实体 $i$ 的发送评分抽象为:
$$ P_i
w_d D_i + w_v V_i + w_g G_i + w_t T_i $$
其中各项分别表示距离、可见性、Gameplay 重要性和等待时间等因素。网络系统在每帧预算内优先发送得分高的更新。
等待时间项很重要,否则低优先级对象可能长期饥饿,永远得不到刷新。
十四、AOI:只同步玩家关心的世界
AOI 是 Area of Interest,即兴趣区域。它回答的问题是:
对于当前玩家,哪些实体值得进入同步集合?
如果世界中有 $N$ 个玩家,每个人都接收其他所有人的状态,消息扇出接近 $O(N^2)$。大型世界必须根据空间或 Gameplay 关系过滤。
AOI 不一定等同于摄像机视锥。服务器可能还需要同步:
- 位于身后但会产生声音的敌人。
- 即将进入视野的高速物体。
- 与任务或队伍相关的远处角色。
- 具有全局影响的事件。
因此 AOI 是“相关性集合”,空间距离只是最常用的第一层过滤。
十五、AOI 的空间网格
1. 实体映射
将二维世界划分为规则网格。若网格尺寸为 $w \times h$,世界原点为 $(x_0,y_0)$,实体位置为 $(x,y)$,其格子坐标可写为:
$$ g_x = \left\lfloor \frac{x-x_0}{w} \right\rfloor $$
$$ g_y = \left\lfloor \frac{y-y_0}{h} \right\rfloor $$
每个格子维护其中的实体集合。玩家只需查询自己所在格子及周围格子,再做精确距离判断。
2. 候选格子
若 AOI 半径为 $r$,横纵格子尺寸分别为 $w$ 和 $h$,最多需要检查的格子数约为:
$$ \left(2\left\lceil\frac{r}{w}\right\rceil+1\right) \left(2\left\lceil\frac{r}{h}\right\rceil+1\right) $$
候选实体仍需通过精确距离测试:
$$ (x_i-x_p)^2 + (y_i-y_p)^2 \leq r^2 $$
比较平方距离可以避免不必要的开方。
3. 优缺点
优点:
- 实现简单。
- 查询速度稳定。
- 玩家 AOI 列表可以缓存。
- 适合实体分布较均匀的世界。
缺点:
- 格子太大会产生大量无关候选。
- 格子太小会增加跨格移动和邻格管理成本。
- 热点区域仍可能集中大量实体。
- 不规则地形和多层空间需要额外处理。
十六、AOI 的正交链表
正交链表思路是分别按 $x$ 坐标和 $y$ 坐标维护有序结构。查询时先在一个轴上找到范围,再用另一个轴过滤。
它的特点包括:
- 内存开销相对可控。
- AOI 半径可以因玩家而异。
- 附近对象查询不依赖固定格子大小。
但实体插入或坐标次序变化时需要维护有序关系。若对象经常长距离移动,调整成本会很高;朴素插入最坏可能达到 $O(n)$。
因此:
- 规则网格适合大量实体持续移动、空间尺度相对稳定的场景。
- 有序链表适合查询半径变化较多、实体移动相对连续的场景。
- 实际系统也可以使用四叉树、BVH、空间哈希或多级网格。
选择数据结构时应测量实体密度、移动分布和查询模式,而不是只比较理论复杂度。
十七、AOI 的进入与离开事件
AOI 不只是每帧计算一个集合,还要处理集合变化。
设上一时刻集合为 $A_{t-1}$,当前集合为 $A_t$:
$$ Enter = A_t \setminus A_{t-1} $$
$$ Leave = A_{t-1} \setminus A_t $$
$$ Stay = A_t \cap A_{t-1} $$
对应处理:
- Enter:发送实体创建和完整初始状态。
- Stay:发送增量更新。
- Leave:发送销毁、隐藏或休眠消息。
在 AOI 边缘反复移动会产生频繁进入和离开。可使用不同的进入半径和退出半径形成滞回区:
$$ r_{leave} > r_{enter} $$
这样可减少边界抖动。
十八、反作弊的基本边界
作弊会直接破坏公平性、经济系统和玩家信任。反作弊的首要原则是:
客户端负责输入和表现,服务器负责验证并决定永久结果。
客户端可以被修改、调试、注入、读取内存或模拟输入,因此不能把以下结果完全交给客户端决定:
- 伤害与命中。
- 移动是否合法。
- 物品生成与消耗。
- 货币变化。
- 冷却和射速。
- 随机掉落。
- 排名和胜负。
但服务器也不能重做客户端的一切。正确边界是让客户端只提交“意图”,服务器根据权威状态验证该意图是否可能。
十九、作弊手段与防护层次
| 层次 | 典型作弊 | 对应防护 |
|---|---|---|
| 客户端程序 | 修改代码、注入、调试、篡改资源 | 完整性校验、混淆、加壳、反调试 |
| 运行时内存 | 修改数值、读取隐藏信息 | 服务端权威、内存保护、最小化敏感数据 |
| 网络协议 | 伪造、重放、篡改或嗅探消息 | 认证、加密、序列号、时效性检查 |
| 输入设备 | 宏、自动瞄准、外部设备 | 行为统计、输入模式分析 |
| 外部视觉 | 截屏识别、AI 自动瞄准 | 服务端行为检测、回放分析 |
| 后台业务 | 刷奖励、复制物品、交易欺诈 | 幂等、事务、审计和风控 |
没有单一技术能够彻底消灭作弊。加固提高逆向成本,服务器验证限制作弊收益,检测系统负责发现异常,两者必须组合。
二十、客户端程序加固
1. 可执行文件加壳
加壳会压缩或混淆原始程序,并加入解包代码。运行时先执行解包器,再把原程序恢复到内存。
作用:
- 增加静态分析难度。
- 隐藏部分代码结构和字符串。
- 提高批量修改客户端的门槛。
局限:
- 程序最终仍要在玩家机器上运行,关键代码会出现在内存中。
- 加壳可能影响启动速度、崩溃分析和兼容性。
- 它只能增加成本,不能建立可信执行环境。
2. 混淆与完整性校验
可组合使用:
- 符号和控制流混淆。
- 关键代码或数据加密。
- 文件和内存区域哈希校验。
- 检测代码注入和异常模块。
- 检测调试器、Hook 和篡改。
这些措施需要谨慎控制误报,并避免对正常工具、辅助功能和系统软件造成不必要干扰。
二十一、网络通信安全
1. 混合加密
非对称加密便于安全交换密钥,但计算成本较高;对称加密适合大量数据传输。常见流程是:
- 客户端获得服务器公钥或通过可信证书验证服务器。
- 双方协商随机会话密钥。
- 后续数据使用对称密钥加密和认证。
这类组合同时利用:
- 非对称密码解决密钥分发和身份问题。
- 对称密码提供高吞吐的数据保护。
2. 加密不等于可信
加密可以保护传输中的机密性和完整性,但客户端本身仍可能主动发送“格式正确、内容恶意”的请求。因此服务器仍需验证:
- 请求者是否有权限。
- 序列号和时间是否合理。
- 操作是否满足当前状态机。
- 数值是否超过物理或规则上限。
- 同一个请求是否已经处理。
3. 防重放
可在消息中加入:
- 会话标识。
- 单调递增序列号。
- 时间戳和有效窗口。
- 随机数。
- 消息认证码。
服务器拒绝过期、重复或认证失败的消息。
二十二、作弊程序与行为检测
1. 已知作弊程序检测
客户端反作弊可扫描已知特征,例如:
- 可执行文件哈希。
- 窗口标题。
- 进程名。
- 已加载模块或驱动。
客户端把检测结果发送到服务器,服务器与特征库比较。此方法对已知作弊有效,但容易被改名、重打包或变种绕过,并存在隐私与误报风险。
2. 行为检测
未知外挂不一定具有已知文件特征,但可能产生异常行为:
- 反应时间长期接近机器极限。
- 瞄准轨迹突然吸附。
- 命中率和爆头率与同层玩家显著偏离。
- 移动速度、加速度或射速超过规则上限。
- 资源增长路径无法由合法事件解释。
行为检测不应只依赖单个阈值。应结合玩家水平、武器、地图、延迟和长期历史建立上下文,并保留回放或证据供复核。
3. 外部 AI 作弊
外部程序可以直接读取屏幕画面,识别目标,移动鼠标并射击:
- 跨平台。
- 不一定注入游戏进程。
- 不要求修改游戏代码。
- 传统文件完整性检测难以发现。
这说明反作弊不能只盯着客户端二进制文件,还要分析服务端可观察到的输入和行为模式。
二十三、服务器权威验证
服务端验证应遵循“便宜检查在前、昂贵检查在后”:
- 身份和会话是否合法。
- 消息格式、长度和频率是否合法。
- 序列号与时间窗口是否合法。
- 当前状态是否允许该操作。
- 资源、冷却和距离是否满足要求。
- 必要时运行碰撞、命中或业务规则检查。
例如移动验证可根据时间差 $\Delta t$ 和允许速度 $v_{\max}$ 限制位移:
$$ |\mathbf{p}t-\mathbf{p}{t-1}| \leq v_{\max}\Delta t + \epsilon $$
其中 $\epsilon$ 用于容纳网络抖动、浮点误差和合法技能位移。服务器不能只凭一次超限立即封禁,还应区分同步误差、Bug 和持续作弊。
二十四、世界分区
单台场景服务器的 CPU、内存和网络能力有限。扩展开放世界的基本办法是把空间划分为多个 Zone,每个 Zone 由一个或多个服务器负责。
1. 硬边界分区
玩家跨区时:
- 当前服务器保存或转交玩家状态。
- 目标服务器加载状态。
- 客户端切换连接或路由。
- 新区域开始同步。
硬边界实现简单,但可能出现读取界面、停顿或断开感。它适合地图、频道或副本之间本来就有明显过渡的游戏。
2. 无缝分区
无缝分区要求玩家在边界附近仍能看到并交互另一侧实体。课程给出的关键条件是边界带宽度至少覆盖最大 AOI 半径:
$$ w_{border} \geq r_{AOI}^{max} $$
这样一侧玩家进入边界带时,服务器有空间提前复制另一侧相关实体,而不是等玩家越界后才突然创建。
二十五、活动实体与影子实体
同一个边界实体可能同时出现在两个 Zone 中,但必须明确谁有权修改它。
- Active Entity:由所属 Zone 权威模拟,可执行 Gameplay 逻辑。
- Shadow Entity:在邻接 Zone 中的只读或受限副本,用于可见性、查询和过渡。
典型流程:
- 实体接近边界。
- 权威服务器把状态复制给邻区,创建影子实体。
- 邻区玩家能够提前看见并接收其更新。
- 实体真正越界时执行所有权移交。
- 新区成为 Active,旧区暂时保留 Shadow。
- 实体离开边界范围后删除旧副本。
所有权移交必须避免:
- 两个 Zone 同时模拟,造成重复伤害或重复掉落。
- 两边都不模拟,造成实体短暂冻结。
- 消息乱序让旧状态覆盖新状态。
- 玩家跨界时会话、技能和仇恨丢失。
可使用实体世代号、所有权纪元或迁移事务标识来区分新旧权威。
二十六、跨区交互
仅复制视觉状态不够。边界两侧的玩家还可能:
- 互相攻击。
- 产生碰撞。
- 触发技能范围效果。
- 交换仇恨与目标。
- 进入同一队伍或事件。
系统需要规定:
- 哪一侧执行判定。
- 如何把请求转发给权威服务器。
- 跨区结果怎样返回并复制。
- 所有权迁移期间如何处理新命令。
一种常见原则是:实体所属服务器负责最终状态修改,其他服务器只能提交请求或维护影子。跨区交互越频繁,服务器间通信成本越高,因此 Zone 边界最好放在玩家交互相对稀疏的区域,并支持动态调整。
二十七、复制、实例与组合扩展
世界扩展不只有空间切分,还可以组合多种手段:
- Zoning:不同服务器负责不同空间区域。
- Instancing:同一内容创建多个独立副本,分流玩家。
- Replication:为热点数据或只读世界状态创建副本。
- Sharding:把玩家分到多个相互独立的世界分片。
- Layering:同一地图显示不同玩家层,降低局部密度。
课程展示的组合方案中,不同 Zone 可以由不同服务器负责;热点区域可以建立多个实例或复制层;边界使用 Active/Shadow 实体保持连续。
组合设计必须回答三个问题:
- 玩家和实体由谁拥有?
- 玩家之间何时能够看见和交互?
- 负载变化时如何迁移而不破坏状态?
如果这三个问题没有清晰答案,架构图中的“多台服务器”并不会自动带来可扩展性。
二十八、热点与负载均衡
空间分区的隐含风险是玩家分布不均。大型活动、主城或世界 Boss 会让某个 Zone 突然成为热点。
可采用:
- 将热点内容实例化。
- 对只读或弱交互内容做复制。
- 动态改变 Zone 边界。
- 把非实时服务移出场景进程。
- 对 NPC、物理和更新频率降级。
- 限制区域容量并排队。
可定义服务器负载评分:
$$ L
\alpha L_{cpu} + \beta L_{mem} + \gamma L_{net} + \delta L_{entity} $$
负载均衡不能只看玩家数量。不同实体、技能、AI 和网络关系的成本差异很大。
动态迁移也有代价:传输状态、建立影子、暂停写入和切换路由都会消耗资源。触发阈值需要滞回,避免负载在两台服务器间来回震荡。
二十九、可观测性与运维
大型在线服务必须把故障和性能问题变成可观测数据。
1. 接入与服务指标
- 当前连接数和登录成功率。
- 网关收发带宽和消息频率。
- 各 RPC 的延迟、错误率和超时率。
- 实例心跳、重启和摘除次数。
- 队列长度与线程占用。
2. 数据指标
- 数据库查询延迟。
- 缓存命中率。
- 热点键与慢查询。
- 事务失败和补偿次数。
- 重复请求与幂等命中次数。
3. Gameplay 与网络指标
- 每个玩家的 AOI 实体数。
- 每类实体的更新频率和字节数。
- 完整快照与差量快照比例。
- Zone 玩家数、实体数和逻辑帧耗时。
- 跨区消息量、影子实体数和迁移耗时。
4. 安全指标
- 非法消息、限流和认证失败。
- 速度、射速与资源规则触发次数。
- 可疑客户端特征。
- 行为模型告警与人工复核结果。
- 封禁后的申诉和误报率。
日志应携带统一的玩家 ID、会话 ID、请求 ID 和实体 ID,才能把一次跨服务操作串联起来。
三十、常见错误与改进
1. 把所有逻辑放进场景服
结果:场景切换会影响聊天、好友和交易,任何模块故障都可能拖垮整个进程。
改进:按生命周期和负载特征拆分,但保留清晰的数据所有权。
2. 每次失败都立即无限重试
结果:下游故障时形成重试风暴,重复执行有副作用的操作。
改进:先保证幂等,再使用超时、有限重试、退避和熔断。
3. 取模分片后随意增减节点
结果:大量键重新映射,引发缓存失效和数据迁移。
改进:使用一致性哈希和虚拟节点,并补充复制与迁移协议。
4. 给所有玩家广播所有实体
结果:带宽和消息数量随人数快速增长。
改进:AOI 过滤、优先级调度、频率分级和状态压缩配合使用。
5. 只做客户端加固
结果:加固被绕过后,服务端仍接受伪造结果。
改进:客户端只提交意图,服务端验证规则并决定永久状态。
6. 认为加密可以防止作弊
结果:恶意客户端仍可使用合法密钥发送非法操作。
改进:加密保护传输,权威验证保护规则,两者职责不同。
7. 只按玩家数量划分 Zone
结果:大量 AI、技能和跨区交互仍可能让单个 Zone 过载。
改进:结合 CPU、网络、实体复杂度和跨区关系评估负载。
8. 在边界两侧都运行完整逻辑
结果:重复计算、状态冲突,甚至产生重复奖励。
改进:严格区分 Active 与 Shadow,所有权移交需要版本和原子切换语义。
三十一、整体设计原则
- MMO 的扩展基础是职责拆分、数据所有权和故障隔离。
- 网关是内外网边界,不是核心玩法逻辑容器。
- 持久数据、运行时数据和缓存数据应采用不同的一致性策略。
- 所有可能重试的写操作都要设计幂等性。
- 分布式故障通常是部分故障,超时不能直接等同于未执行。
- 一致性哈希减少节点变化时的重映射,但不替代复制和一致性协议。
- 服务发现必须与健康检查、负载均衡和摘除机制配套。
- 带宽优化同时控制更新频率、相关实体数量和单次状态大小。
- AOI 是相关性管理,不只是距离裁剪。
- 客户端永远不应成为永久状态的最终裁决者。
- 加固、加密、服务端验证和行为检测解决不同层次的问题。
- 无缝分区需要边界预复制和明确的实体所有权。
- 影子实体用于观察和过渡,活动实体才执行权威逻辑。
- 世界扩展通常是分区、实例、复制和分片的组合。
- 可扩展架构必须同时具备可观测性、审计和故障恢复能力。
三十二、复习要点
- MMO 的关键不是单场战斗人数,而是持久世界、长期服务和大规模数据。
- 登录服务负责认证,网关负责连接、路由和安全边界。
- 场景与副本服务偏实时;聊天、好友、公会和交易偏长期关系。
- 关系型数据库适合结构化持久数据,内存数据库适合热点、会话和快速排序。
- 分布式系统必须面对互斥、幂等、部分故障、一致性和跨服务事务。
- 一致性哈希通过哈希环减少扩缩容时的数据重映射,虚拟节点改善均衡。
- 服务发现维护动态实例列表,健康检查及时摘除不可用实例。
- 带宽近似由更新频率、相关实体数和单状态大小共同决定。
- 量化与差量减少单次数据大小,AOI 减少需要同步的实体数量。
- 空间网格简单稳定;正交链表支持变化半径,但不适合频繁长距离移动。
- AOI 集合应区分 Enter、Stay 和 Leave,并防止边缘反复抖动。
- 服务器只信任客户端输入意图,不信任客户端计算结果。
- 加壳和混淆只能提高逆向成本,不能替代服务器权威。
- 混合加密保护通信,但合法加密消息仍需业务验证。
- 已知特征检测对变种外挂有限,必须结合行为分析和审计证据。
- 无缝 Zone 的边界宽度应覆盖最大 AOI 半径。
- Active/Shadow 模型让跨区可见性与单一权威所有权同时成立。
- 热点扩展需要分区、实例、复制和动态负载管理的组合。