架构混合 Matchmaking:从 Call of Duty 的分队列机制看现代游戏 Matchmaking 架构
概要
本文深入剖析了《Call of Duty: Black Ops 7》拆分 Matchmaking 队列背后的后端架构挑战,对比了 SBMM、Ping-First 以及动态混合匹配模式的技术优劣。文章探讨了解决“延迟-技能差-排队时间”不可能三角的数学模型,并提供了处理动态规则扩展的 C# 代码实现。此外,针对高并发下的 Ticket 锁竞争与 Dedicated Server 调配等问题,本文给出了 5 个可落地的后端架构最佳实践。
当动视(Activision)宣布《Call of Duty: Black Ops 7》将把玩家群拆分到三个独立的 Matchmaking 队列(Skill-Based Matchmaking (SBMM)、经典 Connection-Based 模式以及 Hybrid 混合模式)时,这一决策直接将长期存在于后端工程领域的争议推到了前台。对于竞技类游戏而言,如何为玩家进行匹配不仅仅是游戏设计层面上的偏好,更是一个复杂的 Game Matchmaking 架构问题,它必须在亚毫秒级网络约束、数学上的技能方差(Skill Variance)、匹配池碎片化(Pool Fragmentation)以及云端计算成本之间取得平衡。
从表面上看,将 Matchmaking 系统拆分为多个独立队列似乎只是一个简单的“满足玩家偏好”的功能。但实际上,这会让后端基础设施的工程开销翻倍甚至翻三倍。当你将同时在线玩家数(CCU)切分到不同的独立队列中时,Ticket 密度会急剧下降,低密度地区的排队时间会呈指数级暴涨,服务器分配算法也会面临更剧烈的抖动。
在本文中,我们将深入分析 SBMM 与 Connection-First(延迟优先)Matchmaking 的技术权衡,剖析动态混合队列扩展背后的数学原理,审查用于处理 Ticket 的真实后端 C# 代码,并探讨如何构建能够顺畅扩缩容的高可用 Matchmaking 池。
无法回避的 Matchmaking 不可能三角
任何现代游戏 Matchmaking 架构都必须解决一个受限于三个互相冲突变量的约束优化问题:
- Latency(延迟/RTT): 玩家客户端与已分配的 Dedicated Server 实例之间的往返时间(以毫秒为单位)。
- Skill Delta(技能分差 $\Delta$MMR): 给定 Lobby 中玩家之间技能水平(MMR、Elo 或 TrueSkill)的数学差距。
- Queue Duration(排队时长 $T_{queue}$): 玩家在有效的 Matchmaking Ticket 转变为活跃的 Server Allocation 之前,处于空闲排队等待状态的总时间。
Latency (RTT)
/ \
/ \
/ Ideal \
/ Match \
/ \
Skill Delta (ΔMMR) --------------- Queue Duration (T_queue)
你可以轻松针对其中任意两个变量进行极限优化,但代价是彻底牺牲第三个变量:
- 低 Latency + 低 Skill Delta: 会导致排队时间变长,因为匹配引擎必须寻找极其罕见的、既具备完美匹配技能水平,又恰好位于同一个云数据中心附近的玩家。
- 低 Queue Time + 低 Skill Delta: 会导致高 Latency,因为 Matchmaker 必须在全球范围内扩大地理搜索半径,才能找到技能相当的对手。
- 低 Queue Time + 低 Latency: 会导致极高的技能方差(即经典的“Connection-First”公开匹配体验),因为 Matchmaker 会立即抓取距离最近的可用客户端,而完全忽略性能指标。
当像《Black Ops 7》这样的作品引入三个独立的队列模式时,它会迫使底层 Matchmaking 架构对碎片化的内存池维护三个平行的规则评估循环。
如果某个特定地区(例如凌晨 4 点的南美)的 Concurrent Users(CCU)降至 2,000 名活跃玩家以下,将这些玩家切分到三个独立的匹配池中,会导致每个队列的本地密度骤降至仅几百人。结果就是,Connection-First 队列无法找到低 Ping 的 Dedicated Server,而 SBMM 队列则停滞不前。
解构 SBMM vs. Ping-First vs. Hybrid 队列
要构建一个能够处理数百万 Matchmaking Ticket 的后端,你首先必须理解每种架构模型在底层是如何运行的。
1. Connection-Based(Ping-First)架构
在 Connection-First 引擎中,技能矩阵完全退居其次甚至被直接忽略。核心目标是最大程度减少网络质量劣化的影响(如 Jitter、Packet Loss、高 RTT)。
- 客户端 Ping 探测: 打开队列后,游戏客户端会向一系列区域边缘网关(如
us-east-1、eu-central-1、ap-southeast-1)发送 ICMP 或 UDP Ping 探针。 - Ping 向量生成: 客户端构建一个延迟向量:
[ us-east: 24ms, us-west: 78ms, eu-central: 142ms ]并将其附加到 Matchmaking Payload 中。 - 空间索引(Spatial Indexing): Matchmaker 严格根据可接受的延迟阈值(例如 $RTT < 50ms$)将玩家分桶到区域 Hash 中。
由于网络拓扑结构决定了比赛创建,玩家搜索桶是可预测的,这使得 Ticket 可以使用简单的 FIFO(先进先出)空间队列在 $O(1)$ 时间内完成解析。
2. Skill-Based Matchmaking(SBMM)架构
SBMM 通过使用多维高斯分布(例如 TrueSkill 2)或定制的 Elo 变体对玩家能力建模,从而优先保证比赛的公平性。核心输入包括胜/负率、击杀/死亡比(KD)、每分钟伤害量以及最近的性能轨迹。
- 距离评估: Matchmaker 在 $N$ 维技能空间中计算候选玩家向量之间的欧几里得距离或马氏距离(Mahalanobis distance)。
- 排序成本: Matchmaker 不能依赖简单的 FIFO 队列。它们必须维护 Ticket Profile 的有序集合或空间树(如 KD-trees),以便在可接受的技能方差 $\sigma$ 内快速找到候选对象。
- 搜索空间收缩: 随着技能水平的提升(例如前 0.5% 的玩家),符合条件的候选池会剧烈收缩。这迫使系统要么无限期保持 Ticket,要么缓慢放宽其技能严格度参数。
3. Dynamic Hybrid(动态混合)架构
现代生产环境后端通常会实现 Dynamic Hybrid 模型,而不是将玩家强行限制在硬编码的队列选择中。在这种设置中,每个 Matchmaking Ticket 在开始时都有严格的 SBMM 和低延迟限制。随着 $T_{queue}$ 的增加,规则衰减函数会持续扩展可接受的技能分差($\Delta MMR$)和延迟上限($RTT_{max}$)……
$$\Delta MMR_{allowed}(t) = \Delta MMR_{base} + \alpha \cdot t^{\gamma}$$
$$RTT_{allowed}(t) = \min\left(RTT_{max_cap}, RTT_{base} + \beta \cdot \lfloor t / \Delta t_{step} \rfloor\right)$$
其中 $\alpha$ 和 $\beta$ 为扩展系数,$\gamma$ 控制指数曲线的陡峭程度,$t$ 为已排队的时间(秒)。
通过利用动态放宽(Dynamic Relaxation)机制,你可以防止队列发生无限期卡顿,同时在 CCU 高峰期保持较高的比赛质量。
代码实现:动态规则扩展引擎
下面是一个经受过实战检验的 C# 动态 Matchmaking 规则扩展引擎实现。该服务负责针对活跃匹配池评估传入的 Ticket,实时计算 Ping 兼容性矩阵以及动态 MMR 方差上限。
using System;
using System.Collections.Generic;
using System.Linq;
namespace Horizon.Matchmaking.Engine
{
public class MatchmakingTicket
{
public string TicketId { get; set; } = Guid.NewGuid().ToString();
public string PlayerId { get; set; }
public double SkillRating { get; set; } // Elo/MMR representation
public Dictionary<string, int> RegionalPingMap { get; set; } = new(); // e.g., "us-east": 28
public DateTime EnqueuedAtUtc { get; set; }
}
public class DynamicMatchRules
{
public double MaxAllowedMmrDelta { get; set; }
public int MaxAllowedPingMs { get; set; }
}
public class MatchmakingEvaluator
{
private const double BaseMmrDelta = 50.0;
private const double MaxMmrCap = 600.0;
private const int BasePingMs = 35;
private const int AbsoluteMaxPingMs = 180;
private const double PingStepIntervalSeconds = 4.0;
/// <summary>
/// Calculates the expanded criteria for a ticket based on elapsed queue time.
/// </summary>
public DynamicMatchRules GetRelaxedRules(MatchmakingTicket ticket, DateTime currentUtc)
{
double elapsedTime = (currentUtc - ticket.EnqueuedAtUtc).TotalSeconds;
// Exponential expansion for skill tolerance to ensure high-skill players eventually match
double mmrExpansion = Math.Pow(elapsedTime, 1.35) * 4.5;
double calculatedMmrDelta = Math.Min(MaxMmrCap, BaseMmrDelta + mmrExpansion);
// Step-wise discrete expansion for latency tolerance (prevents constant server hopping)
int pingSteps = (int)Math.Floor(elapsedTime / PingStepIntervalSeconds);
int calculatedPingLimit = Math.Min(AbsoluteMaxPingMs, BasePingMs + (pingSteps * 15));
return new DynamicMatchRules
{
MaxAllowedMmrDelta = calculatedMmrDelta,
MaxAllowedPingMs = calculatedPingLimit
};
}
/// <summary>
/// Evaluates whether two tickets can be paired into a match session.
/// </summary>
public bool CanMatchTickets(MatchmakingTicket ticketA, MatchmakingTicket ticketB, DateTime currentUtc, out string selectedRegion)
{
selectedRegion = null;
DynamicMatchRules rulesA = GetRelaxedRules(ticketA, currentUtc);
DynamicMatchRules rulesB = GetRelaxedRules(ticketB, currentUtc);
// 1. Evaluate Skill Delta
double actualMmrDelta = Math.Abs(ticketA.SkillRating - ticketB.SkillRating);
if (actualMmrDelta > rulesA.MaxAllowedMmrDelta || actualMmrDelta > rulesB.MaxAllowedMmrDelta)
{
return false; // Skill variance too wide for current queue age
}
// 2. Evaluate Common Datacenter Ping Compatibility
int lowestCombinedPing = int.MaxValue;
foreach (var (region, pingA) in ticketA.RegionalPingMap)
{
if (ticketB.RegionalPingMap.TryGetValue(region, out int pingB))
{
// Match must satisfy BOTH players' dynamic ping limits
if (pingA <= rulesA.MaxAllowedPingMs && pingB <= rulesB.MaxAllowedPingMs)
{
int combinedPing = pingA + pingB;
if (combinedPing < lowestCombinedPing)
{
lowestCombinedPing = combinedPing;
selectedRegion = region;
}
}
}
}
return selectedRegion != null;
}
}
}
该实现的核心技术亮点:
- 不对称规则满足(Asymmetric Rule Satisfaction): 该方法会检查 两个 玩家不断扩展的约束是否都得到了满足(
pingA <= rulesA.MaxAllowedPingMs && pingB <= rulesB.MaxAllowedPingMs)。仅仅因为另一个玩家已经排队等待了 90 秒,刚排队 2 秒的新玩家不会被强行拉入 150ms 延迟的服务器中。 - 离散 Ping 阶梯扩展(Discrete Ping Stepping): 延迟上限使用离散时间步长(
PingStepIntervalSeconds)扩展,而不是连续曲线。这避免了在每个 Tick 循环中不必要地重新分配边缘路由器。 - 空间交集(Spatial Intersection): 配对依赖于跨区域映射的字典 Key 交集,选择具有最低累计往返延迟的数据中心。
后端基础设施挑战:并发、锁竞争与 Dedicated Server 调配
独立编写匹配算法并不复杂。真正的工程难点在于当系统运行在处理几十万并发 Ticket 的分布式节点集群上时。
[Player Clients]
│ (WebSockets / Low Latency)
▼
[Ingress Load Balancers]
│
▼
[Distributed Ticket Pool (e.g., Redis Cluster / Memory Grid)]
│
┌────┴────────────────────────┬────────────────────────┐
▼ ▼ ▼
[Worker Node 1] [Worker Node 2] [Worker Node 3]
│ │ │
└────┬────────────────────────┴────────────────────────┘
│ (Atomic Claim / Lua Mutex Lock)
▼
[Server Orchestration API] ──► Spin up Agones / Fleet Instances
1. 分布式 Ticket 锁竞争(Distributed Ticket Lock Contention)
当多个平行的 Matchmaker 进程扫描同一个中央 Ticket 池时,竞争条件(Race Conditions)不可避免地会发生。两个独立的 Worker 线程可能会同时评估 Ticket #1042,并试图将其配对到两个完全不同的 Lobby 中。
为了解决这个问题,开发者必须在发出比赛分配之前,利用分布式原语(如 Redis Lua 脚本或内存网格原子操作)执行原子锁抢占(Atomic Lock Claim)。如果 Ticket 已被另一个 Match Worker 锁定,该线程会立即释放其状态并回溯。
2. 高频实时通信
Matchmaking 状态更新不能依赖标准的 HTTP 轮询(Polling),否则会在无意义的连接握手上消耗巨大的计算资源。为了让客户端实时了解排队时长预估和动态 Ping 搜索状态,后端必须维持持久的、双向通信的 WebSockets 或长连接 gRPC 流。
如果你目前在多重玩家状态处理中仍在依赖低效的轮询循环,请参阅我们的指南:在实时后端中放弃 HTTP 轮询,转向 Unreal Engine WebSockets。
3. 服务器分配握手与 Fleet 调配
匹配玩家只是完成了一半的工作。一旦形成了一个有效的 Ticket 组:
- Matchmaker 会联系 Dedicated Server 编排器(例如 Agones、自定义 Kubernetes 控制器)。
- 必须在严格的截止时间内(通常为 $< 1500ms$)在选定的数据中心内抢占或分配一个干净的游戏服务器实例。
- 服务器启动,绑定其监听的 UDP 端口,并返回其 IP/Port 地址 Payload。
- Matchmaker 将连接信息分发给所有客户端 WebSockets。
手动构建分布式 Ticket 池、处理锁竞争、管理基于区域的 Socket 集群以及编排 Dedicated Server 生命周期需要耗费数月的工程精力。
这正是 horizOn 能够消除巨额工程开销的地方。无需手动拼凑 Redis Ticket 存储、编写自定义 Agones 集群封装,或是管理 Server Fleet 缩容脚本,horizOn 开箱即用提供完全托管的、极低延迟的 Matchmaking 队列以及 Server Fleet 编排服务。你只需编写匹配规则集;horizOn 会无缝处理全局分发、原子 Ticket 锁和自动化服务器调配。
构建现代游戏 Matchmaking 架构的 5 个最佳实践
无论你是在构建一款高规格的竞技射击游戏,还是一款独立休闲街机作品,请遵循以下经受过实战检验的架构原则:
1. 提交 Ticket 前必须获取客户端 Ping 向量
切勿依赖客户端 IP 地理位置(Geo-IP)查找来确定与数据中心的距离。Geo-IP 数据库在边缘路由方面精准度极低,且无法反映实时的 ISP 拥堵情况。务必强制客户端引擎在调用入队 Endpoint 之前,通过对所有区域 Endpoint 发送 UDP Ping 探针来测量直接延迟。
2. 警惕匹配池碎片化(Pool Fragmentation)
除非你的活跃玩家基数足够庞大,否则应避免为次要游戏模式创建单独的队列。将玩家群分散在不同的游戏模式、地图偏好和匹配严格度队列中,会加剧匹配池劣化。如果某个区域内每个队列的活跃 CCU 降至 1,000 名玩家以下,应自动切换到动态单队列的回退模式。
3. 解耦服务器调配与匹配评估
确保 Matchmaker 线程与 Fleet 编排层异步运行。绝不要在等待虚拟服务器实例启动时保持活跃的 Matchmaker Worker 循环处于阻塞状态。利用非阻塞 Pub/Sub 消息队列来请求服务器分配,并在实例上报健康状态后投递连接 Payload。
4. 通过智能休眠优化空闲服务器成本
Matchmaking 在玩家高峰期会无预警剧增,而在非高峰期则会急剧下降。保留数百个空闲的游戏服务器实例会浪费大量的运营开销。应实现动态 Fleet 预热和服务器休眠模式。关于优化空闲服务器使用的深度技术分析,请阅读我们的文章:构建零浪费服务器:Fortnite 服务器优化休眠方案分析。
5. 在高延迟环境下基准测试 Netcode 与同步
即使是最先进的 Matchmaking 架构,在非高峰时段偶尔也会将处于中等延迟差距($100-120ms$)的玩家匹配在一起。确保你的服务器 Netcode 采用了严格的客户端预测(Client Prediction)、延迟补偿(Lag Compensation)和状态重调和(State Reconciliation),以顺畅应对数据包延迟。如果客户端位置在高 Ping 比赛中出现画面撕裂或拉回,请参考我们的指南:如何修复 Unreal Engine 多人游戏中的玩家位置不同步。
总结与后续步骤
动视在《Call of Duty: Black Ops 7》中提供明确的 SBMM、Connection-First 和 Hybrid 队列的决定,充分说明了 Matchmaking 架构对玩家满意度的重要性。然而,拆分队列需要庞大的玩家密度、低延迟 Socket 网络、动态规则衰减算法以及原子级 Ticket 处理能力。
如果你正在构建下一款多人游戏,不要浪费数月时间从头编写后端基础设施、Socket 服务器和 Fleet 分配逻辑。了解 horizOn 如何助力开发者在数分钟内部署可扩展的多人游戏后端、自动化 Matchmaking 以及 Server 编排。立即免费试用 horizOn 或查阅 horizOn 文档 来加速你的后端开发流程。