Real-Time Backend

Design: Live Sports

В момент победного гола на финале ЧМ инфраструктура должна доставить событие 500 млн экранам за полсекунды. Не через минуту. Не через 10 секунд. За 500 миллисекунд.

  • ESPN+ удерживал 22 млн одновременных подключений в финале NBA 2023 - это больше населения Австралии
  • Akamai абсорбировал пиковую нагрузку 60 Tbps во время чемпионатов мира - сравнимо со всем интернет-трафиком США в 2005 году
  • Twitter зафиксировал 7196 твитов в секунду в момент финального гола ЧМ 2014 - спортивные пики острее любых других событий
  • Super Bowl 2023: 120 млн зрителей, пиковая нагрузка на серверы ставок выросла в 40 раз за 3 секунды в момент тачдауна

Архитектура live-трансляций

Финал Чемпионата мира 2022 года собрал пиковую аудиторию 1,5 млрд человек. Инфраструктура ESPN+ удерживала 22 млн одновременных подключений во время финала NBA 2023. Такие нагрузки требуют многослойной архитектуры - единый монолит с WebSocket-сервером схлопнется в первые секунды.

Базовые слои системы

**Ingest Service** принимает события от официальных Stats API (Stats Perform, Sportradar) через HTTP polling каждые 200-500 мс или через их push-webhooks, затем публикует нормализованные события в Kafka. Каждый вид спорта - отдельный topic (`events.football`, `events.basketball`), что позволяет масштабировать обработку независимо.

**Event Processor** - потребитель Kafka, который обогащает сырое событие: добавляет метаданные команды, обновляет счёт, вычисляет статистику. Обработанные события отправляются в pub/sub шину и одновременно записываются в Redis для кеширования текущего состояния матча.

Два транспорта для клиентов: **SSE** (Server-Sent Events) для браузеров - проще в обслуживании, автореконнект встроен. **WebSocket** - для мобильных приложений и случаев, когда нужен двусторонний канал (чат, ставки). ESPN использует оба параллельно.

Поле `seq` критично: при переподключении клиент отправляет `Last-Event-ID: <seq>` и получает только пропущенные события, а не полный replay матча. Это снижает нагрузку при массовых реконнектах (например, после рекламной паузы).

Почему для каждого вида спорта создаётся отдельный Kafka topic, а не один общий?

Fan-out на миллионы подписчиков

Когда Криштиану Роналду забивает гол на 89-й минуте финала, в течение 500 мс нужно уведомить 50 млн подключённых клиентов. Простой loop по WebSocket-соединениям в одном процессе - это O(N) в одном потоке. На 50 млн пользователей это займёт несколько минут, тогда как задержка должна быть под 1 секунду.

Иерархический fan-out

Решение - **многоуровневый fan-out**. Единственный источник события публикует в pub/sub шину (Redis Pub/Sub или собственная реализация на Kafka). Тысячи edge-серверов по всему миру подписаны на шину. Каждый edge-сервер самостоятельно рассылает событие своим 20-50k подключённым клиентам.

УровеньКоличество нодКлиентов на нодуЗадержка
Event Processor10-20-0 мс (источник)
Regional Fan-out100-200-10-50 мс
Edge WebSocket2000-500010k-50k50-200 мс
Клиент50M+-конечная точка

Каждая edge-нода держит в памяти Map `matchId -> Set<WebSocket>`. При подключении клиента он регистрируется в нужной Map по matchId из URL. При голе событие идёт по цепочке: Kafka -> Event Processor -> Redis Pub/Sub -> все edge-ноды -> каждый клиент своей ноды.

Twitter зафиксировал 7196 твитов в секунду в момент гола в финале ЧМ 2014. У платформ реального времени пики ещё острее: один момент генерирует волну запросов, а не размазанный трафик. Поэтому fan-out строится на push-модели, а не на polling от клиентов.

Горизонтальное масштабирование edge-нод

Edge-ноды stateful по природе: WebSocket-соединение привязано к конкретному процессу. Load balancer направляет новые соединения с учётом текущей загрузки (`least-connections` алгоритм), а не round-robin. При добавлении нод к матчу с 5 млн зрителей новые клиенты получат новые ноды автоматически.

  • **Sticky sessions не нужны** - каждый клиент держит одно долгоживущее соединение с одной нодой
  • **Graceful shutdown** - при выводе ноды из ротации клиенты получают Close frame с кодом 1001 и переподключаются к другим нодам
  • **Health checks** каждые 10 сек - проверяют не только HTTP 200, но и количество активных соединений

Почему для рассылки событий от Event Processor к Edge-нодам используется Redis Pub/Sub, а не прямые HTTP-вызовы к каждой edge-ноде?

Обработка traffic burst

Super Bowl 2023 собрал 120 млн зрителей. В момент тачдауна в 4-м квартере число запросов к серверам ESPN выросло в 40 раз за 3 секунды. Стандартное горизонтальное масштабирование не успевает реагировать - новая EC2-инстанция поднимается 2-4 минуты, а пик проходит за 30 секунд.

Природа спортивных burst-паттернов

Спортивные события имеют предсказуемый lifecycle трафика: **pre-game buildup** (нарастающий рост за 30-60 мин до старта), **game spikes** (острые пики при голах/тачдаунах, длительность 10-60 сек), **halftime plateau** (умеренный стабильный трафик), **end-game rush** (максимальный пик, часто превышает game spikes в 2-3 раза).

  1. **Pre-warming** - Akamai и Cloudfront начинают прогрев edge-кешей за 2 часа до матча по расписанию
  2. **Reserved capacity** - AWS/GCP Reserved Instances заранее зарезервированы под известные крупные события
  3. **Load shedding** - при перегрузке некритичные запросы (статистика за прошлые сезоны) отклоняются с HTTP 503
  4. **Backpressure** - очереди Kafka буферируют события, предотвращая каскадный отказ при перегрузке downstream

Akamai и CDN как первая линия защиты

Akamai обслуживает пиковую нагрузку более 60 Tbps - это суммарный трафик нескольких крупных матчей одновременно. CDN принимает удар на себя: статика (страница матча, иконки команд, CSS) отдаётся с edge-нод без обращения к origin. Только динамические события (live-счёт, комментарии) доходят до backend.

**Graceful degradation** при перегрузке: если WebSocket-сервер перегружен, клиент автоматически переключается на SSE (меньше overhead). Если SSE тоже перегружен - на Long Polling с интервалом 5 сек. Пользователь видит счёт с задержкой 5 сек вместо 0.5 сек, но не видит ошибку.

  • Вертикальное масштабирование — Увеличение RAM/CPU инстанции. Быстро, но имеет жёсткий потолок. Не спасает от 40x burst за 3 секунды.
  • Горизонтальное масштабирование — Добавление нод. Работает для предсказуемых нагрузок. Auto-scaling реагирует за 2-4 мин, слишком медленно для спортивных пиков.
  • Pre-warming + CDN — Инфраструктура готова ДО пика по расписанию. CDN поглощает статику. Единственный подход, работающий для live-спорта.

Почему Auto Scaling Groups (AWS) недостаточны для обработки burst трафика при голе на Чемпионате мира?

Стратегия кеширования live-данных

Live-спорт создаёт парадокс кеширования: данные меняются каждые несколько секунд, но именно кеш спасает систему от краша. Наивный подход - TTL 0 (без кеша) - означает, что 50 млн клиентов при каждом обновлении долбят базу напрямую. Правильный подход - многослойный кеш с разными TTL для разных типов данных.

Классификация данных по волатильности

Тип данныхЧастота обновленияTTL кешаХранилище
Текущий счётКаждый гол (~5-10 мин)5 секRedis
Состав командыДо матча3600 сек (1 час)Redis + CDN
Live-события (feed)Каждые 30-60 сек2-3 секRedis
Исторические матчиНикогда86400 сек (1 день)CDN edge
Статистика игрокаПосле матча600 сек (10 мин)Redis + CDN

**Write-through cache**: Event Processor записывает событие в Redis и Kafka одновременно. Redis хранит текущее состояние матча как JSON-объект под ключом `match:{matchId}:state`. TTL 5 секунд гарантирует, что клиент при реконнекте получит актуальный счёт без обращения к PostgreSQL.

Cache stampede и его предотвращение

**Cache stampede** - ситуация, когда TTL кеша истекает одновременно для 50 млн клиентов, и все они одновременно идут в БД. Для матча с 50 млн зрителями это 50 млн запросов за долю секунды - гарантированный отказ базы данных.

  1. **Probabilistic early expiration** (XFetch алгоритм): каждый клиент с вероятностью p обновляет кеш досрочно, пока другие ещё читают свежие данные
  2. **Mutex на обновление**: только один процесс получает lock на обновление кеша, остальные ждут или возвращают stale-данные
  3. **Jitter в TTL**: вместо `setex(key, 30, ...)` использовать `setex(key, 28 + random(4), ...)` - разброс 4 секунды размазывает истечения кешей во времени

ESPN использует **stale-while-revalidate** стратегию: клиент получает данные из кеша мгновенно (даже если TTL истёк), а фоновый worker обновляет кеш. Счёт может быть устаревшим на 1-2 сек, но пользователь не видит задержку загрузки. Это компромисс между consistency и availability.

Чем меньше TTL кеша, тем более live-трансляция. Для настоящего real-time нужен TTL 0.

TTL 0 без кеша убивает систему при пиковой нагрузке. Real-time ощущение создаётся push-нотификациями через WebSocket/SSE, а не частотой polling. Кеш нужен для защиты от stampede и для восстановления состояния при реконнекте.

Итоги

  • **Многослойный pipeline**: Sportradar -> Kafka -> Event Processor -> Redis Pub/Sub -> Edge nodes -> клиенты. Каждый слой горизонтально масштабируется независимо
  • **Иерархический fan-out**: событие публикуется один раз в Pub/Sub, каждая из тысяч edge-нод самостоятельно рассылает своим клиентам - O(1) на стороне publisher
  • **Pre-warming вместо реактивного scaling**: Auto Scaling реагирует за 2-4 мин, спортивный пик длится 10-60 сек. Мощности резервируются заранее по расписанию матчей
  • **Write-through cache с jitter TTL**: Redis хранит текущее состояние матча, разброс TTL предотвращает stampede на БД при одновременном истечении кешей у 50 млн клиентов

Связанные темы

Live-спорт объединяет несколько паттернов распределённых систем:

  • WebSocket и SSE — Транспортный уровень для push-нотификаций клиентам
  • Kafka и event streaming — Backbone для надёжного fan-out событий между сервисами
  • Redis и кеширование — Текущее состояние матча и защита БД от stampede
  • CDN и edge computing — Akamai/Cloudfront поглощают статику и первую волну burst-трафика

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

  • Какой компонент архитектуры является единой точкой отказа (SPOF) и как его устранить?
  • Как изменится архитектура fan-out, если нужно поддерживать персонализированные события (например, уведомление только фанатам команды, забившей гол)?
  • При graceful degradation система переключается с WebSocket на Long Polling с интервалом 5 сек. Как это влияет на нагрузку серверов и как компенсировать рост запросов?

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

  • sd-01-intro
Design: Live Sports

0

1

Войти

WebSocket с TTL 5 сек в Redis даёт пользователю событие через 100-500 мс после гола. Отсутствие кеша при 50 млн одновременных пользователях кладёт базу за секунды - пользователь не увидит ничего.

Почему для текущего счёта матча используется TTL 5 секунд в Redis, а не 0 (без кеша)?