Real-Time Backend

Что такое Real-Time

В 2013 году Facebook столкнулся с проблемой: мобильное приложение каждые 5 секунд спрашивало сервер «есть новые сообщения?». При 500 миллионах пользователей это 6 миллиардов бессмысленных запросов в минуту. Батареи разряжались, серверы горели. Решение - MQTT push-протокол - сократило трафик в 40 раз и сделало Messenger по-настоящему мгновенным.

  • Telegram, WhatsApp - мгновенная доставка сообщений через WebSocket и push
  • Google Docs, Figma - одновременное редактирование документа десятками людей
  • Uber, Яндекс.Такси - live-позиция водителя обновляется каждые 2 секунды
  • Fortnite, CS2 - 128 tick rate, состояние мира обновляется 128 раз в секунду
  • Bloomberg Terminal - котировки акций обновляются в реальном времени по всему миру

От Comet к WebSocket

До 2011 года real-time в вебе был хаком. Техника Comet (2006) использовала скрытый iframe с бесконечным ответом сервера - браузер рендерил <script> теги по мере их поступления. Другой хак - Flash Socket - требовал плагина. В 2011 году RFC 6455 стандартизировал WebSocket, дав вебу полноценный двусторонний канал. Это изменило архитектуру бекендов навсегда.

WebSocket превратил браузер из «запрашивающего клиента» в полноценного участника real-time коммуникации

Что значит «real-time» для бекенда

Вы набираете сообщение в Telegram. Собеседник видит «печатает...» - мгновенно. Вы отправили - он прочитал через полсекунды. Это **real-time**: сервер доставляет данные клиенту в момент их появления, а не когда клиент решит спросить.

В классическом HTTP клиент всегда инициатор: послал запрос - получил ответ. Сервер **не может** сам постучаться к клиенту. Это как почтовый ящик: письмо появится только когда вы его проверите. Real-time переворачивает эту модель - сервер **сам** push'ит данные клиенту.

  • Request-Response (классический HTTP) — Клиент спрашивает → сервер отвечает. Клиент не спросил - данных нет. Задержка = интервал между запросами.
  • Real-Time (push-модель) — Сервер отправляет данные сразу при их появлении. Клиент подписан и ждёт. Задержка = сеть + обработка.

Пользователи **не думают** о протоколах. Они думают в терминах ожиданий: набираю текст - собеседник видит сразу. Лайкнул пост - счётчик обновился. Переместился на карте - курьер видит мою точку. Если задержка превышает ожидания - продукт ощущается «сломанным».

Действие пользователяОжиданиеЕсли медленнее
Печатает сообщение< 100 мс индикаторИндикатор мерцает, раздражает
Отправил сообщение< 300 мс доставкаОщущение «зависло»
Лайкнул пост< 500 мс обновлениеНажимает повторно
Двигается на карте< 1 с позицияКурьер «прыгает»
Получил уведомление< 3 с после событияПропустил важное

**Real-time не означает мгновенно.** Это означает «достаточно быстро для конкретного use case». Чат допускает 200 мс, а торговля акциями - нет.

Real-time значит нулевая задержка

Real-time значит задержка ниже порога восприятия для конкретного use case

Физика сети не позволяет нулевую задержку. «Real-time» - это когда пользователь не замечает задержку. Для чата это 200 мс, для игр - 50 мс, для торговли - микросекунды.

В чём ключевое отличие real-time от классического HTTP?

Polling vs Push: две модели получения данных

Представьте: вы ждёте посылку. **Polling** - вы каждые 5 минут выходите к двери и проверяете, не пришла ли. **Push** - курьер звонит в дверь сам. Какой подход эффективнее?

**Long Polling** - компромисс. Клиент отправляет запрос, но сервер **не отвечает сразу**, а держит соединение открытым, пока не появятся данные (или не истечёт таймаут).

ПодходЗадержкаНагрузка на серверКогда использовать
Short Polling0..interval (среднее = interval/2)Высокая (пустые запросы)Редкие обновления, простой API
Long Polling~сетевая задержкаСредняя (открытые соединения)Когда WebSocket недоступен
WebSocket~сетевая задержкаНизкая (одно соединение)Чат, игры, live-данные
SSE (Server-Sent Events)~сетевая задержкаНизкая (однонаправленный)Уведомления, ленты, дашборды

**Правило большого пальца:** если данные обновляются чаще раза в 10 секунд - polling не подходит. Нужен push.

Long polling решает все проблемы polling

Long polling - компромисс, который создаёт свои проблемы: каждый клиент держит открытое HTTP-соединение

При 50,000 подключений long polling создаёт 50,000 висящих HTTP-соединений. Это потребляет память и file descriptors на сервере. WebSocket использует более лёгкий протокол после handshake.

Чат-приложение с 10,000 пользователей использует short polling каждые 2 секунды. Сколько HTTP-запросов в минуту получает сервер?

Latency Budget: бюджет задержки

Когда пользователь отправляет сообщение в чате, до получателя оно проходит путь: клиент → сеть → сервер (обработка) → сеть → получатель. Каждый шаг добавляет задержку. **Latency budget** - это разбиение допустимой задержки по компонентам.

Use CaseДопустимая задержкаБюджет на серверБюджет на сеть
Typing indicator< 100 мс10 мс (только relay)40 мс (2 hop)
Чат-сообщение< 200 мс40 мс (validate + store)40 мс
Уведомление< 2 с500 мс (generate + route)200 мс
Live dashboard< 1 с200 мс (aggregate)100 мс
Multiplayer игра< 50 мс10 мс (game loop tick)20 мс
HFT трейдинг< 1 мс0.1 мс0.5 мс (colocation)

Бюджет задержки - инструмент проектирования. Он помогает понять, **где оптимизировать**. Если сеть занимает 80% бюджета - оптимизация кода на сервере бессмысленна, нужно менять архитектуру (CDN, edge computing, colocation).

  1. **Определить допустимую задержку** из требований продукта и UX-исследований
  2. **Разложить на компоненты:** клиент → сеть → сервер → сеть → клиент
  3. **Измерить реальные значения** каждого компонента (не угадывать!)
  4. **Найти bottleneck** - компонент, который занимает больше всего бюджета
  5. **Оптимизировать bottleneck** или пересмотреть архитектуру

**P99, а не среднее!** Средняя задержка 50 мс - звучит отлично. Но если 1% запросов занимает 5 секунд, каждый сотый пользователь страдает. При 1 млн пользователей - это 10,000 человек. Всегда считайте бюджет по P99 (99-й перцентиль).

Почему Discord перешёл с Go на Rust

Garbage collector в Go создавал паузы в 1-10 мс каждые несколько секунд. Для чата - допустимо. Для voice - слышно. Discord переписал критичные сервисы на Rust (без GC) и получил стабильный P99 < 1 мс. Latency budget определил выбор языка.

Допустимая задержка для чата - 200 мс. Сеть (RTT) занимает 80 мс, клиентский рендер - 20 мс. Сколько остаётся серверу на обработку?

Карта real-time use cases

Real-time - это не одна технология. Это спектр задач с разными требованиями к задержке, надёжности и масштабу. Понимание карты use cases помогает выбрать правильную технологию для конкретной задачи.

Use CaseЗадержкаНаправлениеТехнологияПримеры
Messaging / Chat< 200 мсBidirectionalWebSocketTelegram, Slack, WhatsApp
Typing indicators< 100 мсBidirectionalWebSocket«Печатает...» в мессенджерах
Notifications< 3 сServer → ClientSSE / Push APIЛайки, комменты, алерты
Live dashboard< 1 сServer → ClientSSE / WebSocketGrafana, торговые терминалы
Collaborative editing< 100 мсBidirectionalWebSocket + CRDT/OTGoogle Docs, Figma
Multiplayer games< 50 мсBidirectionalWebSocket / UDPFortnite, CS2
Live location< 2 сBidirectionalWebSocketUber, Яндекс.Такси
Stock trading< 1 мсBidirectionalCustom TCP / FPGANASDAQ, биржи
Live streaming< 5 сServer → ClientWebRTC / HLSTwitch, YouTube Live

Обратите внимание на колонку **«Направление»**. Не все use cases требуют двустороннюю связь. Уведомления и дашборды - только **server → client**. Для них SSE проще и достаточно. WebSocket нужен, когда клиент тоже активно отправляет данные.

Каждый use case предъявляет разные требования не только к задержке, но и к **гарантиям доставки**:

  • At-most-once (не более одного раза) — Typing indicator, cursor position. Потеря одного события - незаметна. Следующее обновление всё исправит.
  • At-least-once (хотя бы один раз) — Уведомления, события в ленте. Лучше показать дубль, чем потерять важное уведомление.
  • Exactly-once (ровно один раз) — Платежи, сообщения в чате. Дубли - проблема (двойное списание). Требует идемпотентности и подтверждений.

Ключевые идеи урока

  • Real-time - сервер push'ит данные клиенту, не дожидаясь запроса
  • Short polling - простой, но расточительный: 99% запросов впустую
  • Long polling - компромисс, но каждый клиент занимает соединение
  • WebSocket - полнодуплексный канал, стандарт для двусторонней real-time связи
  • SSE - простой однонаправленный поток, идеален для уведомлений и дашбордов
  • Latency budget - разбиение допустимой задержки по компонентам (сеть, сервер, клиент)
  • Всегда считайте по P99, а не по среднему - хвост распределения убивает UX

Что дальше

Теперь вы понимаете зачем нужен real-time и какие задачи он решает. Следующий шаг - разобраться в конкретных протоколах и их внутреннем устройстве.

  • WebSocket протокол — Основной двусторонний протокол real-time
  • Server-Sent Events — Однонаправленный push от сервера
  • Pub/Sub паттерн — Масштабирование real-time на несколько серверов

Вопросы для размышления

  • Какие real-time фичи есть в приложениях, которыми вы пользуетесь каждый день? Какую технологию они скорее всего используют?
  • Если бы вам нужно было добавить real-time уведомления в существующий REST API - какой подход вы бы выбрали и почему?
  • Как изменится latency budget, если ваши пользователи находятся на другом континенте?

Связанные уроки

  • rt-02-http-limits — HTTP limitations discussed next explain why real-time architectures were invented
  • bt-01-overview — Real-time protocols are a specialization of the transport overview covered in backend-transport
  • st-01-feedback-loops — WebSocket creates a closed feedback loop; polling is an open loop with high latency
  • alg-01-big-o — Push O(1) vs polling O(n) is a direct application of complexity analysis to protocol choice
  • sd-01-intro — Real-time requirements appear in System Design estimation: QPS, connection counts, fan-out
  • net-21-http-basics
  • net-63-realtime-compare
Что такое Real-Time

0

1

Войти

**Начинайте с самого простого решения.** SSE покрывает 80% задач (уведомления, ленты, дашборды). WebSocket - для оставшихся 20% (чат, игры, совместное редактирование). Custom UDP - единичные случаи (HFT, шутеры).

Для live-дашборда с обновлением графиков раз в секунду лучше всего подходит: