Перейти к основному содержимому

Асинхронная обработка данных в высоконагруженных системах

Разработчику Архитектору

Асинхронная обработка — способ организовать систему так, чтобы долгая работа не блокировала ответ пользователю.

Клиент получает быстрое подтверждение ("задача принята"). Тяжёлую часть система выполняет позже:

  • в фоновом процессе (воркер);
  • через очередь сообщений;
  • с уведомлением по webhook;
  • при следующем запросе статуса (polling).

Типичные примеры фоновой работы:

  • отправка email и push-уведомлений;
  • генерация PDF-отчёта или выгрузки;
  • обработка и сжатие видео;
  • перенос событий в аналитику (ClickHouse, Elasticsearch);
  • пересчёт рекомендаций и поискового индекса.

Соседние материалы:


Ключевые термины

Перед разбором архитектуры — словарь, который встретится дальше по тексту.

ТерминПростыми словамиПодробнее
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
WebhookHTTP-запрос на URL клиента, когда задача завершенаPolling, SSE, Webhook
SLAДоговорённость о допустимой задержке и доступности сервисаNFR в цифрах
p95 latencyВ 95% случаев ответ быстрее этого порога; "хвост" медленных запросов виден именно здесьЗадержка и throughput
RPSRequests per second — сколько запросов в секунду обрабатывает сервисМасштабируемость
BackpressureОчередь растёт быстрее, чем воркеры успевают разбирать задачи§ backpressure
Eventual consistencyДанные на разных узлах сходятся через короткое время после записиPACELC

Когда вводить асинхронность

Очередь добавляет отдельный компонент в инфраструктуру: кластер брокера, мониторинг, политику повторов, разбор "мёртвых" сообщений. Имеет смысл, когда синхронный путь упирается в измеримый предел.

СимптомЧто происходитТипичное решение
Растёт p95 API из-за редких долгих запросовПул потоков API занят генерацией отчётов по 30 сЗадача уходит в очередь; API отвечает за сотни миллисекунд
Пики трафика кладут все инстансыМежду всплеском запросов и обработкой нет буфераОчередь как амортизатор + autoscaling воркеров (§12 в 141)
Внешний сервис медленный или нестабиленПовтор внутри HTTP-запроса удлиняет ответ клиентуАсинхронный вызов + callback или webhook
На одно событие реагируют несколько подсистемДлинная цепочка синхронных HTTP-вызововТопик событий и несколько подписчиков
Задача должна пережить рестарт APIBackgroundTasks во фреймворке хранит работу только в памяти процессаПерсистентная очередь и подтверждение ack

Синхронный путь уместен, если:

  • операция укладывается в SLA (например, меньше 300 мс) и UI ждёт результат сразу;
  • нужна атомарность в одной БД без распределённого согласования;
  • нагрузка мала, а очередь дороже редкого таймаута;
  • команда пока не готова сопровождать брокер (мониторинг, runbook, дежурства — инциденты).
Антипаттерн "async везде"

Перенос простого 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 storependingrunningdone / failedКлиент не видит прогресс

Stateless API — сервис без локального состояния сессии: любой инстанс может обработать запрос; данные — в БД или кэше (Redis).


Ключевые компоненты

Очереди и брокеры

Брокер сообщений (message broker) временно хранит задачи или события и передаёт их потребителям.

МодельКак устроеноПримеры задачТехнологии
Очередь задачОдно сообщение обычно обрабатывает один воркерОтчёты, письма, превью картинокRabbitMQ, SQS
Журнал событийСообщения хранятся по политике retention; чтение с offsetАналитика, CDC, повторное проигрываниеKafka
Pub/SubОдно событие получают все подписчики топикаOrderCreated → склад, CRM, аналитикаKafka topic, Rabbit fanout

Сравнение брокеров

Подробные гайды — 114, 118, 119.

КритерийRabbitMQApache KafkaAmazon SQSRedis Streams
МодельОчереди, маршрутизация, TTLРазделённый logУправляемая очередь в облакеПоток в памяти
Типичная задачаWorkflow, фоновые jobПотоковая обработка, высокий throughputJob без своего кластераЛёгкие задачи при уже установленном 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 с).

Шаги:

  1. POST /reports — запись reports(id, status=pending) и строка outbox в одной транзакции.
  2. Ответ 202 и { "task_id": "…" }.
  3. Воркер строит CSV/PDF, кладёт в S3 или аналог, обновляет status=ready.
  4. UI опрашивает GET /reports/{id} или слушает SSE.

Обработка видео

  • Загрузка файла — синхронно (multipart → object storage).
  • Транскодинг — цепочка очередей: 1080p720pthumbnailsnotify-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Сложнее инфраструктураИнтерактивные приложения
WebhookPOST на callback URL клиентаБез опросаНужны подпись и идемпотентностьB2B-интеграции

Подробнее — Polling, SSE, Webhook, REST.

HTTP-контракт async API

МетодКодТело ответаСмысл
POST /tasks202 Acceptedtask_id, status_urlЗадача принята, ещё не готова
GET /tasks/{id}200status, progress, resultТекущее состояние
DELETE /tasks/{id}202 / 204Запрос отмены (best-effort)

Практики:

Требования к исходящему 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 после ackAck после 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 = ?
Ключ идемпотентностиУникальный ключ в БД / RedisUNIQUE(message_id) в inbox
Compare-and-setОбновление только из допустимого статусаWHERE status = 'queued'
Ключ внешнего APIПровайдер принимает idempotency keyStripe, SES

Чеклист code review воркера

  1. Есть стабильный message_id или бизнес-ключ в payload?
  2. Запись в БД защищена уникальным constraint?
  3. Вызов внешнего API — после фиксации "уже обработано"?
  4. Повтор с тем же ключом возвращает тот же результат, а не 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 attempts3–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 для поддержки.

Отказоустойчивость брокера

Версионирование схемы

  • новые поля — необязательные (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)Отставание от головы loglag выше 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 проходит через:

  1. access log API;
  2. запись outbox;
  3. publish (offset/partition);
  4. start/finish воркера;
  5. исходящий webhook (если есть).

Антипаттерны

АнтипаттернПочему плохоЧто делать
In-memory queue в APIПотеря при рестартеПерсистентный брокер или job table
Ack до commit в БДПотеря при crashManual 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 с retryRabbitMQ / Redis StreamsЗнакомая эксплуатация, низкая latency
Streaming, replay, много consumersKafkaLog, retention, экосистема
Минимум ops, AWSSQS + Lambda / ECSManaged
Отложенные задачи, Redis уже в стекеBull / Celery / SidekiqБыстрый старт; RAM — лимит
Долгие саги с таймерамиTemporal, CadenceWorkflow как код

Экосистема 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 с кодом входа.

ПодходКак устроено
Отдельные очереди по SLAjobs-critical, jobs-batch
Приоритет в RabbitMQx-max-priority
Отдельный кластер Kafkatransactional и analytics
Weighted fair queueДоля CPU на классы задач

Критичный путь (OTP, оплата) не делит очередь с batch-ETL без записи в ADR.


Деплой воркеров

МодельПлюсМинус
Воркер в том же репо, отдельный entrypointОбщие модели, один CIРиск перепутать деплой API и worker
Отдельный сервис / репоНезависимый scale и релизВерсионирование контрактов
Serverless (Lambda)Оплата за вызовCold start, лимит времени
Kubernetes CronJobBatch по расписаниюНе для постоянного 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)

  1. POST /products/{id}/images → файл в storage, image_id, статус uploaded.
  2. Очередь resize → воркеры 150×150, 800×800, WebP.
  3. Параллельно очередь moderation (ML API).
  4. Статус ready_for_catalog — когда все derivative готовы.

Идемпотентность по ключу (image_id, variant).

Ночной биллинг

Cron в 02:00 кладёт N сообщений в invoices (по контракту). Workers масштабируются; ошибка по одному контракту → DLQ без остановки batch.

Поисковый индекс

Запись в PostgreSQL — синхронно. Elasticsearch — асинхронно через outbox → ProductUpdated. Пользователь 1–2 с может видеть старый снимок в поиске — осознанный trade-off (PACELC).

Импорт CSV

  1. Multipart → S3.
  2. ImportJob + сообщение "начать импорт".
  3. Воркер читает потоком (chunk 5k строк), пишет в staging, обновляет progress_percent.
  4. UI — SSE или polling.

Мультитенантность

УровеньРеализацияКогда
Логическаяtenant_id в payload + ACLБольшинство B2B SaaS
Очередь на tenantjobs-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_readygaugerabbitmq_queue_messages_ready{queue="jobs"}
kafka_consumer_laggaugekafka_consumergroup_lag_sum
job_processing_secondshistogramhistogram_quantile(0.95, …)
outbox_unpublished_totalgaugeSQL count
dlq_messages_totalcounterрост за час

Алерт: queue_messages_ready > 1000 и consume rate меньше publish rate 10 минут.


Миграция на асинхронный путь

ФазаДействиеРиск
1Вынести логику в handler; вызывать синхронноНизкий
2Таблица jobs; poller в том же процессеСредний
3Outbox + отдельные worker-процессыСредний
4Клиент на 202 + pollingКонтракт API
5Scale workers, DLQ, алертыOps

Feature flag для отката без деплоя.


Чек-лист system design interview

Порядок ответа — каркас в 143:

  1. NFR — RPS, задержка job, нужен ли один эффект для пользователя.
  2. Что синхронно в HTTP, что в фоне.
  3. High-level — API, DB, outbox, broker, workers, storage, статус.
  4. Deep dive — идемпотентность, state machine, retry/DLQ, partition key.
  5. Failure modes — падение API, воркера, брокера; дубликаты.
  6. Scale — API и workers отдельно; bottleneck.
  7. 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, p95 job_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 storageLifecycle для артефактов
ОперацииOn-call, мониторинг, апгрейды

Экономия бывает, когда без очереди пришлось бы сильно увеличить число API-инстансов под редкие пики CPU.


Синхронный вызов и событие между сервисами

КритерийHTTP/gRPC напрямуюСобытие через брокер
СвязностьВызывающий знает адрес calleeИздатель не знает подписчиков
Задержка для пользователяСумма всех hopТолько критичный путь синхронен
Отказ зависимости503 клиентуБуфер, retry
СогласованностьПроще read-after-writeEventual consistency
ОтладкаОдин request-idTrace через топик
ВерсииЖёсткий контракт APIНесколько consumer на topic

Три и более синхронных вызова на критичном пути — сигнал вынести хвост в событие. Пример: POST /orders резервирует склад и списывает оплату; OrderPaid запускает письмо, CRM и аналитику.


Мини-postmortem (рост очереди)

Симптом: queue_depth с 200 до 6 000 за час; отчёты опаздывают на 40+ минут.

Хронология:

  1. Деплой воркера: обработка 8 с вместо 2 с.
  2. Пик enqueue +30% от рассылки.
  3. HPA смотрел на CPU, а не на depth; scale с задержкой 10 мин.
  4. Нет DLQ — битый report_id крутится в retry.

Действия: KEDA по depth; алерт oldest_message_age > 300s; DLQ + runbook; canary на воркерах.


CQRS и read-модели

CQRS — разделение модели записи (command) и чтения (query).

  1. Command — принимает намерение, пишет write-модель, публикует событие.
  2. 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 design143
Сквозной кейс outbox + webhooks144
RabbitMQ и Kafka114, 118, 119
Async в коде4.05
Saga, outbox2124
Идемпотентность213
Событийная архитектура2127
Устойчивость2136
Микросервисы8.05 intro
HTTP и статусы118
Чеклист перед внедрением очереди
  1. Что произойдёт при дубликате сообщения?
  2. Где хранится статус для клиента?
  3. Какой лимит retry и куда попадёт poison message?
  4. Как измеряете depth, lag, age?
  5. Как связаны запись в БД и publish (outbox)?
  6. Какой контракт с клиентом — 202, polling, webhook?
  7. Есть ли runbook при росте DLQ?
Содержание