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 Processor | 10-20 | - | 0 мс (источник) |
| Regional Fan-out | 100-200 | - | 10-50 мс |
| Edge WebSocket | 2000-5000 | 10k-50k | 50-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 раза).
- **Pre-warming** - Akamai и Cloudfront начинают прогрев edge-кешей за 2 часа до матча по расписанию
- **Reserved capacity** - AWS/GCP Reserved Instances заранее зарезервированы под известные крупные события
- **Load shedding** - при перегрузке некритичные запросы (статистика за прошлые сезоны) отклоняются с HTTP 503
- **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 млн запросов за долю секунды - гарантированный отказ базы данных.
- **Probabilistic early expiration** (XFetch алгоритм): каждый клиент с вероятностью p обновляет кеш досрочно, пока другие ещё читают свежие данные
- **Mutex на обновление**: только один процесс получает lock на обновление кеша, остальные ждут или возвращают stale-данные
- **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 сек. Как это влияет на нагрузку серверов и как компенсировать рост запросов?