Назад к блогу

Разработка гибридного Matchmaking: как разделение очередей в Call of Duty раскрывает архитектуру современного Game Matchmaking

Опубликовано 24 июля 2026 г.
Разработка гибридного Matchmaking: как разделение очередей в Call of Duty раскрывает архитектуру современного Game Matchmaking

Коротко о главном

Разделение очередей матчмейкинга в Call of Duty: Black Ops 7 подсвечивает ключевые архитектурные вызовы современной Backend-инфраструктуры мультиплеерных игр. В статье анализируются компромиссы между SBMM и Connection-Based подходами, а также математика динамического расширения очередей. Рассматриваются практическая реализация алгоритма на C#, проблемы race conditions в распределенных пулах тикетов и методы оптимизации аллокации dedicated-серверов.

Когда Activision объявила, что в Call of Duty: Black Ops 7 пулы игроков будут разделены по трем отдельным очередям матчмейкинга — Skill-Based Matchmaking (SBMM), классической Connection-Based и гибридной — это вынесло давнюю дискуссию среди backend-инженеров на первый план. В соревновательных играх подбор игроков — это не просто геймдизайнерское решение, а сложная задача в рамках game matchmaking architecture, требующая баланса между миллисекундными сетевыми ограничениями, математической дисперсией скилла, фрагментацией пула игроков и затратами на облачные вычисления.

Разделение системы matchmaking на несколько независимых очередей на первый взгляд кажется простой фичей для удобства игроков. На практике же это в два или три раза увеличивает нагрузку на backend-инфраструктуру. При разделении текущего онлайна (CCU) по отдельным очередям плотность тикетов резко падает, время ожидания в очередях в регионах с низким онлайном растет по экспоненте, а алгоритмы аллокации серверов сталкиваются с повышенным churn.

В этой статье мы разберем технические компромиссы между SBMM и connection-first матчмейкингом, изучим математику динамического расширения гибридных очередей, разберем C#-код backend-сервиса для обработки тикетов и рассмотрим, как создавать отказоустойчивые пулы матчмейкинга, которые легко масштабируются.


Незыблемая трилемма матчмейкинга

Каждая современная архитектура game matchmaking должна решать задачу условной оптимизации, ограниченную тремя конкурирующими переменными:

  1. Задержка (RTT): Время задержки (round-trip time) между клиентом игрока и выделенным инстансом dedicated server (измеряется в миллисекундах).
  2. Дельта скилла ($\Delta$MMR): Математический разрыв в уровне мастерства (MMR, Elo или TrueSkill) между игроками в конкретном лобби.
  3. Время в очереди ($T_{queue}$): Суммарное время, которое игрок проводит в ожидании в очереди, пока валидный тикет на подбор не перейдет в активную аллокацию сервера.
                    Latency (RTT)
                      /       \
                     /         \
                    /   Ideal   \
                   /    Match    \
                  /               \
Skill Delta (ΔMMR) --------------- Queue Duration (T_queue)

Вы можете легко оптимизировать любые две из этих переменных за счет абсолютного ухудшения третьей:

  • Низкий RTT + низкая дельта скилла: Приводит к долгому ожиданию в очереди, так как движок должен искать редких игроков с идеальным скиллом, живущих рядом с тем же облачным дата-центром.
  • Быстрый подбор + низкая дельта скилла: Приводит к высокой задержке (RTT), так как matchmaker вынужден расширять географический радиус поиска по всему миру, чтобы найти оппонентов равного уровня.
  • Быстрый подбор + низкий RTT: Приводит к высокой разнице в скилле (классический опыт «connection-first» в публичных матчах), так как matchmaker мгновенно подбирает ближайших доступных клиентов независимо от их показателей эффективности.

Когда тайтл уровня Black Ops 7 вводит три отдельные очереди, подлежащая архитектура матчмейкинга вынуждена поддерживать три параллельных цикла вычисления правил над фрагментированными пулами памяти.

Если ваш онлайн (CCU) в конкретном регионе — например, в Южной Америке в 4 утра — падает ниже 2 000 активных игроков, разделение этих игроков на три пула снижает локальную плотность для каждой очереди всего до пары сотен человек. В результате connection-first очереди не могут найти dedicated server с низким пингом, а очереди SBMM застревают на неопределенный срок.


Разбор SBMM, Ping-First и гибридных очередей

Чтобы спроектировать backend, способный обрабатывать миллионы тикетов, необходимо сначала понять, как каждая из архитектурных моделей работает «под капотом».

1. Архитектура Connection-Based (Ping-First)

В connection-first движке матрицы скилла уходят на второй план или игнорируются вовсе. Главная цель — минимизировать сетевые деградации (джитер, потерю пакетов, высокий RTT).

  • Пинг-зондирование клиента: При входе в очередь игровой клиент отправляет ICMP- или UDP-пинг-маяки на серию региональных edge-шлюзов (например, us-east-1, eu-central-1, ap-southeast-1).
  • Генерация вектора пинга: Клиент формирует вектор задержек: [ us-east: 24ms, us-west: 78ms, eu-central: 142ms ] и прикрепляет его к пейлоаду матчмейкинга.
  • Пространственная индексация (Spatial Indexing): Matchmaker строго распределяет игроков по хэшам регионов на основе допустимых порогов задержки (например, $RTT < 50ms$).

Поскольку создание матча определяется сетевой топологией, бакеты поиска игроков предсказуемы, что позволяет резолвить тикеты за время $O(1)$, используя простые пространственные FIFO-очереди (First-In, First-Out).

2. Архитектура Skill-Based Matchmaking (SBMM)

SBMM отдает приоритет честности матча, моделируя навыки игроков с помощью многомерных гауссовских распределений (например, TrueSkill 2) или кастомных вариаций Elo. Ключевые метрики включают соотношение побед/поражений, K/D, урон в минуту и динамику последних игр.

  • Оценка расстояния: Matchmaker рассчитывает евклидово расстояние или расстояние Махаланобиса между векторами игроков в $N$-мерном пространстве навыков.
  • Затраты на сортировку: Matchmaker не может полагаться на простые FIFO-очереди. Ему необходимо поддерживать отсортированные множества или пространственные деревья (например, KD-деревья) профилей тикетов, чтобы быстро находить кандидатов в пределах допустимой дисперсии скилла $\sigma$.
  • Сжатие пространства поиска: По мере роста уровня мастерства (например, топ-0.5% игроков) пул подходящих кандидатов резко сокращается. Это вынуждает систему либо удерживать тикеты неограниченно долго, либо постепенно ослаблять параметры жесткости отбора по скиллу.

3. Динамическая гибридная архитектура

Вместо того чтобы заставлять игроков выбирать жестко заданные режимы очередей, современные продакшен-backend'ы часто внедряют динамическую гибридную модель. В этой конфигурации каждый тикет начинается со строгими ограничениями по SBMM и RTT. По мере роста $T_{queue}$ функция спада правил (rule decay function) непрерывно расширяет допустимую дельту скилла ($\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$ — время нахождения в очереди в секундах.

Используя динамическую релаксацию условий, вы предотвращаете бесконечные зависания в очереди, сохраняя высокое качество матчей в периоды пикового CCU.


Реализация кода: движок динамического расширения правил

Ниже представлена проверенная C#-реализация движка динамического расширения правил матчмейкинга. Этот сервис оценивает входящие тикеты относительно активных пулов очередей, рассчитывая матрицы совместимости пинга в реальном времени и динамические лимиты разброса 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;
        }
    }
}

Ключевые технические аспекты реализации:

  • Асимметричное удовлетворение правил: Метод проверяет, соблюдаются ли расширяющиеся ограничения обоих игроков (pingA <= rulesA.MaxAllowedPingMs && pingB <= rulesB.MaxAllowedPingMs). Игрок, пробывший в очереди 2 секунды, не будет втянут на сервер с пингом 150 мс только потому, что другой игрок ждет уже 90 секунд.
  • Дискретные шаги расширения пинга: Допустимый RTT расширяется с помощью дискретных временных интервалов (PingStepIntervalSeconds), а не непрерывных кривых. Это предотвращает постоянные переназначения edge-маршрутизаторов на каждом итерационном цикле.
  • Пересечение пространств: Подбор основан на пересечении ключей словаря региональных карт с выбором дата-центра с наименьшей суммарной задержкой.

Проблемы backend-инфраструктуры: конкурентность, Lock Contention и провижининг Dedicated Server

Написание алгоритмов подбора в изоляции — задача несложная. Настоящие инженерные сложности возникают, когда вы запускаете эту систему на кластерах распределенных нод, обрабатывающих сотни тысяч одновременных тикетов.

[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. Борьба за блокировки в распределенном пуле тикетов (Distributed Ticket Lock Contention)

Когда несколько параллельных процессов матчмейкера сканируют один и тот же центральный пул тикетов, неизбежно возникают состояния гонки (race conditions). Два разных воркера могут одновременно оценить тикет #1042 и попытаться объединить его в два совершенно разных лобби.

Чтобы решить эту проблему, инженеры должны выполнять атомарный захват блокировок с использованием распределенных примитивов (таких как Redis Lua-скрипты или атомарные операции Memory Grid) перед отправкой назначения на матч. Если тикет заблокирован другим воркером, поток немедленно освобождает состояние и выполняет откат.

2. Высокочастотное взаимодействие в реальном времени

Обновления статуса матчмейкинга не могут полагаться на стандартный HTTP-поллинг, так как это приводит к колоссальным расходам вычислительных ресурсов на бесполезные handshake-запросы. Чтобы информировать клиентов об ориентировочном времени ожидания и поиске по пингу, backend должен поддерживать постоянные дуплексные WebSockets или долгоживущие gRPC-стримы.

Если в вашей мультиплеерной архитектуре до сих пор используется неэффективный поллинг, ознакомьтесь с нашим руководством о том, как отказаться от HTTP polling в пользу Unreal Engine WebSockets в реальном времени.

3. Рукопожатие аллокации серверов и провижининг флита

Сформировать группу игроков — это только половина дела. Как только валидная группа тикетов создана:

  1. Matchmaker связывается с оркестратором dedicated-серверов (например, Agones, кастомные контроллеры Kubernetes).
  2. Свежий инстанс игрового сервера должен быть зарезервирован или выделен в выбранном дата-центре в жесткие сроки (обычно $< 1500ms$).
  3. Сервер поднимается, биндится на слушающий UDP-порт и возвращает IP/Port пейлоад.
  4. Matchmaker рассылает данные для подключения всем клиентам через WebSockets.

Ручная сборка распределенных пулов тикетов, обработка lock contention, управление региональными сокет-кластерами и оркестрация жизненного цикла dedicated-серверов требуют месяцев разработки инфраструктуры.

Именно здесь horizOn избавляет от огромных инженерных затрат. Вместо того чтобы собирать хранилища тикетов на Redis, писать кастомные обертки над Agones и скрипты автомасштабирования флита, horizOn предоставляет полностью управляемые очереди матчмейкинга с ультранизкой задержкой и оркестрацию серверов «из коробки». Вы пишете правила подбора, а horizOn берет на себя глобальное распределение, атомарную блокировку тикетов и автоматическую аллокацию серверов.


5 лучших практик создания современной архитектуры Game Matchmaking

Строите ли вы высококонкурентный шутер или инди-казуальную аркаду, следуйте этим проверенным архитектурным принципам:

1. Требуйте векторы пинга от клиента до отправки тикета

Никогда не полагайтесь на Geo-IP клиента для определения близости к вашим дата-центрам. Базы данных Geo-IP известны своей неточностью при edge-маршрутизации и не учитывают загруженность провайдеров в реальном времени. Всегда заставляйте игровой клиент измерять задержку через прямой UDP-пинг до всех региональных эндпоинтов до вызова эндпоинта вставания в очередь.

2. Защищайте систему от фрагментации пула

Избегайте создания отдельных очередей для второстепенных игровых режимов, если только ваш текущий онлайн явно этого не позволяет. Разделение аудитории по режимам, предпочтениям карт и жесткости SBMM усугубляет деградацию пулов. Если активный CCU на очередь в регионе падает ниже 1 000 игроков, автоматически переключайтесь на динамические фолбэк-режимы с единой очередью.

3. Разделите провижининг серверов и оценку матчей

Убедитесь, что потоки матчмейкера работают асинхронно от слоя оркестрации флита. Никогда не блокируйте рабочий цикл матчмейкера в ожидании загрузки виртуального сервера. Используйте неблокирующие очереди сообщений pub/sub для запроса аллокации серверов и передачи параметров подключения после того, как инстансы подтвердят статус готовности (healthy).

4. Оптимизируйте расходы на простаивающие сервера с помощью умной гибернации

Нагрузка на матчмейкинг скачкообразно растет в пиковые часы и резко падает в офф-пик. Простой сотен пустых инстансов игровых серверов впустую сжигает бюджет. Внедряйте паттерны динамического разогрева флита (warm-up) и гибернации серверов. Глубокий технический анализ оптимизации простаивающих серверов читайте в нашей статье о проектировании серверов с нулевыми потерями и анализе предложений по оптимизации серверов Fortnite.

5. Тестируйте неткод и репликацию при высокой задержке

Даже самая продвинутая архитектура матчмейкинга время от времени будет сводить игроков с умеренным пингом ($100-120ms$) в непиковые часы. Убедитесь, что сетевой код сервера использует клиентское предсказание (client prediction), компенсацию лагов (lag compensation) и сведение состояний (state reconciliation) для сглаживания сетевых задержек. Если позиции игроков «рвутся» или телепортируются при высоком пинге, ознакомьтесь с нашим руководством о том, как исправить рассинхронизацию позиций игроков в мультиплеере Unreal Engine.


Итоги и следующие шаги

Решение Activision предложить явное разделение на SBMM, Connection-First и гибридные очереди в Call of Duty: Black Ops 7 подчеркивает, насколько архитектура матчмейкинга важна для удержания игроков. Однако разделение очередей требует огромного онлайна, низколатентных сокетных сетей, алгоритмов спада правил и атомарной обработки тикетов.

Если вы разрабатываете следующий мультиплеерный хит, не тратьте месяцы на написание backend-инфраструктуры, сокет-серверов и логики аллокации флита с нуля. Узнайте, как horizOn помогает разработчикам развертывать масштабируемые мультиплеерные backend'ы, автоматический matchmaking и оркестрацию серверов за считанные минуты. Попробуйте horizOn бесплатно уже сегодня или изучите документацию horizOn, чтобы ускорить разработку вашего backend'а.


Источник: Call of Duty: Black Ops 7 will very soon offer three different matchmaking systems, and you'd be right to be confused