Концепт архитектуры сетевой части виртуального мира


Когда в мире одновременно находятся тысячи людей и ИИ-агентов, сеть быстро становится самым капризным местом системы. Чтобы не утонуть в этом, я разложил её на пять слоёв. Каждый слой решает одну задачу и почти ничего не знает про остальные.

Архитектура сетевой подсистемы по уровням

Правило простое: зависимости идут сверху вниз. Верхний слой знает только контракт соседа под собой, нижний о верхних не знает ничего. При приёме данные движутся против направления зависимостей.

Уровень Сущность Задача
5 Model Что клиент вправе знать и что ему разрешено менять
4 Managers Каждый отвечает за свой тип данных и кодирует свои сообщения
3 Dispatcher Что уйдёт первым, что подождёт, что устарело
2 Delivery Доставка, подтверждения, права и допуск
1 Transport Единый контракт «передай надёжно» или «передай самое свежее» и технология, которая реально отправляет байты

Level 5. World Model

Это модель мира. Здесь решается, что именно должен знать каждый клиент, а что ему знать не обязательно или нельзя. Любая команда игрока приходит сюда как просьба, а не как приказ: World Model держит власть над куском мира и принимает окончательное решение по каждой команде — права, роль, частота, допустимые значения. Последнее слово всегда за сервером, и именно поэтому мир трудно обмануть.

Зона интереса клиента считается в два этапа: сначала грубый отбор по пространственному индексу, потом точная проверка прав и ранжирование в пределах бюджета. Полного перебора «каждый клиент × каждая сущность» на каждом тике нет. Симуляция, физика и ECS живут за World Model и о сетевых деталях не знают.

Level 4. Managers

Менеджеры отвечают каждый за свой тип данных. Они не знают друг о друге и не обращаются к уровням 1 и 2. Данные о мире они читают через контракт, доступный только на чтение, — его реализует World Model. Каждый владеет схемой своих сообщений и кодирует данные по общим Wire Schemas, единым для всей системы.

  • Event Manager передаёт события в обе стороны: сработал триггер, кто-то что-то сделал. Надёжные события не теряются: при потере они отправляются повторно с тем же идентификатором, а на приёме отсеиваются дубли.
  • Input Manager занимается управлением. Клиент присылает не координаты, а то, что игрок нажал, а позицию считает сервер. В пакет идут последние K неподтверждённых команд — избыточность вместо повтора устаревшего. Так отклик остаётся быстрым, а подделать своё положение нельзя.
  • Dynamic Object Manager отвечает за динамические объекты: подвижные сущности вокруг игрока и их «призраков» — копии на его экране. Он отправляет только то, что изменилось, и в первую очередь то, что важнее.
  • Static Object Manager работает с неподвижным миром: зданиями, ландшафтом. Такие данные меняются редко, клиент подгружает их кусками по мере движения, и у каждого куска есть номер ревизии.
  • Observation Manager обслуживает тех, кто не играет, а наблюдает: ИИ-агентов и инструменты. Им нужны не отдельные обновления, а компактные сводки и пачки данных.
  • Durable Operation Manager отвечает за важные операции вроде покупки или передачи предмета. Они выполняются ровно один раз, даже если связь оборвалась посреди запроса. Результат всегда можно запросить по номеру операции.
  • Reference Data Manager занимается справочниками: описаниями предметов, параметрами и прочим. Данных много, они версионируются и меняются нечасто. Поэтому их загрузка идёт отдельно от игрового канала, как обычная загрузка файлов.

Level 3. Data Flow

Диспетчер потока данных. Менеджеры отдают ему сообщения в служебной оболочке, а он решает, что уйдёт первым, что подождёт, а что вообще устарело. В оболочке только служебные поля: класс доставки, приоритет, срок годности, ключ замены, метка «о чём сообщение», маршрут. Содержимое диспетчер не знает и не разбирает.

  • Очереди, приоритеты и срок годности — по классам данных. Данные разных менеджеров собираются в общие пакеты, поэтому клиента не заваливает мелочью.
  • Для данных, у которых важна только новейшая версия, новое сообщение вытесняет неотправленное старое.
  • Все очереди ограничены, и поведение при переполнении определено заранее: что-то заменяется, что-то останавливает источник, что-то отбрасывается первым.
  • Кредитная обратная связь. Диспетчер заранее выдаёт менеджерам лимит в байтах на ближайший тик и актуальный максимальный размер сообщения. Менеджер не кодирует то, что всё равно не уйдёт, а важное не отбрасывается вслепую. Остановка источника — крайняя мера, когда лимит уже исчерпан.
  • Сообщение всегда влезает в один пакет. Есть гарантированный минимум, текущий предел пути для конкретного соединения и предел для менеджеров за вычетом накладных расходов. Фрагментации на уровне IP архитектура не допускает: потеря одного фрагмента убивает всё сообщение целиком.

Level 2. Delivery

Здесь два компонента.

  • Connection Manager нумерует пакеты, следит, дошли ли они, и сообщает вверх, что произошло с отправленным. Подтверждение пакета означает только, что он дошёл до слоя доставки получателя, но не что команда применена. Заодно здесь проверяется допуск: синтаксис, размеры, квоты, частота — но окончательные права проверяет мир.
  • Session Manager помнит, кто подключён и какие у него права. Сессия живёт дольше соединения: если связь оборвалась, клиент может вернуться, в том числе на другой узел, и продолжить, а не начинать всё заново. При возобновлении поколение сессии увеличивается, и команды старого поколения отвергаются.

Level 1. Transport

Этот уровень состоит из двух частей: единого контракта и подключаемых технологий, которые его реализуют. Верхние слои видят только контракт.

Packet Transmitter

Единая точка для отправки и приёма данных. Верхние слои просят у неё «передай надёжно» или «передай самое свежее» и не думают о сокетах, адресах и пакетах. Контракт выражен в терминах сообщений и каналов: ненадёжное сообщение, надёжный канал с приоритетом, события соединения, состояние канала — полоса, задержка, уровень перегрузки, предельный размер сообщения. О мире и игре этот компонент ничего не знает. Обратных вызовов наверх нет: входящие события копятся в очереди, а вышестоящий слой вычитывает их пачками в своём ритме.

Transport Backends

Конкретная технология, которая реально отправляет байты по сети. Какую именно использовать, определяет конфигурация, а не код верхних слоёв. Это единственное место, где зависимость осознанно перевёрнута: бэкенды сами реализуют контракт Packet Transmitter, и именно поэтому транспорт заменяем.

  • QUIC (сейчас). Современный протокол поверх UDP. Он умеет шифрование, независимые потоки и ненадёжные сообщения, поэтому потерянный пакет не тормозит всё остальное.
  • DPDK (в планах). Приём и отправка пакетов прямо с сетевой карты, минуя ядро операционной системы. Это путь к максимальной скорости, но шифрование и защиту придётся делать самим.
  • Mock (для тестов). Имитация сети: потери, задержки, перестановки пакетов. На ней можно проверить, как система ведёт себя при плохой связи, не выходя из тестового окружения.

У бэкенда два набора настроек: для клиентов и для служебных соединений между узлами. Различаются они только параметрами, не кодом: наружу нужна проверка сервера и смена адреса клиента без переподключения, внутри — взаимная проверка сертификатов при каждом соединении. Свои возможности бэкенд сообщает наверх, поэтому запасной транспорт без надёжной доставки подключается без правок в остальной системе.

Где это живёт: Gateway и Simulator

Уровни 1–3 работают на Gateway — узле внешних соединений, который состоянием мира не владеет. Уровни 4–5 работают на Simulator — процессе авторитетной симуляции. Между ними протянут внутренний канал: тот же QUIC, тот же контракт, только профиль внутренний. Для менеджеров это выглядит так, будто диспетчер стоит рядом, поэтому кредиты, отчёты о доставке и ранжирование данных продолжают работать.

Архитектура сетевой подсистемы по сервисам

Зачем так:

  • Всплеск подключений или атака бьют по Gateway, а не по миру;
  • Сеть и симуляция масштабируются независимо;
  • Клиент держит одно соединение, даже если видит несколько частей мира;
  • Когда кусок мира переходит к другому серверу, клиент не переподключается — узел просто меняет маршрут.

Кто владеет каким куском мира, решает Directory — отдельный отказоустойчивый сервис. Он выдаёт право владения на время и следит за сменой владельца, а в обработке тиков не участвует: команда клиента его ответа не ждёт.

Что это даёт

  • Заменяемость. Транспорт можно поменять, не переписывая менеджеров и логику мира.
  • Тестируемость. Каждую границу между слоями можно подменить: время в тестах продвигается вручную, а сеть имитируется с потерями, задержками и перестановками. Плохая связь воспроизводится по сценарию.
  • Один клиент для всех. Человек и ИИ-агент подключаются одинаково, по одному коду. Различаются только права и лимиты, и они собраны в одном месте — профиле сессии.
  • Честный учёт канала. Вместо «отправь и посмотрим, что получится» — заранее известный лимит, поэтому работа не пропадает впустую и важное не тонет вместе с неважным.
  • Сервер всегда прав. Клиент просит, сервер решает.
  • Физика не ждёт сеть. Симуляция идёт в своём ритме, а сеть читает готовое состояние в своём. На всём пути состояния мира от физики до экрана нет чисел с плавающей запятой — только фиксированная точность, поэтому одинаковый ввод даёт одинаковый результат на любой платформе.