Асинхронная обработка данных в высоконагруженных системах
Асинхронная обработка — способ организовать систему так, чтобы долгая работа не блокировала ответ пользователю.
Клиент получает быстрое подтверждение ("задача принята"). Тяжёлую часть система выполняет позже:
- в фоновом процессе (воркер);
- через очередь сообщений;
- с уведомлением по webhook;
- при следующем запросе статуса (polling).
Типичные примеры фоновой работы:
- отправка email и push-уведомлений;
- генерация PDF-отчёта или выгрузки;
- обработка и сжатие видео;
- перенос событий в аналитику (ClickHouse, Elasticsearch);
- пересчёт рекомендаций и поискового индекса.
Соседние материалы:
- 12 концепций распределённой архитектуры — краткая шпаргалка;
- System Design — карта тем — порядок изучения;
- Email-рассылка как распределённая система — сквозной кейс с outbox и webhooks;
- Асинхронная коммуникация — протоколы и брокеры в продакшене;
- Брокеры сообщений — ack, retry, DLQ;
- Асинхронность в коде — event loop,
async/await.
Ключевые термины
Перед разбором архитектуры — словарь, который встретится дальше по тексту.
| Термин | Простыми словами | Подробнее |
|---|---|---|
| API | Программный интерфейс, через который клиент (браузер, мобильное приложение, другой сервис) вызывает ваш backend | Основы интеграционного взаимодействия |
| Воркер (worker) | Отдельный процесс или сервис, который забирает задачи из очереди и выполняет тяжёлую логику | § воркеры |
| Брокер сообщений | Промежуточное хранилище между отправителем и исполнителем задачи (RabbitMQ, Kafka, SQS) | Брокеры |
| Очередь (queue) | Список задач: каждую задачу обычно выполняет один воркер | §4 в 12 концепциях |
| Топик (topic) | Канал для рассылки события нескольким подписчикам | Pub/Sub |
| Publish | Отправить сообщение в брокер | Асинхронная коммуникация |
| Ack (acknowledgment) | Подтверждение воркером, что задача обработана; до ack брокер может отдать задачу снова | Брокеры § ack |
| DLQ (Dead Letter Queue) | Отдельная очередь для "битых" задач, которые не удалось выполнить после нескольких попыток | § DLQ |
| Outbox | Таблица в БД, куда в одной транзакции с бизнес-данными записывается намерение отправить задачу в очередь | § outbox |
| Идемпотентность | Повторный запуск даёт тот же результат, что и первый (важно при двойной доставке) | design/213 |
| Webhook | HTTP-запрос на URL клиента, когда задача завершена | Polling, SSE, Webhook |
| SLA | Договорённость о допустимой задержке и доступности сервиса | NFR в цифрах |
| p95 latency | В 95% случаев ответ быстрее этого порога; "хвост" медленных запросов виден именно здесь | Задержка и throughput |
| RPS | Requests per second — сколько запросов в секунду обрабатывает сервис | Масштабируемость |
| Backpressure | Очередь растёт быстрее, чем воркеры успевают разбирать задачи | § backpressure |
| Eventual consistency | Данные на разных узлах сходятся через короткое время после записи | PACELC |
Когда вводить асинхронность
Очередь добавляет отдельный компонент в инфраструктуру: кластер брокера, мониторинг, политику повторов, разбор "мёртвых" сообщений. Имеет смысл, когда синхронный путь упирается в измеримый предел.
| Симптом | Что происходит | Типичное решение |
|---|---|---|
| Растёт p95 API из-за редких долгих запросов | Пул потоков API занят генерацией отчётов по 30 с | Задача уходит в очередь; API отвечает за сотни миллисекунд |
| Пики трафика кладут все инстансы | Между всплеском запросов и обработкой нет буфера | Очередь как амортизатор + autoscaling воркеров (§12 в 141) |
| Внешний сервис медленный или нестабилен | Повтор внутри HTTP-запроса удлиняет ответ клиенту | Асинхронный вызов + callback или webhook |
| На одно событие реагируют несколько подсистем | Длинная цепочка синхронных HTTP-вызовов | Топик событий и несколько подписчиков |
| Задача должна пережить рестарт API | BackgroundTasks во фреймворке хранит работу только в памяти процесса | Персистентная очередь и подтверждение ack |
Синхронный путь уместен, если:
- операция укладывается в SLA (например, меньше 300 мс) и UI ждёт результат сразу;
- нужна атомарность в одной БД без распределённого согласования;
- нагрузка мала, а очередь дороже редкого таймаута;
- команда пока не готова сопровождать брокер (мониторинг, runbook, дежурства — инциденты).
Перенос простого CRUD в очередь "для масштабируемости" без цифр в требованиях часто добавляет только задержку согласованности и усложняет отладку. Сначала зафиксируйте NFR и профиль нагрузки.
Два уровня асинхронности
Слово "асинхронность" в IT означает два разных механизма. Их часто путают; на практике они дополняют друг друга.
| Уровень | Что происходит | Инструменты | Масштаб |
|---|---|---|---|
| Внутри одного процесса | Пока сервис ждёт ответа от сети или диска, он обрабатывает другие запросы | async/await, event loop, goroutines | Один инстанс API |
| Между процессами и сервисами | Задача сохраняется в брокере; API и воркер работают независимо | Очередь, топик, outbox | Кластер, несколько команд |
Неблокирующий ввод-вывод в приложении
Event loop (цикл событий) — механизм в рантайме (Node.js, Python asyncio, Go), который переключается между запросами, пока один ждёт I/O. Это повышает RPS на одном сервере.
Ограничение: если работа нагружает CPU (рендер PDF, транскодинг видео), одного event loop мало — нужны отдельные воркеры или параллельные вычисления.
Типичная ошибка новичка: контроллер объявлен async, но внутри вызывается синхронная библиотека без пула потоков — event loop блокируется, выигрыша нет. Подробнее — асинхронное выполнение.
Очередь между компонентами
Сообщение записывается на диск брокера (или в реплицированный журнал вроде Kafka). Если API упадёт сразу после публикации, задача дождётся воркера. Это другой уровень надёжности, чем фоновая goroutine или setTimeout в памяти процесса.
На практике часто сочетают оба уровня:
- на входе — неблокирующий HTTP (stateless API);
- на выходе в тяжёлую подсистему — брокер и пул воркеров.
Типовой продакшн-контур
Схема повторяется в отчётах, рассылках, медиа и ETL:
Тот же каркас описан в типовом контуре system design, email-рассылке и очередях в 12 концепциях.
| Компонент | Роль | Если компонента нет |
|---|---|---|
| Stateless API | Принимает запрос, валидирует, фиксирует намерение | — |
| Primary DB | Источник истины по задаче и данным | Статус только в памяти воркера |
| Outbox / job table | Атомарность "данные + задача" | Задачи без записи или записи без задач |
| Брокер | Буфер, повторы, рассылка подписчикам | Потеря задач при рестарте API |
| Workers | Тяжёлая CPU/I/O логика | API не масштабируется по фону |
| Status store | pending → running → done / failed | Клиент не видит прогресс |
Stateless API — сервис без локального состояния сессии: любой инстанс может обработать запрос; данные — в БД или кэше (Redis).
Ключевые компоненты
Очереди и брокеры
Брокер сообщений (message broker) временно хранит задачи или события и передаёт их потребителям.
| Модель | Как устроено | Примеры задач | Технологии |
|---|---|---|---|
| Очередь задач | Одно сообщение обычно обрабатывает один воркер | Отчёты, письма, превью картинок | RabbitMQ, SQS |
| Журнал событий | Сообщения хранятся по политике retention; чтение с offset | Аналитика, CDC, повторное проигрывание | Kafka |
| Pub/Sub | Одно событие получают все подписчики топика | OrderCreated → склад, CRM, аналитика | Kafka topic, Rabbit fanout |
Сравнение брокеров
Подробные гайды — 114, 118, 119.
| Критерий | RabbitMQ | Apache Kafka | Amazon SQS | Redis Streams |
|---|---|---|---|---|
| Модель | Очереди, маршрутизация, TTL | Разделённый log | Управляемая очередь в облаке | Поток в памяти |
| Типичная задача | Workflow, фоновые job | Потоковая обработка, высокий throughput | Job без своего кластера | Лёгкие задачи при уже установленном Redis |
| Порядок сообщений | В пределах одной очереди | В пределах partition | Опциональная FIFO-очередь | В пределах stream |
| Хранение | До ack или по TTL | Дни и недели по настройке | До 14 дней | Ограничено RAM |
| Эксплуатация | Средняя (свой кластер) | Высокая | Низкая (managed) | Низкая; RAM — узкое место |
Выбор фиксируют в ADR с учётом команды, облака, объёма сообщений в секунду и необходимости replay.
Фоновые воркеры
Воркер — процесс или сервис, который:
- подписывается на очередь или топик;
- забирает сообщение;
- выполняет бизнес-логику;
- подтверждает обработку (ack) или откладывает повтор (nack).
Рекомендации по эксплуатации:
- Масштабирование — по длине очереди или consumer lag (KEDA, HPA); CPU API для этого слабый сигнал.
- Prefetch — сколько сообщений воркер берёт заранее; согласовать с временем обработки одного сообщения.
- Graceful shutdown — при SIGTERM перестать брать новые задачи, завершить текущие или вернуть их в очередь.
- Изоляция — тяжёлые job (FFmpeg) в отдельном deployment от лёгких уведомлений.
- Версионирование — поле
schema_versionв payload; новые поля добавлять как необязательные.
Воркер должен быть идемпотентным.
Планировщики и отложенные задачи
| Механизм | Задержка | Пример |
|---|---|---|
| Очередь с TTL / delayed exchange | Секунды и часы | Повтор через 5 мин, напоминание |
| Cron + batch | По расписанию | Ночной отчёт (системное администрирование) |
| Workflow engine (Temporal, Cadence) | Долгие цепочки с таймерами | Бронь с удержанием 15 минут |
Для "выполнить через N секунд" часто хватает TTL в RabbitMQ или таблицы scheduled_jobs с poller.
Publish-Subscribe
Pub/Sub (издатель — подписчик) — паттерн, при котором один факт рассылается нескольким независимым сервисам.
- Очередь задач — каждое письмо из 100 000 обрабатывает ровно один воркер.
- Топик событий — событие "заказ создан" одновременно получают склад, аналитика и CRM.
Подробнее — событийная архитектура, типы взаимодействия.
Outbox, CDC и согласованность с БД
Если API сначала пишет в БД, а потом отдельным шагом шлёт в брокер, возможны рассинхроны:
- БД сохранилась, publish упал — задачи в очереди нет.
- Publish прошёл, БД откатилась — в очереди "лишняя" задача.
Transactional Outbox
Outbox — таблица, куда в той же транзакции, что и бизнес-запись, добавляется строка "нужно опубликовать событие X". Отдельный процесс (relay или poller) читает outbox и шлёт в брокер; после успеха строка помечается опубликованной.
CREATE TABLE outbox (
id BIGSERIAL PRIMARY KEY,
aggregate_id UUID NOT NULL,
event_type TEXT NOT NULL,
payload JSONB NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
published_at TIMESTAMPTZ
);
Скелет для иллюстрации; в проде нужны индексы и политика очистки старых строк.
CDC (Change Data Capture)
CDC — чтение журнала изменений БД (WAL в PostgreSQL) и передача в поток (Debezium → Kafka). Меньше кода в приложении; больше инфраструктуры стриминга. Связь с репликацией.
Inbox на стороне consumer
Inbox — таблица у получателя с уникальным message_id. Повторная доставка того же id не запускает обработку второй раз. Вместе с outbox даёт устойчивость при семантике at-least-once (§ семантика).
Типичные сценарии
| Сценарий | Поведение | Риски | Углубление |
|---|---|---|---|
| Фоновые задачи | 202 Accepted + id; PDF или видео в фоне | Дубликаты, потеря статуса | Паттерны с клиентом, state machine |
| Телеметрия и логи | События в Kafka → хранилище аналитики | Потеря при at-most-once | Пакетная работа |
| Микросервисы | OrderCreated → несколько подписчиков | Распределённая согласованность | Saga |
| ETL / bulk | Данные режут на chunk по 10k строк | OOM, долгие транзакции | Конвейеры |
| Входящий webhook | Быстрый 200, обработка в очереди | Таймаут и повторы у партнёра | Входящие webhooks |
Генерация отчёта
Пользователь нажимает "Скачать отчёт за год". Синхронный путь занимает 40–120 с и упирается в таймаут балансировщика (часто 60 с).
Шаги:
POST /reports— записьreports(id, status=pending)и строка outbox в одной транзакции.- Ответ
202и{ "task_id": "…" }. - Воркер строит CSV/PDF, кладёт в S3 или аналог, обновляет
status=ready. - UI опрашивает
GET /reports/{id}или слушает SSE.
Обработка видео
- Загрузка файла — синхронно (multipart → object storage).
- Транскодинг — цепочка очередей:
1080p→720p→thumbnails→notify-user. - Узкое место — CPU; воркеры на нодах с высоким CPU limit или GPU.
- Статус — конечный автомат на
video_id.
Создание заказа
Синхронно на критичном пути:
- резерв на складе;
- списание оплаты (или Saga с компенсацией).
Асинхронно после события OrderPaid:
- письмо с чеком;
- бонусы;
- аналитика;
- push.
Отдельное событие нужно, чтобы сбой почты не откатывал заказ. См. сценарий User/Order и email-рассылку.
Конечный автомат задачи
Статус фоновой задачи хранят в БД (таблица jobs / tasks). Память воркера для этого недостаточна — при рестарте процесса статус исчезнет.
| Статус | Для пользователя | Действия инженера |
|---|---|---|
pending | "Принято" | Проверить outbox relay |
queued | "В очереди" | Смотреть depth и lag |
running | "Выполняется" | Lease на случай падения воркера |
succeeded | Ссылка на результат | TTL артефакта в хранилище |
failed | Ошибка | Алерт; не бесконечный retry |
dead | "Обратитесь в поддержку" | Разбор DLQ |
Lease (аренда задачи) — при переходе в running воркер пишет locked_until = now() + 5 min. Другой воркер не берёт задачу, пока lock активен. После истечения — повторная доставка.
Аналогичная схема для письма — state machine в email-рассылке.
Паттерны взаимодействия с клиентом
Как клиент узнаёт, что фоновая задача завершена:
| Паттерн | Суть | Плюсы | Минусы | Когда уместен |
|---|---|---|---|---|
| Fire-and-forget | Задача ушла в очередь без статуса для клиента | Минимальная задержка | Нет обратной связи | Метрики, некритичные события |
| Polling | Периодический GET /tasks/{id} | Просто, работает везде | Лишние запросы | Мобильные клиенты, B2B |
| Long polling | Сервер держит соединение до смены статуса | Меньше опросов | Таймауты прокси | Умеренная нагрузка |
| SSE | Поток text/event-stream | Живой прогресс в браузере | Только server → client | Дашборды |
| WebSocket | Двусторонний канал | Чат и статус в одном UI | Сложнее инфраструктура | Интерактивные приложения |
| Webhook | POST на callback URL клиента | Без опроса | Нужны подпись и идемпотентность | B2B-интеграции |
Подробнее — Polling, SSE, Webhook, REST.
HTTP-контракт async API
| Метод | Код | Тело ответа | Смысл |
|---|---|---|---|
POST /tasks | 202 Accepted | task_id, status_url | Задача принята, ещё не готова |
GET /tasks/{id} | 200 | status, progress, result | Текущее состояние |
DELETE /tasks/{id} | 202 / 204 | — | Запрос отмены (best-effort) |
Практики:
- заголовок
Locationс URL статуса; Idempotency-Keyпри повторномPOST— идемпотентность;- коды ошибок и формат — проектирование API.
Требования к исходящему webhook
- подпись тела (HMAC-SHA256), проверка заголовка
X-Signature; - идемпотентность по
event_idу получателя; - быстрый
200 OKи своя очередь на обработку (иначе партнёр шлёт повторы); - timestamp и окно допустимого времени против replay.
Примеры — webhooks ESP, публичный API.
Семантика доставки
Семантика доставки — ответ на вопрос, сколько раз сообщение будет обработано и может ли оно потеряться.
| Гарантия | Смысл | Цена |
|---|---|---|
| At-most-once | Может потеряться; дубликатов нет | Риск для критичных задач |
| At-least-once | Доставят минимум один раз; возможны дубликаты | Нужен идемпотентный воркер |
| Exactly-once (в рамках брокера) | Одна запись в транзакции producer | Сложность, связность компонентов |
| Effectively exactly-once | Пользователь видит один эффект | At-least-once + dedup + идемпотентность |
Подробнее — идемпотентность и семантика доставки.
Где теряются и дублируются сообщения
| Точка сбоя | Риск | Митигация |
|---|---|---|
| API упал до commit БД | Задача не создана | Повтор POST с Idempotency-Key |
| Commit есть, relay не опубликовал | Задача в outbox, не в брокере | Poller/CDC, алерт на "застрявшие" строки |
| Publish OK, crash до ack | Повторная доставка | Идемпотентность |
| Обработка OK, crash до ack | Повторная доставка | Идемпотентность + inbox |
| Ack до commit в БД | Потеря при crash после ack | Ack после commit (manual ack) |
Backpressure и управление нагрузкой
Backpressure — ситуация, когда задачи поступают в очередь быстрее, чем воркеры их разбирают.
Признаки:
- растёт queue depth (число сообщений в очереди);
- растёт age of oldest message (возраст самой старой задачи);
- растёт consumer lag в Kafka;
- нарушается SLA ("отчёт за 5 минут" систематически опаздывает);
- заканчивается память или диск брокера (нет TTL / max-length).
Стратегии:
- добавить воркеры или partition (autoscaling);
- ускорить handler (batch, меньше round-trip к БД);
- throttle на
POST /tasks(rate limiting); - отдельные очереди по приоритету (
critical/batch); - честный ответ "система перегружена" вместо молчаливого lag;
- отброс некритичных событий при перегрузке (только для некритичного трафика).
Идемпотентность
При at-least-once повторная доставка неизбежна. Обработчик должен давать тот же эффект, что при первом запуске.
| Техника | Как работает | Пример |
|---|---|---|
| Естественная идемпотентность | Повтор безопасен сам по себе | SET status = 'done' WHERE id = ? |
| Ключ идемпотентности | Уникальный ключ в БД / Redis | UNIQUE(message_id) в inbox |
| Compare-and-set | Обновление только из допустимого статуса | WHERE status = 'queued' |
| Ключ внешнего API | Провайдер принимает idempotency key | Stripe, SES |
- Методы и ключ идемпотентности
- Кейс дубликата письма — 144
Чеклист code review воркера
- Есть стабильный
message_idили бизнес-ключ в payload? - Запись в БД защищена уникальным constraint?
- Вызов внешнего API — после фиксации "уже обработано"?
- Повтор с тем же ключом возвращает тот же результат, а не 500?
Упорядочивание и партиционирование
| Требование | Решение | Цена |
|---|---|---|
| Строгий порядок всех событий | Одна очередь / один partition | Сложнее масштабировать consume |
| Порядок в рамках одной сущности | Partition key = order_id | "Горячие" ключи перегружают partition |
| Порядок не важен | Много consumers и partitions | Проще горизонтальный scale |
В Kafka порядок гарантирован внутри partition. События одного user_id часто кладут в одну partition, если нужна последовательная обработка профиля.
Head-of-line blocking — одно "застрявшее" сообщение блокирует всю partition. Поэтому нужны DLQ после N попыток и отдельные очереди для тяжёлых job.
Retry, DLQ и poison messages
Retry (повтор) — повторная попытка после ошибки. Часть инженерии устойчивости; не бесконечный цикл while.
| Параметр | Типичное значение | Заметка |
|---|---|---|
| Max attempts | 3–10 | Зависит от побочных эффектов |
| Backoff | Экспоненциальный + jitter (случайный разброс) | Без jitter — "стадный" всплеск повторов |
| DLQ | Отдельная очередь / topic | Ручной разбор, алерт |
Poison message — сообщение, которое всегда падает (битый JSON, несуществующий user_id). Без DLQ оно крутится в retry и блокирует очередь.
| Ситуация | Повторять? |
|---|---|
| Таймаут внешнего API, 5xx | Да |
| 4xx, ошибка валидации | Нет → сразу DLQ |
| Дубликат после успеха | Нет (идемпотентность) |
| Брокер недоступен | Повтор publish из outbox |
Saga и длинные процессы
Когда операция затрагивает несколько сервисов без общей БД, одной очереди мало. Saga — цепочка локальных шагов с компенсацией при сбое (например, отмена брони, если оплата не прошла).
| Тип | Как координируется | Плюс | Минус |
|---|---|---|---|
| Choreography | События в брокере | Меньше центрального сервиса | Сложнее увидеть всю цепочку |
| Orchestration | Центральный координатор | Явный граф шагов | Ещё один сервис |
Пример: бронирование → оплата → письмо. Подробно — Saga. Outbox на каждом шаге связывает commit в БД и публикацию следующего события.
Отладка, безопасность и версии сообщений
Сложность отладки
Цепочка API → брокер → воркер → внешний API → ещё один топик. В логах одного сервиса виден только фрагмент.
Что помогает:
trace_id/correlation_idиз HTTP в payload и логи воркера;- structured logging (JSON) с
task_id,partition,offset; - distributed tracing (OpenTelemetry, Jaeger, Zipkin);
- показ
task_idв UI для поддержки.
Отказоустойчивость брокера
- кластер с репликацией (quorum queues, Kafka ISR);
- мониторинг disk, memory, connections;
- runbook при потере лидера — алгоритмы выбора лидера.
Версионирование схемы
- новые поля — необязательные (backward-compatible);
- schema registry (Avro, Protobuf) для Kafka;
- поле
schema_versionв конверте; - dual write / dual read при миграции.
Безопасность
| Угроза | Мера |
|---|---|
| Подмена сообщения | TLS, ACL на топики (ИБ) |
| PII в топике | Минимальный payload; шифрование at rest |
| Replay webhook | Подпись + timestamp |
| Переполнение очереди | Rate limit, auth на API (уязвимости API) |
Наблюдаемость
Без метрик очередь "слепая": lag замечают пользователи, а не дашборд.
Основные метрики
| Метрика | Зачем смотреть | Алерт |
|---|---|---|
| Queue depth | Нехватка воркеров | depth выше порога N минут |
| Consumer lag (Kafka) | Отставание от головы log | lag выше SLA |
| Age of oldest message | Реальная задержка job | больше 5 мин для transactional |
| Publish rate и consume rate | Накопление | consume меньше publish 10 мин |
| DLQ size | Битые сообщения / баги | любой рост |
| Processing time p95 | Медленный handler | рост после деплоя |
| Retry count | Нестабильная зависимость | всплеск |
| Outbox unpublished | Сломан relay | больше 0 долго |
Сквозной trace
Один task_id проходит через:
- access log API;
- запись outbox;
- publish (offset/partition);
- start/finish воркера;
- исходящий webhook (если есть).
Антипаттерны
| Антипаттерн | Почему плохо | Что делать |
|---|---|---|
| In-memory queue в API | Потеря при рестарте | Персистентный брокер или job table |
| Ack до commit в БД | Потеря при crash | Manual ack после commit |
| Бесконечный retry | Очередь забита | Max attempts + DLQ |
| Огромный payload в сообщении | Нагрузка на брокер и сеть | URL в S3 + metadata |
| Синхронный RPC в цикле consumer | Блокировка partition | Отдельные очереди, batch |
| Одна очередь на всё | Head-of-line, нет приоритетов | Разделение по SLA |
| Нет идемпотентности | Дубликаты после инцидента | inbox + unique keys |
| "Exactly-once" без обоснования | Ложная уверенность | Явный at-least-once + design |
Выбор технологий
| Ситуация | Частый выбор | Почему |
|---|---|---|
| Kubernetes, job с retry | RabbitMQ / Redis Streams | Знакомая эксплуатация, низкая latency |
| Streaming, replay, много consumers | Kafka | Log, retention, экосистема |
| Минимум ops, AWS | SQS + Lambda / ECS | Managed |
| Отложенные задачи, Redis уже в стеке | Bull / Celery / Sidekiq | Быстрый старт; RAM — лимит |
| Долгие саги с таймерами | Temporal, Cadence | Workflow как код |
Экосистема MSA · продакшн-стек
Оценка ёмкости
Грубая формула для планирования воркеров:
время_разбора ≈ (глубина_очереди × среднее_время_задачи) / число_воркеров
Пример: 10 000 задач, 2 с на задачу, 50 воркеров → около 400 с (6,7 мин) в худшем случае без нового притока.
Если приходит 100 msg/s, а обрабатывается 80 msg/s, очередь растёт линейно — нужен scale или throttle.
Конверт сообщения
В очередь кладут конверт (envelope) с метаданными. Сырой JSON всего заказа в сообщение обычно не помещают.
| Поле | Назначение |
|---|---|
message_id | Уникальный id (UUID); dedup, inbox |
correlation_id | Связь с HTTP-запросом / task_id |
event_type | Тип для маршрутизации |
schema_version | Версия контракта |
occurred_at | Время факта (ISO 8601) |
payload | Бизнес-данные |
{
"message_id": "550e8400-e29b-41d4-a716-446655440000",
"correlation_id": "task_8f3a2b",
"event_type": "ReportRequested",
"schema_version": 1,
"occurred_at": "2026-06-15T14:30:00Z",
"payload": {
"report_id": "rpt_991",
"user_id": "usr_42",
"format": "pdf"
}
}
Большие файлы передают по ссылке (S3 key). Base64 в теле сообщения раздувает брокер и сеть.
Приоритеты и несколько очередей
Одна FIFO-очередь на всё — простой старт. Минус: тяжёлый отчёт на 20 минут задерживает SMS с кодом входа.
| Подход | Как устроено |
|---|---|
| Отдельные очереди по SLA | jobs-critical, jobs-batch |
| Приоритет в RabbitMQ | x-max-priority |
| Отдельный кластер Kafka | transactional и analytics |
| Weighted fair queue | Доля CPU на классы задач |
Критичный путь (OTP, оплата) не делит очередь с batch-ETL без записи в ADR.
Деплой воркеров
| Модель | Плюс | Минус |
|---|---|---|
| Воркер в том же репо, отдельный entrypoint | Общие модели, один CI | Риск перепутать деплой API и worker |
| Отдельный сервис / репо | Независимый scale и релиз | Версионирование контрактов |
| Serverless (Lambda) | Оплата за вызов | Cold start, лимит времени |
| Kubernetes CronJob | Batch по расписанию | Не для постоянного consume |
Долгие job (видео) — отдельный deployment, autoscaling по queue depth (Kubernetes).
terminationGracePeriodSeconds в K8s должен превышать worst-case время обработки сообщения, иначе SIGKILL оборвёт работу без ack.
Circuit Breaker в async-контуре
Circuit Breaker (предохранитель) — паттерн, который временно прекращает вызовы к "больному" сервису (§7 в 141).
| Ситуация | Поведение |
|---|---|
| Воркер вызывает внешний API | При состоянии open — nack с delay или retry-очередь |
| Брокер недоступен | Outbox копится; relay повторяет publish |
| Лавина retry на мёртвую зависимость | Exponential backoff + DLQ |
Bulkhead — отдельные пулы соединений к разным внешним API, чтобы сбой одного не исчерпал все исходящие вызовы.
Расширенные кейсы
Пайплайн изображений (e-commerce)
POST /products/{id}/images→ файл в storage,image_id, статусuploaded.- Очередь
resize→ воркеры 150×150, 800×800, WebP. - Параллельно очередь
moderation(ML API). - Статус
ready_for_catalog— когда все derivative готовы.
Идемпотентность по ключу (image_id, variant).
Ночной биллинг
Cron в 02:00 кладёт N сообщений в invoices (по контракту). Workers масштабируются; ошибка по одному контракту → DLQ без остановки batch.
- Пакетная работа
- checkpoint по
contract_id
Поисковый индекс
Запись в PostgreSQL — синхронно. Elasticsearch — асинхронно через outbox → ProductUpdated. Пользователь 1–2 с может видеть старый снимок в поиске — осознанный trade-off (PACELC).
Импорт CSV
- Multipart → S3.
ImportJob+ сообщение "начать импорт".- Воркер читает потоком (chunk 5k строк), пишет в staging, обновляет
progress_percent. - UI — SSE или polling.
Мультитенантность
| Уровень | Реализация | Когда |
|---|---|---|
| Логическая | tenant_id в payload + ACL | Большинство B2B SaaS |
| Очередь на tenant | jobs-tenant-{id} | Жёсткий SLA |
| Кластер на tenant | Отдельный брокер | Enterprise, compliance |
Один tenant не должен забить общую очередь — per-tenant rate limit и квота на параллельные job.
Локальная разработка
| Подход | Плюс | Минус |
|---|---|---|
| Docker Compose (API + Rabbit + worker) | Близко к проду | Тяжелее на ноутбуке |
| Testcontainers в CI | Реальный брокер в тесте | Медленнее unit |
| In-memory queue в dev | Быстрый старт | Не ловит баги ack/retry |
На проде Rabbit, локально только asyncio.create_task — семантика разъедется при первом инциденте.
Минимум parity: outbox, manual ack, DLQ в dev-dashboard (RabbitMQ Management, Kafka UI).
Данные в очереди и compliance
Сообщения часто содержат PII (персональные данные). Очередь — ещё одно хранилище.
- минимальный payload (id вместо полного профиля);
- шифрование at rest (KMS);
- TTL согласован с GDPR;
- логи без полного тела сообщения.
Для аудита в конверте: occurred_at, actor_id, correlation_id.
Дерево решений
Пример метрик для дашборда
| Метрика | Тип | Идея PromQL |
|---|---|---|
queue_messages_ready | gauge | rabbitmq_queue_messages_ready{queue="jobs"} |
kafka_consumer_lag | gauge | kafka_consumergroup_lag_sum |
job_processing_seconds | histogram | histogram_quantile(0.95, …) |
outbox_unpublished_total | gauge | SQL count |
dlq_messages_total | counter | рост за час |
Алерт: queue_messages_ready > 1000 и consume rate меньше publish rate 10 минут.
Миграция на асинхронный путь
| Фаза | Действие | Риск |
|---|---|---|
| 1 | Вынести логику в handler; вызывать синхронно | Низкий |
| 2 | Таблица jobs; poller в том же процессе | Средний |
| 3 | Outbox + отдельные worker-процессы | Средний |
| 4 | Клиент на 202 + polling | Контракт API |
| 5 | Scale workers, DLQ, алерты | Ops |
Feature flag для отката без деплоя.
Чек-лист system design interview
Порядок ответа — каркас в 143:
- NFR — RPS, задержка job, нужен ли один эффект для пользователя.
- Что синхронно в HTTP, что в фоне.
- High-level — API, DB, outbox, broker, workers, storage, статус.
- Deep dive — идемпотентность, state machine, retry/DLQ, partition key.
- Failure modes — падение API, воркера, брокера; дубликаты.
- Scale — API и workers отдельно; bottleneck.
- Observability — depth, lag, DLQ, trace_id.
Уточняющие вопросы интервьюеру:
- нужен ли прогресс в реальном времени;
- допустима ли потеря некритичных событий;
- нужен ли строгий порядок по
order_id.
Пример ответа (сервис отчётов)
NFR: 10k отчётов/день, пик 500/ч, p95 API <300 мс, отчёт за 15 мин, дубликат PDF недопустим.
Схема: Client → LB → API → PostgreSQL + outbox → RabbitMQ → workers → S3 → обновление tasks.status. Клиент polling GET /tasks/{id}.
Детали:
- outbox в той же TX, что
INSERT INTO reports; UNIQUE(report_id)в inbox воркера;- DLQ после 5 retry;
- метрики
queue_depth, p95job_duration,dlq_size.
Отказы: worker упал — сообщение вернётся после visibility timeout; Rabbit недоступен — растёт outbox, алерт; S3 down — retry, без ack.
Бюджет задержки
| Этап | Синхронный путь (job 60 с) | Асинхронный путь |
|---|---|---|
| Приём запроса | 50 мс | 50 мс |
| Запись намерения | — | 30 мс (DB + outbox) |
| Ответ клиенту | 60 000 мс | 80 мс (202) |
| Генерация PDF | в HTTP-потоке | 45 с в worker |
| Результат | в теле ответа | polling / webhook |
В async-модели две задержки: быстрый ack и отдельно время до succeeded. UX должен это показывать ("отчёт готовится").
Стоимость и FinOps
| Статья | Комментарий |
|---|---|
| Брокер | Kafka с длинным retention дороже SQS pay-per-request |
| Workers 24/7 | Постоянный pool или scale-to-zero на Lambda |
| Object storage | Lifecycle для артефактов |
| Операции | On-call, мониторинг, апгрейды |
Экономия бывает, когда без очереди пришлось бы сильно увеличить число API-инстансов под редкие пики CPU.
Синхронный вызов и событие между сервисами
| Критерий | HTTP/gRPC напрямую | Событие через брокер |
|---|---|---|
| Связность | Вызывающий знает адрес callee | Издатель не знает подписчиков |
| Задержка для пользователя | Сумма всех hop | Только критичный путь синхронен |
| Отказ зависимости | 503 клиенту | Буфер, retry |
| Согласованность | Проще read-after-write | Eventual consistency |
| Отладка | Один request-id | Trace через топик |
| Версии | Жёсткий контракт API | Несколько consumer на topic |
Три и более синхронных вызова на критичном пути — сигнал вынести хвост в событие. Пример: POST /orders резервирует склад и списывает оплату; OrderPaid запускает письмо, CRM и аналитику.
Мини-postmortem (рост очереди)
Симптом: queue_depth с 200 до 6 000 за час; отчёты опаздывают на 40+ минут.
Хронология:
- Деплой воркера: обработка 8 с вместо 2 с.
- Пик enqueue +30% от рассылки.
- HPA смотрел на CPU, а не на depth; scale с задержкой 10 мин.
- Нет DLQ — битый
report_idкрутится в retry.
Действия: KEDA по depth; алерт oldest_message_age > 300s; DLQ + runbook; canary на воркерах.
CQRS и read-модели
CQRS — разделение модели записи (command) и чтения (query).
- Command — принимает намерение, пишет write-модель, публикует событие.
- Projector — consumer обновляет read-модель (таблица, Elasticsearch, Redis).
После POST на GET данные могут обновиться с задержкой в секунды. UI — optimistic update или индикатор "обновляется".
| Вопрос продукта | Ответ |
|---|---|
| Почему счётчик не сразу +1? | Read model отстаёт |
| Нужна мгновенная согласованность? | Read-after-write из primary — дороже |
| События в неправильном порядке? | Partition по aggregate_id, version в событии |
FAQ
Вопрос. Чем async в коде отличается от очереди?
Ответ. Async I/O — внутри одного процесса; задача пропадёт при рестарте сервера. Очередь — в брокере между процессами; задача переживёт рестарт API. См. два уровня асинхронности и раздел 4.05.
Вопрос. Нужен ли Kafka для 1000 писем в день?
Ответ. Обычно нет. Достаточно outbox, RabbitMQ или SQS и воркеров. Kafka уместен при потоковой аналитике, replay и множестве consumers на один поток.
Вопрос. Как тестировать воркеры?
Ответ. Unit-тест handler без брокера; интеграционный тест с Testcontainers; дважды один payload — проверка идемпотентности. Тестирование, нагрузочное тестирование.
Вопрос. Celery / Sidekiq — это архитектура?
Ответ. Это библиотеки воркеров поверх брокера (часто Redis). Outbox, семантика доставки и DLQ по-прежнему проектируете вы.
Вопрос. Можно ли без брокера — только таблица jobs в PostgreSQL?
Ответ. Да при умеренной нагрузке: SELECT … FOR UPDATE SKIP LOCKED, poller-воркеры. Fan-out и replay слабее, чем у Kafka; зато меньше компонентов. SQL, управление РСУБД.
Вопрос. Что такое visibility timeout в SQS?
Ответ. Время, пока сообщение скрыто от других consumers после получения. Если не подтвердить обработку — сообщение снова станет видимым (аналог nack + retry). Должно быть больше worst-case времени обработки.
Вопрос. Eventual consistency — это баг?
Ответ. Нет, если это записано в требованиях. Баг — когда продукт обещает мгновенную согласованность без синхронного чтения после записи. PACELC.
Вопрос. Где граница между очередью и data pipeline?
Ответ. Очередь задач — "выполни X один раз". Pipeline — непрерывный поток с checkpoint. Конвейеры данных, пакетная работа.
Вопрос. Нужен ли API Gateway перед async API?
Ответ. Не обязателен на старте. Полезен для auth, rate limit и TLS. Тяжёлую работу gateway не выполняет — только POST и 202. API Gateway в 12 концепциях.
См. также
| Тема | Материал |
|---|---|
| Шпаргалка по инфраструктурным идеям | 141 |
| System design | 143 |
| Сквозной кейс outbox + webhooks | 144 |
| RabbitMQ и Kafka | 114, 118, 119 |
| Async в коде | 4.05 |
| Saga, outbox | 2124 |
| Идемпотентность | 213 |
| Событийная архитектура | 2127 |
| Устойчивость | 2136 |
| Микросервисы | 8.05 intro |
| HTTP и статусы | 118 |
- Что произойдёт при дубликате сообщения?
- Где хранится статус для клиента?
- Какой лимит retry и куда попадёт poison message?
- Как измеряете depth, lag, age?
- Как связаны запись в БД и publish (outbox)?
- Какой контракт с клиентом — 202, polling, webhook?
- Есть ли runbook при росте DLQ?