返回博客

架构混合 Matchmaking:从 Call of Duty 的分队列机制看现代游戏 Matchmaking 架构

发布于 2026年7月24日
架构混合 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 架构都必须解决一个受限于三个互相冲突变量的约束优化问题:

  1. Latency(延迟/RTT): 玩家客户端与已分配的 Dedicated Server 实例之间的往返时间(以毫秒为单位)。
  2. Skill Delta(技能分差 $\Delta$MMR): 给定 Lobby 中玩家之间技能水平(MMR、Elo 或 TrueSkill)的数学差距。
  3. 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-1eu-central-1ap-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 组:

  1. Matchmaker 会联系 Dedicated Server 编排器(例如 Agones、自定义 Kubernetes 控制器)。
  2. 必须在严格的截止时间内(通常为 $< 1500ms$)在选定的数据中心内抢占或分配一个干净的游戏服务器实例。
  3. 服务器启动,绑定其监听的 UDP 端口,并返回其 IP/Port 地址 Payload。
  4. 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 文档 来加速你的后端开发流程。