METABYTE
К списку статей

Лучший бэкенд для realtime браузерной игры: стек и архитектура

Практичный разбор стеков для realtime мультиплеерной браузерной игры: протоколы, архитектура, языки, масштабирование и стоимость — без лишнего шума, с продакшн-нюансами.

19 мая 202612 мин чтенияAI-research draft
Лучший бэкенд для realtime браузерной игры: стек и архитектура

Если промахнуться с бэкендом для realtime мультиплеера, вы платите дважды: деньгами за железо, которое не тянет пиковые тики, и репутацией — лаги, десинхрон, абьюзеры. Исправлять потом в три раза дороже, чем построить правильно изначально.

Коротко: лучший бэкенд для браузерной multiplayer-игры — авторитетный игровой сервер (authoritative), транспорт WebSocket или WebRTC DataChannel, тики 20–30 Гц (для большинства жанров), синхронизация state через дельты, Redis для матчмейкинга и эфемерного состояния, Postgres для метаданных/платежей, горизонтальное масштабирование по комнатам. По языку: Node.js для скорости запуска, Go — для предсказуемой производительности, Rust — для максимальной эффективности, Elixir/BEAM — для сверхконкурентных задач. Дальше — подробности, архитектура и деньги.

Что значит «лучший бэкенд» для браузерного realtime

«Лучший» — это сочетание шести ограничений:

  • Задержка: целим 50–120 мс RTT по региону. Всё выше — требует агрессивной интерполяции/предсказаний.
  • Пропускная способность: 10–50 КБ/с на игрока в среднем для action-жанров при 20–30 Гц и дельта-компрессии.
  • Стабильность тиков: джиттер сервера важнее среднего CPU. 1–2 пропущенных тика подряд — заметный рывок.
  • Масштабирование: тысячи комнат, устойчивое распределение нагрузки, отказоустойчивость.
  • Безопасность: сервер — авторитет. Клиент не решает исход коллизий, урона, лута. Анти-чит — на стороне сервера.
  • Стоимость: железо и трафик съедают бюджет гораздо быстрее, чем база данных.

Для браузера мы живём в реальности HTTP(S), WebSocket и WebRTC DataChannel. Чистого UDP у клиента нет, значит всё пронизано надёжными протоколами поверх UDP/TCP с особенностями NAT и шифрования.

Архитектура по умолчанию для 90% кейсов

В большинстве проектов мы рекомендуем такую схему:

  • Cloudflare (или NGINX+HAProxy) — TLS termination, WebSocket proxy, DDoS mitigation.
  • API/Backend (REST/GraphQL) — логин, инвентарь, матчмейкинг, платежи (Stripe), хранение профилей (Postgres).
  • Matchmaker — Redis Streams/Sorted Sets для очередей, NATS для событий.
  • Room Server — авторитетная игровая логика: ECS/physics/ability-cooldowns; протокол WebSocket или WebRTC DC.
  • State Sync — дельты + снапшоты, бинарный протокол (например, FlatBuffers/Protocol Buffers/MessagePack).
  • Storage — Postgres (персистент), Redis (эфемерный state/leaderboards), объектное хранилище (реплеи/ассеты).
  • Observability — Prometheus + Grafana, Sentry, p99 latency трейсинг (OpenTelemetry), логирование (Loki/Elastic).

Поток:

  1. Клиент авторизуется и получает токен комнаты.
  2. Обращается к матчмейкеру → получает адрес ближайшего регионального Room Server.
  3. Устанавливает WebSocket/WebRTC соединение.
  4. Сервер запускает луп тиков (например, 30 Гц), принимает инпуты, обновляет мир, рассылает дельты состояния.
  5. По окончании матча метрики и результаты уезжают в Postgres; быстрозаводимые счётчики — в Redis.

Если вы ещё выбираете движок для рендера на клиенте, посмотрите наш разбор сравнение Three.js/Babylon.js/Pixi.js — это влияет на частоту апдейтов и формат пакетов.

Протокол: WebSocket vs WebRTC (и немного про WebTransport)

Сводка по транспорту для браузера:

ТранспортПлюсыМинусыКогда брать
WebSocketПростота, обратная совместимость, дешёвый прокси через Cloudflare, зрелые библиотекиTCP head-of-line blocking, нет P2P, всё через серверБольшинство PvP/PvE матчей до 50–100 игроков на комнату
WebRTC DataChannelUDP под капотом, частично надёжные каналы, P2P возможен, хорош для голосового чатаСложность: STUN/TURN, ICE, отладка; TURN-трафик дорогИгры, чувствительные к джиттеру; браузер-к-браузеру или для смешанных медиа
WebTransport (HTTP/3)QUIC, частичная надёжность, сервер проще, чем WebRTCЕщё не везде доступен, экосистема молодаПерспектива на 1–2 года вперёд; пилоты

По нашему опыту как строителей, 80% браузерных проектов стартуют с WebSocket и живут отлично. WebRTC имеет смысл, если нужно P2P/voice или вас душит TCP HOL-blocking при активном обновлении состояния. P2P для честных матчей — спорная идея: авторитетным должен оставаться сервер.

Язык и фреймворк сервера: скорость разработки vs скорость тиков

Короткий ориентир:

Язык/стекСильные стороныСлабые стороныТипичный use-case
Node.js (uWebSockets.js, Colyseus)Быстрый старт, богатая экосистема, хорошая DX, много готовых решенийGC-паузы, однопоточность (хотя есть Worker Threads), нужно следить за backpressureИнди и midcore, time-to-market критичен
Go (netpoll, nhooyr/websocket)Предсказуемые t-latency, низкие паузы GC, простое деплой/кросс-компиляцияМеньше «из коробки» гейм-фреймворковСессионные MMO-lite, стабильные 30–60 Гц, тысячи комнат
Rust (Tokio, quinn, webrtc-rs)Максимум производительности и контроля, нулевые паузы GCБольше времени на разработку, высокая планка входаЖанры, где CPU-тяжёлая физика и строгие SLAs
Elixir (Phoenix Channels, OTP)Миллионы соединений, fault-tolerance, отличное наблюдение, hot reloadНизкоуровневого контроля над памятью меньшеОгромное количество комнат/чатов, мидкор с умеренной физикой

Тонкость: «быстрый» язык не починит плохую архитектуру. Падение производительности обычно из-за неоптимальной сериализации/дельт и лишних аллокаций, а не из-за «не того языка».

Минимальный сервер на Node.js с тиками и дельтами

Для скорости демонстрации покажем sketched-подход на TypeScript с uWebSockets.js. Он даёт низкие накладные по сравнению с популярными ws/Socket.IO и хорошо дружит с бинарными протоколами.

// package.json: uWebSockets.js, msgpackr
import uWS from 'uWebSockets.js';
import { pack } from 'msgpackr';

const TICK_HZ = 30;
const clients = new Map<number, uWS.WebSocket>();
let nextId = 1;

// Игровой стейт (упрощённо): позиции игроков и версии снапшота
type Vec2 = { x: number; y: number };
const state: Record<number, Vec2> = {};
let snapshotVersion = 0;

function applyInput(id: number, input: { dx: number; dy: number }) {
  const p = state[id] || { x: 0, y: 0 };
  p.x += input.dx;
  p.y += input.dy;
  state[id] = p;
}

function computeDeltas(prevVersion: number) {
  // В реальности используем хэши/битовые маски; тут — вся карта позиций
  return { v: snapshotVersion, players: state };
}

uWS
  .App()
  .ws('/room', {
    compression: uWS.DEDICATED_COMPRESSOR_3KB,
    maxPayloadLength: 16 * 1024,
    idleTimeout: 120,
    upgrade: (res, req, context) => {
      res.upgrade({},
        req.getHeader('sec-websocket-key'),
        req.getHeader('sec-websocket-protocol'),
        req.getHeader('sec-websocket-extensions'),
        context
      );
    },
    open: (ws) => {
      const id = nextId++;
      (ws as any).id = id;
      clients.set(id, ws);
      state[id] = { x: 0, y: 0 }; // spawn
      // Отправим текущий снапшот
      ws.send(pack({ t: 'snapshot', v: snapshotVersion, players: state }));
    },
    message: (ws, message, isBinary) => {
      try {
        const data = JSON.parse(Buffer.from(message).toString());
        if (data.t === 'input') applyInput((ws as any).id, data);
      } catch {}
    },
    close: (ws) => {
      const id = (ws as any).id;
      clients.delete(id);
      delete state[id];
      snapshotVersion++;
    }
  })
  .listen(9001, (token) => {
    if (!token) throw new Error('Port busy');
    console.log('Room listening on :9001');
  });

// Тик-луп: обновление и рассылка дельт
setInterval(() => {
  snapshotVersion++;
  const payload = pack({ t: 'delta', ...computeDeltas(snapshotVersion - 1) });
  for (const [id, ws] of clients) {
    // Backpressure: не шлём, если сокет перегружен
    if (ws.getBufferedAmount() < 64 * 1024) ws.send(payload, true);
  }
}, 1000 / TICK_HZ);

Пояснения:

  • Используем бинарную сериализацию (msgpackr), следим за getBufferedAmount() — иначе улетим в лаг из-за переполнения буфера.
  • Реальный проект держит «версионированные» снапшоты и рассылает только изменившиеся компоненты (ECS-компоненты), добавляет аутентификацию токеном и миграцию игроков между процессами при рестартах.

Сервер на Go будет выглядеть компактнее по памяти; на Rust — ещё экономнее и предсказуемее по p99. Но TypeScript часто выигрывает спринт MVP → траектория монетизации.

Синхронизация состояния: тики, предсказание, интерполяция

  • Тик-частота: 20–30 Гц достаточно для MOBA/экшенов с умеренным TTK. 60 Гц имеет смысл, когда локальная физика перетекает в исход боя (и у вас бюджет на железо).
  • Клиентское предсказание: инпуты применяются локально сразу, сервер присылает authoritative-стейт; клиент делает корректировку (reconciliation) при расхождениях.
  • Интерполяция/экстраполяция: для других игроков рендерим интерполяцию между последними снапшотами; при потере пакетов переключаемся на кратковременную экстраполяцию.
  • Дельта-компрессия: храните bitset изменённых компонентов и кодируйте изменения компактно. 60–80% трафика обычно экономится именно тут, а не на zlib.
  • Квантование: координаты/углы в int16/int32 в мирных единицах — против «сырых» float.

Сервер должен чётко различать: вход игрока (input), результаты симуляции (snapshot), и side-effects (события/аудио/эффекты), иначе вы смешаете каналы и получите ненужный трафик. Один сухой совет: не передавайте состояние, которое можно детерминированно воспроизвести на клиенте.

Масштабирование: комнаты, регионы, балансировка

  • Комнаты = процесс/актори. Самый простой и надёжный юнит масштабирования. В Kubernetes — по pod на N комнат, stickiness по roomId.
  • Matchmaker оперирует пулами комнат и публикует события в NATS/Redis Streams. Комната регистрируется в сервис-дискавери (Consul/etcd или headless-сервис в K8s).
  • Region routing: резолвим ближайший регион по RTT/GeoIP. Пользователь может перезаписать выбор, если играет с другом.
  • Sticky sessions: по WebSocket — через cookie или query token → балансировщик должен поддерживать sticky по хешу.
  • Горизонт: тысячи комнат = тысячи соединений между сервисами. Думайте об observability с первого дня: метрики на комнату, не «в целом по кластеру».

Сохранение состояния при рестартах — боль многих инди. Решение: быстрый снапшот комнаты (инкрементально) в Redis/объектку с TTL 10–30 сек; при рестарте — тёплый рекавери и soft-reconnect игроков. Это дешевле, чем вечная «высокая доступность» одной комнаты.

Что ломается в продакшене

  • Backpressure: если игнорировать, то WebSocket-буферы раздуваются, GC зашкаливает, тики рваные. Лечится проверкой bufferedAmount, передачей приоритезированных пакетов и капингом частоты рассылок.
  • GC-паузы и аллокации: особенно в Node.js при частых JSON-парсах. Решение — бинарные протоколы, object pooling, избегать временных объектов внутри тика.
  • Nagle/Delayed ACK: на TCP убедитесь, что TCP_NODELAY включён (библиотеки реального времени обычно это делают).
  • ICE/TURN расходы: WebRTC через TURN может умножить трафик и счет. Самостоятельный coturn + агрессивный STUN и fallback на WebSocket для data-only — сбалансированная схема.
  • Десинхрон из-за дрейфа часов: не полагайтесь на Date.now() для симуляции; у сервера должен быть authoritative time, у клиента — локальный смещение-дрифт.
  • DDoS/WebSocket flood: rate limit на gateway (Cloudflare Rules), токены с коротким TTL, circuit breakers на уровне комнаты.
  • Сохранение результатов: слишком частые записи в Postgres во время матча → contention. Записывайте итоги батчами после матча, используйте idempotent операции.

Классическая шутка: если ваша игра иногда «телепортирует» игроков — это либо читер, либо вы слишком оптимистично сжимали дельты. Или и то, и другое.

Экономика и сроки: сколько стоит и когда окупается

Примерные ориентиры (региональная оценка, без маркетинга):

  • MVP (браузерный PvP 2–10 игроков/комната, 30 Гц, WebSocket, авторитетный сервер): 4–8 недель, команда 2–4 инженера. Бюджет разработки: $30k–$120k в зависимости от контента и интеграций.
  • Инфраструктура на 5k CCU по региону: 6–12 узлов c8a/cpu-оптимизированные (AWS/GCP), Redis (managed), Postgres (HA), Cloudflare. Оценка $2k–$8k/мес + исходящий трафик.
  • TURN/голос: дополнительно $300–$3000/мес при активном использовании.
  • Тестирование нагрузки и телеметрия: 1–2 недели. Без этого вы будете «гадать» на проде.

Где горят деньги:

  • Трафик и неэффективная сериализация (в 2–5 раз больше байтов, чем нужно).
  • Громоздкая физика на сервере без профайлинга.
  • Плохой матчмейкинг (долгие очереди → люди уходят → CAC вырастает).

Окупаемость приходит тогда, когда матч стабилен (ни лагов, ни дропов), а экономика игры удерживает сессии. Технический долг мультиплеера редко чинится «по ходу» без пауз.

Когда брать какой стек

  • Инди/MVP, быстрый рынок, 2D/изометрия, комнаты до 20 игроков: Node.js + uWebSockets.js/Colyseus; Redis; Postgres; WebSocket. Сборка через Docker, деплой на Fly.io/Render или k8s в managed-кластере.
  • Средний онлайн, стабильные 30–60 Гц, упор на предсказуемость и простое масштабирование: Go + nhooyr/websocket или fasthttp + Redis/NATS + Postgres. Kubernetes, horizontal pod autoscaler.
  • Жёсткие SLAs, CPU-тяжёлая симуляция, баттлы с большим количеством объектов: Rust + Tokio, бинарная сериализация (FlatBuffers/Cap’n Proto), специализированный профайлинг.
  • Крайняя конкуренция по соединениям/акторам, hot upgrades, сложная оркестрация комнат: Elixir + Phoenix Channels/LiveView, кластеризация через libcluster.

Если планируете выпускаться не только в браузере, но и в десктопе/Steam, смотрите наш текст о том, как выпустить игру в Steam, будучи веб-студией — нюансы билда и дистрибуции влияют на сетевую архитектуру и тестирование.

Инструменты и практики, которые экономят месяцы

  • Набор протокольных тестов: реплеи входов, симуляция джиттера/потерь, автосравнение снепшотов.
  • Канарейка на 1–5% игроков при выкладках. Feature flags и пер-игроковая телеметрия.
  • Единый формат сообщений: один прото-файл/IDL для сервера и клиента; автогенерация кодеков.
  • Отдельный канал для reliability-critical событий (hit/kill) и для «косметики». Разные приоритеты в очереди отправки.
  • Бюджет трафика на дизайн-стадии. Спорьте о килобайтах, пока все трезвы.

FAQ

Можно ли сделать полноценный мультиплеер на Firebase/Firestore без собственного сервера?

Можно сделать прототип. Но как только потребуется авторитетная логика, стабильные тики и защита от читов, нужен собственный сервер. Firestore/RTDB не предоставляют нужной детерминированности и пинг-ориентированного обмена, а стоимость операций быстро улетает.

Serverless (AWS Lambda/Cloudflare Workers) подойдёт для комнат?

Для матчмейкинга, лобби, inventory — да. Для авторитетной логики с тиками — нет: нужен долгоживущий процесс, предсказуемый CPU-слайс и сокеты. Иначе вы упираетесь в холодные старты и ограничения соединений.

Какой тикрейт выбрать: 20, 30 или 60 Гц?

20–30 Гц — по умолчанию для большинства браузерных экшенов. 60 Гц — когда механики критичны к задержке и у вас достаточно бюджета на оптимизацию и железо. Важнее стабильность тика (джиттер), чем сам номинал.

Имеет ли смысл P2P между браузерами через WebRTC для экономии серверов?

Редко. Авторитет всё равно у сервера, иначе — читы и десинхрон. P2P часто превращается в relay через TURN → вы платите трафик и усложняете сеть. Лучше экономьте байты дельтами и квантованием.

Какую базу выбрать для игровых данных?

  • Постоянные данные (аккаунты, инвентарь, платежи) — Postgres.
  • Эфемерные (очереди матчей, присутствие, счётчики) — Redis.
  • Логи/телеметрия — ClickHouse/BigQuery. Документные БД годятся, но чаще не нужны.

Как бороться с читами в браузере?

Сервер — авторитет на всё значимое. Клиент ничего «не решает»: попадания, урон, лут — на сервере. Сигнатуры подозрительных паттернов, трейсинг инпутов, rate limits. Браузерные анти-чит средства ограничены, поэтому участь чита — бан по поведенческим метрикам.

Key takeaways

  • Архитектура по умолчанию: авторитетный Room Server, WebSocket/WebRTC, дельта-компрессия, Redis+Postgres, горизонтальное масштабирование по комнатам.
  • Язык выбирайте по целям: Node.js — скорость запуска, Go — предсказуемость, Rust — максимальная эффективность, Elixir — сверхконкурентность.
  • Производительность чаще упирается в сериализацию и backpressure, а не «в язык».
  • Тики 20–30 Гц хватает для большинства браузерных жанров; 60 Гц дороже и сложнее.
  • Планируйте наблюдаемость и нагрузочные тесты с первого спринта — иначе будете чинить на проде.
  • Экономьте байты: квантование, дельты, приоритетные каналы отправки важнее «магических оптимизаций».

Если вы строите realtime мультиплеер в браузере и хотите пройти от прототипа до стабильного продакшена без лишних трат, MTBYTE может спроектировать архитектуру, собрать MVP и подготовить масштабирование. Напишите нам: /contact.

СЛЕДУЮЩИЙ ШАГ

Понравилось как мыслим?

Применяем те же принципы в клиентских проектах: AI, автоматизации, продукты, которые не умирают после релиза.