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.

Схема архитектуры

Serverless-архитектура чата: компоненты и поток данных

На рисунке: клиент (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/IOPS1 000 000 RU/мес
YMQочередь событий (SQS-совместимая)decoupling, retry, at-least-once, DLQ100 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 мс):

  1. Клиент отправляет сообщение (REST POST /messages через API Gateway).
  2. API Gateway проверяет JWT и маршрутизирует в Ingestion Function.
  3. Ingestion валидирует, сохраняет сообщение в YDB, публикует событие message_created в YMQ.
  4. 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) идёт в фоне и переживает пики нагрузки.

Инициализация клиента

  1. Пользователь открывает URL → запрос идёт в Object Storage.
  2. Object Storage отдаёт index.html, JS-бандл (Vite), стили.
  3. PWA регистрируется и кэширует ресурсы для офлайн.
  4. Клиент подключается по WebSocket к API Gateway и проходит JWT-аутентификацию.

Преимущества Object Storage: бесконечное масштабирование статики, интеграция с CDN, дешёвое хранение, высокая доступность. Отдельно от compute — статику можно обновлять без редеплоя бэкенда.

Почему очереди (YMQ/YDS) необходимы

  1. Отказоустойчивость: если YDB перегружена, сообщение не теряется — ждёт в очереди.
  2. Декаплинг: Ingestion быстро отвечает, Worker обрабатывает в своём темпе.
  3. Буфер под пики: YMQ примет любое количество сообщений, Worker разгребает постепенно.
  4. Повторы: при сбое Worker-а сообщение возвращается в очередь и переобрабатывается.
  5. Гарантии доставки: at-least-once + идемпотентный потребитель + DLQ для неисправимых.

YMQ vs YDS: YMQ — очередь задач (триггерит функцию), YDS — стрим для real-time fan-out подписчикам (WS Gateway читает поток). Это два разных паттерна: «задача → обработчик» и «событие → подписчики».

Преимущества архитектуры

Производительность

МетрикаЗначениеОписание
Latency Ingestion~50 мсБыстрый ответ клиенту (202 Accepted)
Latency Broadcast1–2 сАсинхронная доставка через очередь
Throughput1000+ msg/секОграничено YDB, а не кодом
Cold Start<500 мсПрогрев Cloud Functions
WebSocketpersistentПостоянное соединение без 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 при подключении
APIAPI 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 + auth100 0000₽0
Cloud Functions (Auth + Ingestion + Workers)~80 000 вызовов1 000 0000₽0
YDB~120 000 операций1 000 0000₽0
YMQ~30 000 запросов100 0000₽0
YDS< 2 млн единиц данных2 000 0000₽0
Serverless Containers (WS Gateway)20 клиентов × 30 дней1 000 0000₽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 без GPUCompute Cloud с GPU
Долгие операции (> 15 мин)Таймаут функцийContainer/VM

Идеально для: event-driven систем (чат, уведомления, webhooks), непредсказуемой нагрузки, MVP/стартапов (быстрый старт за ₽0), микросервисов с независимым масштабированием.

Best Practices

Подтверждайте обработку в YMQ. Вернули ошибку — сообщение вернётся в очередь; успех — удаляется.

go
func handle(msg Message) error {
    if err := process(msg); err != nil {
        return err
    }
    return nil
}

Идемпотентность по message_id. YMQ может доставить сообщение дважды — повторная обработка не должна создавать дубль.

go
func 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) вместо прямых вызовов.


🧩 Проверь себя

1. YDS (Yandex Data Streams) в архитектуре — для чего?
2. Сколько компонентов в спроектированной serverless-архитектуре чата (по курсу)?
3. Какой сервис хранит секреты (JWT-ключи, пароль БД) в serverless?
4. Serverless Container vs Cloud Functions — когда Container?
5. ALB (Application Load Balancer) L7 в serverless — зачем?

🔗 Мостик к Модулю 4

Serverless-дизайн готов: компоненты выбраны, потоки данных описаны, бюджет free-tier посчитан. Но diagrams и таблицы — это не код. В Модуле 4 начнём реализацию: бэкенд на Go по Clean Architecture — слои Domain/Application/Infrastructure, UseCase, Composition Root и правило зависимостей, которое позволит сменить YDB на PostgreSQL, не переписывая 20 файлов.