i007.cc

i007.cc

优先队列-降维打击

05.价值资料

大规模玩家匹配系统(Matchmaking)设计

匹配系统是游戏后端系统设计里最经典的题目之一,难点不在于单一技术,而在于多个目标之间的持续权衡。FC Mobile 这类全球运营的手游有自己的特殊挑战,值得深入拆解。


先定义”好的匹配”是什么

匹配系统本质上是一个多目标优化问题,三个核心目标天然存在张力:

技能公平性:双方实力接近,比赛有竞争性,玩家才有成就感。
网络质量:延迟低,对战体验流畅,尤其手游网络差异更大。
等待时间:玩家最多等 30-60 秒,超过就会流失。

三者不可兼得。玩家池小的时候(深夜、冷门段位),必须在公平性和等待时间之间做出取舍。好的匹配系统就是把这个取舍做得优雅。


评分系统设计

ELO 的原理与局限

ELO 是最基础的竞技评分系统:

期望胜率 E_A = 1 / (1 + 10^((R_B - R_A) / 400))

更新公式:
R_A' = R_A + K × (实际结果 - E_A)
  K = 变化系数,新玩家用 32,高分玩家用 16

 

ELO 的问题:只记录分数,不记录”分数的可信度”。一个打了 10 场的玩家和打了 1000 场的玩家,同样是 1500 分,可信度完全不同。

Glicko-2 / TrueSkill:加入不确定性

主流游戏用的都是 ELO 的改进版,核心是引入评分偏差(Rating Deviation, RD)

玩家评分 = (μ, σ)
  μ  = 技能均值(就是 ELO 里的分数)
  σ  = 标准差(越小说明评分越可信)

新玩家:μ=1500, σ=350(很不确定)
老玩家:μ=1500, σ=50 (很确定)

长时间未玩:σ 自动增大(技术可能退步了,需要重新校准)

 

实战 MMR 设计:商业游戏很少直接展示真实评分,而是把内部 μ 映射到玩家看到的”段位/星级”,原因是:内部评分频繁小数点波动会让玩家焦虑,而段位制让玩家有”升段”的成就感。


核心架构设计

玩家点击"匹配" 
    ↓
Match Request Service(接入层)
    ↓
Queue Manager(按区域 + 段位分桶的等待队列)
    ↓
Matching Engine(定期扫描队列,执行匹配算法)
    ↓
Session Allocator(分配游戏服务器)
    ↓
通知双方 → 进入游戏

 

Queue Manager:分桶存储

不能把所有玩家放进一个全局队列线性扫描,要按维度分桶:

Queue[region=CN][tier=Gold]    → [player1, player2, player5, ...]
Queue[region=CN][tier=Plat]    → [player3, player8, ...]
Queue[region=SEA][tier=Gold]   → [player4, ...]
Queue[region=CN][tier=Gold][mode=Ranked]  → ...

 

每个桶内的玩家按入队时间排序,优先匹配等待最久的。桶用 Redis Sorted Set 实现,score 是入队时间戳,天然有序且支持范围查询。

Matching Engine:定时 Tick 驱动

匹配引擎不是来一个玩家就立刻尝试匹配,而是定时批量处理(通常每 500ms 到 1s 跑一次),好处是一次扫描可以做全局最优分配,而不是贪心地让第一个来的玩家抢走最好的对手:

python
def matching_tick():
    for region in regions:
        for tier in tiers:
            candidates = queue.get_all(region, tier)
            pairs = find_optimal_pairs(candidates)
            for (p1, p2) in pairs:
                create_game_session(p1, p2)
                queue.remove(p1, p2)

 

匹配算法:多维度评分

给每对候选玩家打一个”匹配质量分”,选质量最高的组合:

python
def match_score(p1, p2) -> float:
    # MMR 差距:差距越小,分越高
    mmr_diff   = abs(p1.mmr - p2.mmr)
    mmr_score  = max(0, 1 - mmr_diff / MMR_TOLERANCE)

    # 网络延迟:预估或实测
    latency    = estimate_latency(p1.region, p2.region)
    lat_score  = max(0, 1 - latency / LATENCY_TOLERANCE)

    # 等待时间:等得越久,权重越高(下面详细说)
    wait_score = min(1.0, p1.wait_time / MAX_WAIT) * 0.5 \
               + min(1.0, p2.wait_time / MAX_WAIT) * 0.5

    # 加权组合
    return 0.5 * mmr_score + 0.3 * lat_score + 0.2 * wait_score

 


等待时间与公平性的动态平衡:条件松弛

这是匹配系统最核心的机制。初始设置严格条件,随着等待时间增加,逐步放宽标准:

等待 0-10s:  MMR 差 ≤ 100,延迟 ≤ 50ms(严格)
等待 10-30s: MMR 差 ≤ 200,延迟 ≤ 80ms
等待 30-60s: MMR 差 ≤ 400,延迟 ≤ 120ms
等待 > 60s:  MMR 差 ≤ 600,跨区域也可以,或者填入机器人

 

松弛不是线性的,可以是阶梯式或指数式,根据玩家活跃数据调参。关键原则:宁可让玩家等久一点,也不要让他打一场毫无悬念的比赛,但超过某个阈值后,等待本身就是最差的体验。


跨区域匹配

FC Mobile 是全球运营产品,不同地区玩家数量差异很大:

中国服  → 百万级 DAU,随时可以匹配
欧洲服  → 十万级,高峰期好,低谷期等待长
中东服  → 万级,经常需要跨区

 

分级路由策略

优先级1:同区域匹配(延迟最优)
    ↓ 超时 20s 未匹配
优先级2:邻近区域匹配(CN ↔ SEA / EU ↔ MENA)
    ↓ 超时 40s 未匹配  
优先级3:全球匹配 + 中转服务器(延迟补偿)
    ↓ 超时 60s
优先级4:填入 AI 对手,继续等真人加入

 

中转服务器(Relay Server):跨区对战时,双方都连接到地理上居中的中转节点,而不是直连。比如中国和欧洲玩家对战,可以走新加坡中转,各自延迟控制在可接受范围内。


组队匹配(Party Matching)

两人以上组队匹配会引入额外复杂度:

评分计算:组队 MMR 不是简单平均,要用”队伍中最高分”或”加权平均 + 惩罚系数”,防止高分玩家带低分小号碾压匹配池。

完整队伍 vs 混合队伍:5 人完整队伍只和另一个完整队伍匹配;2-4 人队伍可能和混合队伍对阵,但要保证双方组队人数对等,不能让 4 人组打散人。


冷启动与特殊玩家处理

新玩家定级赛:新账号没有历史数据,先打 5-10 场定级赛,σ 很大,快速收敛到真实水平。定级赛期间优先和其他新玩家或较低段位老玩家匹配,保护新玩家体验。

作弊玩家隔离:被风控系统标记的可疑玩家,悄悄放入”可疑池”,只和同样可疑的玩家匹配,正常玩家不受影响(Shadow Matching,延续上一话题里说的 Shadow Ban 思路)。

排位保护期:赛季初大量玩家段位重置,短期内队伍 MMR 和真实水平差距大,匹配系统需要设置过渡期策略,避免高分玩家和新号混在一起。


可扩展性设计

水平扩展:Matching Engine 按 region 分片,每个 region 独立运行,互不依赖。跨 region 匹配由独立的 Cross-Region Broker 负责协调。

队列持久化:玩家进入队列的状态要持久化到 Redis,Matching Engine 是无状态服务,挂掉重启后玩家不会丢失队列位置。

监控指标:匹配系统必须实时监控这几个指标:平均等待时间(P50/P95/P99)、匹配质量分分布、各段位队列深度、跨区域匹配比例。这些指标的异常往往是服务故障或玩家活跃度变化的第一信号。


面试里的答题框架

系统设计题回答匹配系统,建议按这个顺序推进:

先说需求拆解——功能需求(什么是好匹配)和非功能需求(QPS 量级、延迟要求);然后说核心数据结构(分桶队列);再说匹配算法(多维评分 + 条件松弛);最后说扩展性(分片、跨区、容灾)。

如果面试官追问”你们项目里怎么做的”,把你实际经历过的匹配系统挑一个最有亮点的决策展开说,哪怕只是其中一个维度,真实经验比理论描述有力得多。

发表回复