Очередь обработки задач

На интервью на лида хороший ответ должен звучать не как “я возьму RabbitMQ и
всё”, а как проектирование механики надёжного выполнения задач: постановка
задачи, хранение состояния, воркеры, повторные попытки, идемпотентность,
мониторинг, dead letter queue.

Хороший ответ можно строить так.


1. Сначала уточняю требования

Я бы начал с уточнения, что именно значит “очередь обработки задач”.

Например:

  • какие задачи обрабатываем: отправка email, генерация отчётов, обработка
    файлов, транскрибация аудио, импорт данных;
  • задачи быстрые или долгие;
  • нужна ли строгая очередность;
  • допустима ли повторная обработка;
  • какой SLA по времени обработки;
  • сколько задач в секунду ожидается;
  • можно ли потерять задачу;
  • нужны ли приоритеты;
  • нужна ли отложенная обработка;
  • нужна ли отмена задачи;
  • нужно ли показывать пользователю статус.

Потом фиксирую базовую модель:

Система должна принимать задачи, сохранять их надёжно, раздавать воркерам,
отслеживать статус, повторять при временных ошибках и изолировать
окончательно упавшие задачи в dead letter queue.


2. Основные компоненты

Я бы спроектировал систему из таких частей:

Client / API
    |
    v
Task API Service
    |
    v
Database / Queue Storage
    |
    v
Message Broker / Queue
    |
    v
Workers
    |
    v
External services / Processing logic

Компоненты

Task API Service
Принимает запрос на создание задачи, валидирует данные, создаёт запись в базе,
публикует сообщение в очередь.

Task Storage
Хранит состояние задачи: PENDING, IN_PROGRESS, COMPLETED, FAILED,
RETRYING, CANCELLED.

Message Queue / Broker
RabbitMQ, Kafka, SQS, Redis Streams или другой брокер. Он нужен для
буферизации нагрузки и раздачи задач воркерам.

Workers
Отдельные процессы, которые забирают задачи из очереди, выполняют обработку и
обновляют статус.

Dead Letter Queue
Очередь для задач, которые не удалось обработать после нескольких попыток.

Monitoring / Observability
Метрики, логи, трассировка, алерты.


3. Жизненный цикл задачи

Типичный flow:

1. Client отправляет запрос на создание задачи.
2. API создаёт запись в БД со статусом PENDING.
3. API публикует task_id в очередь.
4. Worker забирает task_id.
5. Worker переводит задачу в IN_PROGRESS.
6. Worker выполняет обработку.
7. При успехе задача становится COMPLETED.
8. При ошибке задача уходит на retry.
9. После превышения лимита retry задача становится FAILED и уходит в DLQ.

Важно: в очередь лучше класть не весь большой payload, а task_id.
Сам payload, параметры и статус лучше хранить в базе или object storage.


4. Пример модели данных

tasks
-----
id
type
status
payload_location / payload_json
result_location / result_json
priority
attempt_count
max_attempts
scheduled_at
started_at
finished_at
created_at
updated_at
error_message
idempotency_key

Для больших данных — например файлов, аудио, отчётов — payload не кладём прямо
в БД или очередь. Сохраняем файл в S3/object storage, а в задаче храним
ссылку.


5. API

Минимальный API:

POST /tasks
GET /tasks/{id}
POST /tasks/{id}/cancel
GET /tasks?status=FAILED

Создание задачи:

{
  "type": "transcribe_audio",
  "payload": {
    "file_id": "abc123",
    "language": "ru"
  },
  "idempotency_key": "user-123-file-abc123"
}

Ответ:

{
  "task_id": "task-789",
  "status": "PENDING"
}

Получение статуса:

{
  "task_id": "task-789",
  "status": "IN_PROGRESS",
  "attempt_count": 1
}

6. Надёжность

Самое важное в такой системе — не просто “обработать задачу”, а сделать это
надёжно.

At-least-once delivery

Обычно я бы закладывал модель at-least-once delivery.

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

Поэтому система должна быть готова к повторной обработке.

Идемпотентность

Каждая задача должна быть идемпотентной либо через idempotency_key, либо
через проверку состояния.

Например, если задача уже COMPLETED, воркер не должен выполнять её второй
раз.

if task.status == COMPLETED:
    ack message
    return

Если задача отправляет email или списывает деньги, нужно особенно аккуратно
защищаться от повторного выполнения.


7. Идемпотентность

Идемпотентность — не дополнительная опция, а базовое свойство надёжной
обработки.

Проблемы, которые нужно учитывать:

  • клиент повторил создание задачи;
  • сеть оборвалась;
  • клиент не получил ответ;
  • сообщение пришло в очередь дважды;
  • воркер упал после выполнения, но до ack;
  • внешний сервис ответил таймаутом, хотя операция уже выполнилась.

Что помогает:

idempotency_key
unique(owner_id, idempotency_key)
if task.status == COMPLETED:
    return current state
processing job unique(task_id, job_type)

Хорошая формулировка:

“Я бы проектировал обработку как at-least-once, но идемпотентную. Очередь и
воркеры могут доставлять сообщение повторно, поэтому duplicate event не
должен создавать duplicate task result.”


8. Lease и visibility timeout

Если воркер взял задачу и умер, задача не должна зависнуть навсегда в
IN_PROGRESS.

Для этого нужен механизм lease / visibility timeout:

Worker берёт задачу → получает lease на 5 минут.
Если успел завершить → ack.
Если умер → lease истёк → задача снова доступна другому worker.

В SQS это называется visibility timeout.
В собственной реализации можно хранить locked_until.

Пример:

status = IN_PROGRESS
locked_by = worker-1
locked_until = now + 5 minutes

Если locked_until истёк, другой воркер может подобрать задачу.


9. Retry logic

Ошибки надо делить на временные и постоянные.

Временные ошибки

  • внешний сервис недоступен;
  • timeout;
  • rate limit;
  • network error;
  • временная ошибка БД.

Для них делаем retry с backoff:

1-я попытка: сразу
2-я попытка: через 30 секунд
3-я попытка: через 2 минуты
4-я попытка: через 10 минут
5-я попытка: через 1 час

Постоянные ошибки

  • некорректный payload;
  • файл не найден;
  • неверный формат;
  • нет прав доступа;
  • задача логически невозможна.

Такие задачи не нужно бесконечно гонять по retry. Их лучше сразу переводить в
FAILED.


10. Dead Letter Queue

Если задача упала после max_attempts, она попадает в DLQ.

DLQ нужна не просто как мусорка, а как диагностический слой:

task_id
task_type
payload reference
last_error
attempt_count
failed_at
worker_id

Потом можно:

  • посмотреть причину падения;
  • переотправить задачу вручную;
  • исправить bug и requeue;
  • построить алерт, если DLQ резко растёт.

11. Масштабирование

Система масштабируется горизонтально:

Queue depth растёт → добавляем workers.
Workers не успевают → увеличиваем consumer group / replicas.
Один тип задач тяжёлый → выносим его в отдельную очередь.

Я бы разделял очереди по типам нагрузки:

high_priority_queue
default_queue
low_priority_queue
long_running_queue
external_api_limited_queue

Например, транскрибация аудио и отправка email — это разные профили нагрузки.
Их лучше не мешать в одной очереди, иначе тяжёлые задачи могут забить лёгкие.


12. Приоритеты

Если нужны приоритеты, есть несколько вариантов:

Вариант 1: отдельные очереди

critical_queue
normal_queue
background_queue

Воркеры сначала читают critical, потом normal, потом background.

Вариант 2: priority queue

Некоторые брокеры поддерживают приоритеты внутри очереди.

Но я бы осторожно использовал priority queue, потому что можно получить
starvation — низкоприоритетные задачи никогда не выполняются.


13. Ordering

Если нужна строгая очередность, это резко усложняет дизайн.

Например, если задачи одного пользователя должны выполняться по порядку, можно
делать partitioning:

partition_key = user_id

Все задачи одного пользователя идут в одну partition / shard, а разные
пользователи обрабатываются параллельно.

Если строгий порядок не нужен, лучше его не обещать.

Фраза для интервью:

Я бы отдельно уточнил требование к ordering. Если строгий порядок не нужен,
я бы не закладывал его, потому что он снижает параллелизм и усложняет
восстановление после ошибок.


14. Exactly-once

Если спросят про exactly-once, хороший ответ:

В распределённых системах я бы не обещал настоящий exactly-once processing.
На практике я бы строил at-least-once delivery плюс идемпотентную обработку.
Для бизнес-операций использовал бы транзакции, idempotency keys, уникальные
constraints и outbox pattern.

То есть не говорить “мы гарантируем exactly once”, если система реально этого
не гарантирует.


15. Transactional outbox

Важный advanced-момент.

Проблема:

1. API записал задачу в БД.
2. API попытался отправить сообщение в очередь.
3. Между этими действиями сервис упал.

Тогда задача есть в БД, но сообщения в очереди нет.

Или наоборот:

1. Сообщение ушло в очередь.
2. Запись в БД не сохранилась.

Чтобы избежать рассинхронизации, можно использовать transactional outbox.

Схема:

В одной транзакции:
- создаём task
- создаём outbox_event

Отдельный publisher читает outbox_events и публикует их в брокер.
После успешной публикации помечает event как published.

Это даёт надёжную публикацию событий.


16. Backpressure

Если задач приходит больше, чем система может обработать, нужно не падать, а
управлять давлением.

Механизмы:

  • лимит на создание задач;
  • rate limiting по пользователю;
  • ограничение размера очереди;
  • autoscaling workers;
  • graceful degradation;
  • приоритеты;
  • отложенная обработка;
  • отказ с понятной ошибкой, если система перегружена.

Например:

429 Too Many Requests

или

503 Service Temporarily Unavailable

если очередь достигла критического размера.


17. Observability

Нужны метрики:

queue_depth
oldest_message_age
tasks_created_total
tasks_completed_total
tasks_failed_total
retry_count
dlq_size
processing_time_p95
processing_time_p99
worker_error_rate
worker_utilization

Алерты:

DLQ > threshold
oldest message age > SLA
failure rate вырос
очередь растёт быстрее, чем обрабатывается
нет активных воркеров

Логи должны содержать:

task_id
correlation_id
worker_id
attempt_count
status transition
error details

18. Security

Если задачи содержат пользовательские данные:

  • проверяем права на создание и чтение задачи;
  • worker не должен обрабатывать чужие данные без проверки доступа;
  • чувствительные payload лучше хранить зашифрованно;
  • в логах не должно быть персональных данных;
  • result должен быть доступен только владельцу.

19. Что выбрать: RabbitMQ, Kafka, SQS, Redis?

На интервью можно сказать так:

RabbitMQ

Хорош для классических task queues:

  • ack/nack;
  • routing;
  • retry;
  • DLQ;
  • consumer-based processing.

SQS

Хорош для cloud-native решения:

  • managed;
  • visibility timeout;
  • DLQ;
  • autoscaling;
  • меньше операционной нагрузки.

Kafka

Хорош, если это больше event streaming:

  • большой throughput;
  • replay;
  • ordering per partition;
  • event log.

Но Kafka не всегда лучший выбор именно для task queue, особенно если нужны
индивидуальные retries, delayed jobs и простая DLQ-механика.

Redis Streams / BullMQ

Хорош для более лёгких систем, но надо аккуратно смотреть на durability и
operational risk.

Фраза:

Для обычной очереди фоновых задач я бы выбрал RabbitMQ или SQS. Kafka взял
бы, если задача ближе к event streaming и нужен replay событий.


20. Итоговая архитектура и consistency

                ┌──────────────┐
                │    Client    │
                └──────┬───────┘
                       │
                       v
                ┌──────────────┐
                │  Task API    │
                └──────┬───────┘
                       │
          ┌────────────┴────────────┐
          v                         v
   ┌──────────────┐          ┌──────────────┐
   │   Task DB    │          │ Outbox Table │
   └──────────────┘          └──────┬───────┘
                                    │
                                    v
                             ┌──────────────┐
                             │  Publisher   │
                             └──────┬───────┘
                                    │
                                    v
                             ┌──────────────┐
                             │ Message Queue│
                             └──────┬───────┘
                                    │
                    ┌───────────────┼───────────────┐
                    v               v               v
              ┌─────────┐     ┌─────────┐     ┌─────────┐
              │ Worker  │     │ Worker  │     │ Worker  │
              └────┬────┘     └────┬────┘     └────┬────┘
                   │               │               │
                   v               v               v
              ┌──────────────────────────────┐
              │ External services / Storage  │
              └──────────────────────────────┘

Система почти всегда eventually consistent.

Например:

task created
message published later
worker picks it up later
external service completes later
status visible later

Важно честно разделить states:

PENDING
IN_PROGRESS
COMPLETED
FAILED
RETRYING
CANCELLED

Если требуется строгая синхронность, нужно отдельно сказать, что это уже
другая задача и другой класс отказов.


21. Короткий сильный ответ на интервью

Можно сказать так:

“Я бы спроектировал очередь обработки задач как надёжный асинхронный
pipeline. API принимает задачу, сохраняет её в БД, через transactional
outbox публикует событие в брокер. В очередь кладётся task_id, а не
большой payload. Воркеры забирают задачи, переводят их в IN_PROGRESS,
выполняют обработку и обновляют статус. Доставка сообщений проектируется
как at-least-once, поэтому обработка должна быть идемпотентной. Для сбоев
используются retry с exponential backoff, lease/visibility timeout, а после
превышения лимита попыток задача попадает в dead letter queue. Система
масштабируется горизонтально через увеличение числа воркеров и разделение
очередей по типам задач или приоритетам. Отдельно закладываю
observability: queue depth, age of oldest message, retry rate, DLQ size,
latency p95/p99 и алерты. Exactly-once я бы не обещал, вместо этого
использовал бы идемпотентность, уникальные ключи и транзакционные границы.”


22. Самая сильная финальная формулировка

“Очередь обработки задач — это не просто брокер сообщений. Это механизм
надёжного выполнения фоновой работы: принять задачу, сохранить её состояние,
безопасно раздать воркерам, повторить при временных ошибках, изолировать
окончательно упавшие задачи и при этом не потерять управляемость в
production. Главные риски здесь — потерять задачу, выполнить её дважды,
зависнуть в IN_PROGRESS или ослепнуть без observability. Поэтому я бы
строил систему вокруг task DB, брокера, идемпотентных воркеров, retry,
DLQ, outbox и строгого контроля состояния.”

Прокрутить вверх