Serverless-архитектура чата (детально)
Перед началом: Модули 1–2 (serverless-концепции, сравнение архитектур). Инструменты: Yandex Cloud Functions, API Gateway, Serverless Containers. Время: ~6 ч.
🔥 Инцидент: «connecting...» — и пользователь ушёл
Вы выбрали Serverless. Дизайн готов: API Gateway → Cloud Functions → YDB. Пользователь открывает чат, нажимает «войти»... и видит «connecting...» три секунды. Cold Start: Cloud Function «просыпается», подтягивает зависимости, подключается к YDB. Пользователь не дожидается и уходит к конкурентам.
Хуже — на WebSocket: каждое новое соединение триггерит холодный старт контейнера. Пик в 9:00 утра — сотни пользователей одновременно подключаются, каждый ждёт свои 3 секунды. А когда контейнер тёплый — ответ за 50 мс. Нужна стратегия прогрева, разделение stateless (Functions) и stateful (Container), и чёткое понимание, что выполнять синхронно, а что — через очередь.
В этом модуле — детальный дизайн serverless-чата, который переживает cold start и пики нагрузки.
📖 Теория
В этом модуле разбираем детальный дизайн serverless-чата в Yandex Cloud: роли сервисов, поток данных, почему нужны очереди (YMQ/YDS), как достигается отказоустойчивость, и как всё укладывается в free-tier.
Схема архитектуры
На рисунке: клиент (PWA), точка входа (API Gateway), вычислительные компоненты (Cloud Functions и Serverless Container), хранилище и очереди (YDB, YMQ, YDS). Стрелки — направления вызовов и поток данных.
Роли сервисов и почему именно serverless
| Сервис YC | Роль в чате | Почему serverless | Лимит free-tier (ориентир) |
|---|---|---|---|
| API Gateway | точка входа: HTTP REST + WebSocket-маршрутизация, JWT-проверка | масштабируется автоматически, декларативная конфигурация | 100 000 запросов/мес |
| Cloud Functions (Go) | stateless-логика: Auth, Ingestion, Broadcast, Notifications | масштаб 0→N, оплата за вызовы, нет серверов | 1 000 000 вызовов/мес |
| Serverless Container (WS Gateway) | stateful WebSocket-хаб | постоянные соединения; масштаб от 0; контейнер, а не функция | 1 000 000 запросов/мес |
| YDB (serverless) | основная БД: пользователи, комнаты, сообщения, сессии | SQL, автомасштабирование, оплата за RU/IOPS | 1 000 000 RU/мес |
| YMQ | очередь событий (SQS-совместимая) | decoupling, retry, at-least-once, DLQ | 100 000 запросов/мес |
| YDS (Data Streams) | стрим для real-time fan-out подписчикам | порядок в шарде, подписки WS-воркера | 2 000 000 единиц данных/мес |
| Object Storage | хостинг статики PWA (S3) | бесконечное масштабирование, дёшево | 1 ГБ + HTTP-запросы |
Ключевой принцип: stateless-операции (REST: создать комнату, получить историю, поиск) — в Cloud Functions; stateful (постоянные WebSocket-соединения) — в Serverless Container. Смешивать нельзя: функция не «держит» сокет.
Компоненты архитектуры
1. PWA Client (React + TypeScript)
- Доставка: SPA загружается из Object Storage (S3-совместимый бакет с публичным доступом).
- Офлайн: Service Worker кэширует статику и последние сообщения; исходящие сообщения попадают в outbox.
- Push: Web Push для оффлайн-уведомлений.
- Связь с бэком: REST (HTTP) для CRUD + WebSocket для real-time.
2. Yandex API Gateway
- Принимает HTTP-запросы и проксирует WebSocket-соединения.
- Маршрутизация: REST → Cloud Functions, WebSocket → Serverless Container.
- Проверка JWT на защищённых эндпоинтах, rate limiting.
3. Auth Service (Cloud Function, Go)
- Ответственность: регистрация, вход, обновление токенов, выход.
- Поток: email+password → bcrypt-хеш в YDB; выдача пары JWT.
- Токены: Access (15 мин), Refresh (7 дней, rotation при refresh).
- UseCase:
RegisterUseCase,LoginUseCase,RefreshTokenUseCase. - Безопасность: пароли не хранятся в открытом виде; сессии инвалидлируются.
4. Message Ingestion Service (Cloud Function, Go)
- Ответственность: принять новое сообщение и быстро ответить клиенту (202 Accepted).
- Действия: валидация → сохранение в YDB → публикация события в YMQ →
202 Accepted. - UseCase:
SendMessageUseCase. - Зачем так: клиент не ждёт доставки всем адресатам — это асинхронная работа Broadcast Worker.
5. YMQ (Yandex Message Queue)
- Очередь событий для асинхронной обработки (совместима с AWS SQS).
- Семантика at-least-once: сообщение доставляется ≥1 раз → потребитель обязан быть идемпотентным.
- Поддержка DLQ (Dead Letter Queue) для «битых» сообщений.
6. Broadcast Worker (Cloud Function, Go)
- Триггер: сообщение в YMQ (запускается платформой).
- Действия: найти активных участников комнаты в YDB → опубликовать в YDS для online-подписчиков; для offline — положить задачу в очередь пушей.
- Идемпотентность: по
message_id(YMQ может доставить дважды).
7. YDS (Yandex Data Streams)
- Стрим данных для real-time доставки (Kinesis-совместимый).
- WS Gateway подписан на стрим и получает события для трансляции клиентам.
- Порядок сообщений гарантируется в пределах партиции (шарда по
room_id).
8. WebSocket Gateway (Serverless Container)
- Держит долгоживущие WebSocket-соединения с клиентами.
- Подписан на YDS → отправляет входящие сообщения нужным клиентам.
- Масштабируется от 0; при росте — несколько реплик + согласованное состояние через YDS/YDB.
- Graceful shutdown: при остановке корректно закрывает соединения (close-фреймы).
9. YDB (Yandex Database)
- Serverless, SQL-совместимая.
- Хранит: пользователи, комнаты, участники, сообщения, активные соединения, push-подписки.
- Индексы:
room_id + created_at(история),name(автокомплит),user_id(комнаты пользователя).
10. Notification Worker (Cloud Function, Go)
- Триггер: очередь пуш-уведомлений.
- Действия: достать VAPID-ключи/подписки из YDB → отправить Web Push оффлайн-клиентам.
Поток данных: отправка и доставка сообщения
Синхронная часть (быстро, ~50 мс):
- Клиент отправляет сообщение (REST
POST /messagesчерез API Gateway). - API Gateway проверяет JWT и маршрутизирует в Ingestion Function.
- Ingestion валидирует, сохраняет сообщение в YDB, публикует событие
message_createdв YMQ. - Ingestion отвечает клиенту
202 Accepted— клиент не ждёт рассылки.
Асинхронная часть (доставка, 1–2 с): 5. YMQ-триггер запускает Broadcast Worker. 6. Broadcast Worker находит активных участников комнаты в YDB. 7. Для online-участников публикует сообщение в YDS; для offline — задачу в очередь пушей. 8. WS Gateway получает событие из YDS и отправляет его подключённым клиентам. 9. Notification Worker обрабатывает очередь пушей и шлёт Web Push оффлайн-клиентам.
Граница синхронного/асинхронного — это и есть развязка (decoupling): клиент быстро видит «отправлено», а тяжёлая работа (найти участников, разослать, push) идёт в фоне и переживает пики нагрузки.
Инициализация клиента
- Пользователь открывает URL → запрос идёт в Object Storage.
- Object Storage отдаёт
index.html, JS-бандл (Vite), стили. - PWA регистрируется и кэширует ресурсы для офлайн.
- Клиент подключается по WebSocket к API Gateway и проходит JWT-аутентификацию.
Преимущества Object Storage: бесконечное масштабирование статики, интеграция с CDN, дешёвое хранение, высокая доступность. Отдельно от compute — статику можно обновлять без редеплоя бэкенда.
Почему очереди (YMQ/YDS) необходимы
- Отказоустойчивость: если YDB перегружена, сообщение не теряется — ждёт в очереди.
- Декаплинг: Ingestion быстро отвечает, Worker обрабатывает в своём темпе.
- Буфер под пики: YMQ примет любое количество сообщений, Worker разгребает постепенно.
- Повторы: при сбое Worker-а сообщение возвращается в очередь и переобрабатывается.
- Гарантии доставки: at-least-once + идемпотентный потребитель + DLQ для неисправимых.
YMQ vs YDS: YMQ — очередь задач (триггерит функцию), YDS — стрим для real-time fan-out подписчикам (WS Gateway читает поток). Это два разных паттерна: «задача → обработчик» и «событие → подписчики».
Преимущества архитектуры
Производительность
| Метрика | Значение | Описание |
|---|---|---|
| Latency Ingestion | ~50 мс | Быстрый ответ клиенту (202 Accepted) |
| Latency Broadcast | 1–2 с | Асинхронная доставка через очередь |
| Throughput | 1000+ msg/сек | Ограничено YDB, а не кодом |
| Cold Start | <500 мс | Прогрев Cloud Functions |
| WebSocket | persistent | Постоянное соединение без overhead на запрос |
Масштабирование
Каждый компонент масштабируется независимо под свою нагрузку:
- Ingestion Function — по числу входящих сообщений;
- Broadcast Worker — по глубине очереди YMQ;
- WS Container — по числу активных WebSocket-соединений;
- YDB — автоматически (serverless, по RU/IOPS).
Cloud Functions создают новые инстансы под параллельные вызовы, поэтому пик входящих сообщений автоматически «переваривается» без ручного масштабирования.
Отказоустойчивость
Единой точки отказа (SPOF) нет: трафик балансируется через API Gateway, функции и контейнеры многорепличны, а очередь (YMQ) и стрим (YDS) сохраняют данные при сбое обработчиков.
При сбое компонента:
- Cloud Function упала → перезапуск за 1–2 с, сообщение переобработается из очереди.
- YDB недоступен → сообщения накапливаются в YMQ.
- WS Container упал → клиенты переподключаются, сообщения ждут в YDS.
Безопасность
| Аспект | Решение |
|---|---|
| Аутентификация | JWT: Access 15 мин, Refresh 7 дней (rotation) |
| Пароли | bcrypt + salt, хранение только хеша |
| WebSocket | проверка JWT при подключении |
| API | API Gateway: rate limiting, JWT validation |
| Данные | шифрование at rest в YDB |
| Транспорт | принудительный HTTPS/WSS |
Обслуживание
Независимые деплои: Auth, Ingestion, Broadcast, Notifications обновляются раздельно; мгновенный rollback по версиям Cloud Functions. Мониторинг — Yandex Monitoring (метрики) и Yandex Logging (логи).
Оценка затрат на Free Tier
Сценарий: 20 клиентов, 1000 сообщений в день (30 дней).
| Сервис | Операций в месяц | Free-tier | Превышение | Стоимость |
|---|---|---|---|---|
| API Gateway | ~30 000 + auth | 100 000 | 0 | ₽0 |
| Cloud Functions (Auth + Ingestion + Workers) | ~80 000 вызовов | 1 000 000 | 0 | ₽0 |
| YDB | ~120 000 операций | 1 000 000 | 0 | ₽0 |
| YMQ | ~30 000 запросов | 100 000 | 0 | ₽0 |
| YDS | < 2 млн единиц данных | 2 000 000 | 0 | ₽0 |
| Serverless Containers (WS Gateway) | 20 клиентов × 30 дней | 1 000 000 | 0 | ₽0 |
| Object Storage | ~1 ГБ статики | 1 ГБ | 0 | ₽0 |
| Исходящий трафик | ~2 ГБ | 100 ГБ | 0 | ₽0 |
Дополнительно с Auth Service: ~20 регистраций, ~600 входов, ~1 200 refresh-запросов/мес.
Итог: ₽0/месяц — free-tier покрывает small-to-medium чат с 20 пользователями и 1000 сообщений/день.
Когда начнётся оплата (ориентир):
- ~3 300 сообщений/день — заканчивается free-tier Cloud Functions и API Gateway;
- ~50 000 сообщений/день — заканчивается free-tier YDB.
Когда НЕ использовать эту архитектуру
| Сценарий | Почему не подходит | Альтернатива |
|---|---|---|
| Stateful с long-lived соединениями в огромных количествах | WS Container масштабируется с оверхедом | Выделенные VM/Kubernetes |
| Требуется latency < 50 мс | Cold start + задержка очереди | Виртуальная машина |
| Жёсткая предсказуемость стоимости | Serverless-цены переменны | Reserved instances |
| GPU / тяжёлые вычисления | Cloud Functions без GPU | Compute Cloud с GPU |
| Долгие операции (> 15 мин) | Таймаут функций | Container/VM |
Идеально для: event-driven систем (чат, уведомления, webhooks), непредсказуемой нагрузки, MVP/стартапов (быстрый старт за ₽0), микросервисов с независимым масштабированием.
Best Practices
Подтверждайте обработку в YMQ. Вернули ошибку — сообщение вернётся в очередь; успех — удаляется.
gofunc handle(msg Message) error { if err := process(msg); err != nil { return err } return nil }
Идемпотентность по message_id. YMQ может доставить сообщение дважды — повторная обработка не должна создавать дубль.
gofunc sendMessage(msg Message) error { if exists(msg.ID) { return nil } return save(msg) }
DLQ. Сообщения, которые не удалось обработать после N попыток, уходят в Dead Letter Queue → алерт → разбор вручную.
Ключевые метрики: ingestion_latency_p99, broadcast_latency_p99, ymq_depth (глубина очереди),
websocket_connections, error_rate по сервисам, ydb_throttled. Подробнее — в модуле по наблюдаемому (observability).
JWT rotation. Access token — короткоживущий (компромисс); Refresh — длиннее (удобство); при refresh выдаётся новая пара, старый refresh инвалидидируется.
Компромиссы (Trade-offs)
| Решение | Плюс | Минус |
|---|---|---|
| Sync Ingestion + async Broadcast | быстрый ответ клиенту (202) | задержка доставки 1–2 с |
| YMQ (очередь) vs YDS (стрим) | разные паттерны: задача / подписчик | два сервиса вместо одного |
| Serverless Container для WS | постоянные соединения, масштаб от 0 | оверхед при масштабировании |
| Cloud Functions для REST | масштаб 0 → N, оплата за вызовы | cold start ~500 мс |
| At-least-once + идемпотентность | гарантия доставки | сложность потребителя |
| JWT на API Gateway | централизованная проверка | доверие шлюзу (defense in depth) |
Вывод модуля: serverless-дизайн — это набор компромиссов между задержкой, стоимостью и сложностью. Free-tier заставляет выбирать асинхронные паттерны (YMQ/YDS) вместо прямых вызовов.
🧩 Проверь себя
🔗 Мостик к Модулю 4
Serverless-дизайн готов: компоненты выбраны, потоки данных описаны, бюджет free-tier посчитан. Но diagrams и таблицы — это не код. В Модуле 4 начнём реализацию: бэкенд на Go по Clean Architecture — слои Domain/Application/Infrastructure, UseCase, Composition Root и правило зависимостей, которое позволит сменить YDB на PostgreSQL, не переписывая 20 файлов.