Real-Time Backend
Обзор экосистем: Action Cable, Phoenix Channels, SignalR, Centrifugo
В 2014 году WhatsApp на двух дюжинах FreeBSD-серверов обслуживал 200 миллионов пользователей; в Discord три инженера на Elixir выдерживают 26 миллионов одновременных WebSocket-соединений к голосовым каналам; а в Basecamp DHH с командой из шести человек гонит 100 тыс. concurrent connections на Action Cable. Эти цифры показывают, что выбор фреймворка determine'ит не столько лимит масштабирования, сколько то, какой ценой команда его достигает. Четыре зрелых realtime-стека - Action Cable, Phoenix Channels, SignalR, Centrifugo - представляют четыре разные стратегии: 'вшить в фреймворк', 'построить на ВМ для concurrency', 'дать RPC поверх WebSocket', 'вынести в отдельный сервис'. Понимание их компромиссов делает выбор осознанным, а не данью моде.
- **Basecamp HEY**: Action Cable на ~100 тыс. concurrent connections через шардирование по subdomain - DHH регулярно публикует архитектурные посты
- **Discord**: Phoenix Channels на Elixir для голосовых каналов, 26+ миллионов одновременных WebSocket; технический блог разбирает работу с ETS и Phoenix.Presence
- **Microsoft Teams**: SignalR + Azure SignalR Service для presence и chat, плюс gRPC streaming для видео-метаданных
- **Авито**: Centrifugo для notifications-канала; PHP-backend публикует через HTTP API, Centrifugo держит миллион клиентских соединений
Action Cable: Ruby on Rails и Redis pub/sub
**Action Cable** появился в Rails 5 (2016) как стандартный способ дать Rails-приложению WebSocket-канал, не выходя за рамки фреймворка. Архитектурно это thin-layer: один процесс на Puma запускает event loop на основе nio4r (Ruby NIO), каждый WebSocket-клиент - тонкая `Connection` с авторизацией через cookie-сессию Rails, а сообщения курсируют через **Redis pub/sub** между процессами. Главная сила - тесная интеграция с Active Record, Action View и Devise: один и тот же `current_user` доступен на REST-endpoint и в WebSocket-канале. Главная слабость - однопоточный GVL Ruby: при работе на одном процессе потолок ~3-4 тыс. одновременных соединений, дальше требуется горизонтальное масштабирование с балансировщиком.
Rails 7 вводит **Action Cable Subscription Adapter** для NATS и PostgreSQL LISTEN/NOTIFY, что позволяет уйти от Redis в командах со строгим стеком Postgres. Производительность LISTEN/NOTIFY уступает Redis pub/sub (~5-10x), но для приложений с десятками тысяч соединений это компромисс приемлемый. На практике DHH (создатель Rails) в Basecamp эксплуатирует Action Cable на ~100 тыс. concurrent connections через шардирование по subdomain.
Почему Action Cable сравнительно низко масштабируется на одном процессе Ruby?
Phoenix Channels: BEAM и миллион соединений
**Phoenix Channels** строится на принципиально другом фундаменте - виртуальная машина **BEAM** (Erlang VM), на которой работает Elixir. BEAM создавалась в Ericsson для телефонной коммутации и проектировалась под десятки тысяч лёгких процессов на узел. Каждое WebSocket-соединение в Phoenix - отдельный BEAM-процесс (~2 КБ overhead), а pub/sub встроен в платформу через **`Phoenix.PubSub`** с pg2/pg-distribution: сообщение, отправленное на одном узле кластера, доезжает до всех остальных без внешнего брокера. Это позволило WhatsApp на FreeBSD держать 2 миллиона соединений на одном сервере (доклад 2013 года) на похожем стеке.
Phoenix 1.7+ добавил **LiveView** - SSR-фреймворк, где HTML-разметка отрисовывается на сервере, а Phoenix Channel доставляет diff-патчи браузеру. Это альтернатива React/Vue для real-time UI без написания JavaScript. Discord использует Elixir + Phoenix для голосовых каналов: серверы обрабатывают 26 миллионов WebSocket-соединений с p99 latency в считанные миллисекунды.
Какая ключевая способность BEAM даёт Phoenix Channels её масштабируемость?
SignalR: .NET, hubs и автоматический fallback
**SignalR** - официальный realtime-фреймворк Microsoft для .NET. Главная архитектурная особенность - концепция **Hubs**: вместо явной отправки JSON-сообщений клиент вызывает методы серверного класса (`hub.invoke('SendMessage', ...)`), а сервер вызывает методы на клиенте (`Clients.All.SendAsync('ReceiveMessage', ...)`). Это RPC поверх WebSocket с прозрачной сериализацией. Второе свойство - **transport fallback**: если WebSocket недоступен (firewall, прокси), SignalR автоматически переключается на Server-Sent Events, затем на Long Polling. Третье - **Azure SignalR Service**: managed-вариант на Azure, обрабатывающий до миллиона соединений без управления узлами.
В отличие от Action Cable и Phoenix, SignalR изначально проектировался под enterprise: интеграция с Azure AD, gRPC streaming, MessagePack-binary сериализация и redis-backplane для масштабирования. .NET имеет real preemptive threading через CLR, поэтому SignalR на одном узле упирается не в GIL, а в количество дескрипторов сокетов ОС - обычно 64-128 тыс. соединений на узел при тюнинге.
В чём практическая ценность концепции Hubs в SignalR по сравнению с обычными WebSocket-сообщениями?
Centrifugo: language-agnostic real-time service
**Centrifugo** (Александр Емелин, 2014) - принципиально другой подход: не библиотека внутри приложения, а самостоятельный сервис на Go, к которому подключаются клиенты по WebSocket/SSE/HTTP-stream, а ваше backend-приложение (на любом языке) публикует сообщения через HTTP API или gRPC. Centrifugo разгружает основной бэкенд от WebSocket-нагрузки: 1 миллион соединений на узел стандартный сценарий. Подходит для случаев, когда стек уже выбран (Django, Laravel, Express) и не хочется тащить туда полноценный realtime-движок. Также Centrifugo даёт встроенные фичи: presence, history с TTL, JWT-аутентификация, recovery после переподключения.
Compare-таблица: Action Cable - вшита в Rails, удобство интеграции против низкого потолка масштабирования. Phoenix Channels - предельно высокая масштабируемость, но требует Elixir-стека. SignalR - типобезопасный RPC и transport fallback, идеален для .NET-приложений. Centrifugo - универсальный sidecar, работает с любым backend-стеком, но требует двойной аутентификации (приложение -> Centrifugo через JWT). Выбор определяется не производительностью, а тем какой стек уже используется и насколько критичны масштабирование и встроенные фичи.
Выбор realtime-фреймворка определяется производительностью
Главный фактор выбора - стек, в котором уже работает команда, и масштаб ожидаемой нагрузки. Производительность фреймворков отличается в разы, но различия редко становятся узким местом до 100 тыс. одновременных соединений.
Все четыре фреймворка справляются с типичной нагрузкой SaaS (1-50 тыс. соединений). Phoenix - очевидный выбор только когда планируется миллион соединений. До этого порога выбор определяется удобством для команды: переход с Ruby/Rails на Elixir ради realtime редко окупается, а Centrifugo позволяет получить такую же масштабируемость без миграции.
Когда выбор Centrifugo предпочтительнее Action Cable / SignalR / Phoenix Channels?
Ключевые идеи
- **Action Cable** - вшит в Rails, лёгкая интеграция с Active Record/Devise, но GVL Ruby ограничивает потолок на ~3-4 тыс. соединений на процесс; идеален когда стек уже Rails
- **Phoenix Channels** - на BEAM, лёгкие изолированные процессы (~2 КБ) + встроенный pg-pubsub дают миллион соединений на узел архитектурно естественно
- **SignalR** - RPC-абстракция Hubs поверх WebSocket с typed-интерфейсом, transport fallback (WS -> SSE -> Long Polling), managed-вариант Azure SignalR Service
- **Centrifugo** - standalone-сервис на Go: language-agnostic, sidecar для любого backend-стека, встроенные presence/history/JWT
- **Главный критерий выбора - стек команды, а не производительность**; различия в скорости становятся узким местом редко, до 100 тыс. соединений все варианты справляются
Связанные темы
Realtime-фреймворки развиваются на пересечении нескольких направлений:
- Scaling WebSocket — Все четыре фреймворка опираются на pub/sub-backplane для горизонтального масштабирования; Redis/NATS/pg-distribution - три типичных решения
- Authentication — JWT, session cookies, OAuth - выбор зависит от близости фреймворка к существующей системе авторизации (Action Cable нативно к Rails session, Centrifugo - JWT)
- Real-time architecture patterns — Pattern 'realtime as a service' через Centrifugo противопоставлен embedded-фреймворку в Rails/Phoenix; выбор определяет операционную модель
Вопросы для размышления
- Если Phoenix Channels поддерживает миллион соединений на узел, а Action Cable - три тысячи, почему большинство Rails-проектов всё ещё выбирают Action Cable вместо миграции на Elixir?
- Centrifugo и Azure SignalR Service оба - 'realtime as a service', но устроены принципиально по-разному. В чём ключевая разница их операционных моделей?
- Какой инвариант помогает выбрать между embedded-фреймворком и standalone-сервисом до того, как команда столкнётся с лимитами масштабирования?
Связанные уроки
- rt-12 — Предыдущий урок по RT экосистемам
- rt-14 — Выбор фреймворка открывает путь к production deployment
- net-15-tcp-basics — WebSocket строится поверх TCP - основа для сравнения фреймворков
- aie-08-streaming — LLM streaming через SSE - та же архитектура RT push
- ds-01-intro — Распределённые системы и RT-экосистемы решают похожие задачи масштабирования
- net-36-websocket