Ctrl K
Урок

Очереди сообщений

Интуиция: кафе с номерками

Представь кафе в обеденный час. Если кассир сам жарит каждый бургер, очередь быстро дойдёт до двери. Поэтому кассир только принимает заказ, выдаёт номерок и вешает чек на ленту. Повара снимают чеки по одному и готовят в своём темпе. Если людей стало больше, можно поставить ещё одного повара. Кассу при этом трогать не нужно.

Лента с чеками и есть очередь сообщений. Кассир здесь продюсер (producer), повара — консьюмеры (consumers). Клиент получает ответ «заказ принят» сразу, а работа делается позже. Это и называется асинхронностью.

Зачем асинхронность

Очередь решает три типовые проблемы:

  • Пики нагрузки. Очередь работает как буфер: принимает всплеск, а воркеры спокойно его разбирают. Без неё пик бьёт напрямую по базе, и она падает.
  • Медленные операции. Отправка письма, генерация PDF, обработка видео, вызов внешнего платёжного API. Пользователь не должен ждать 5 секунд, пока ты рендеришь картинку.
  • Развязка сервисов. Сервис заказов публикует событие «заказ создан» и не знает, кто его читает: склад, аналитика, уведомления. Если сервис уведомлений упал, заказы всё равно продолжают приниматься.
POST /orders202 Acceptedзадачазаписьпосле N неудачКлиентклиентAPI заказовсервисОчередь задачочередьВоркер #1воркерВоркер #2воркерВоркер #3воркерPostgreSQLБДDead Letter Queueочередь
API отвечает сразу, воркеры разбирают очередь в своём темпе

Совет: на собесе проговори код ответа. Асинхронный API возвращает 202 Accepted с id задачи, а статус клиент узнаёт позже (поллингом или вебхуком). Это сразу показывает, что ты понимаешь разницу.

Посчитаем буфер

Допустим, в распродажу приходит 3000 заказов/с в течение 10 минут, а воркеры вместе обрабатывают 1000/с.

Шаг Расчёт Итог
Прирост очереди в пик 3000 − 1000 2000 сообщ./с
Накопится за 10 минут 2000 × 600 с 1,2 млн сообщений
После пика входит обычные 300/с, запас мощности 1000 − 300 700 сообщ./с
Время на разгребание 1 200 000 / 700 ≈ 29 минут

Из таблицы видно, что очередь спасла систему от падения, но задержка обработки выросла до получаса. Если это неприемлемо, нужно автомасштабирование воркеров по длине очереди. Это сильная фраза для собеса.

Очередь задач vs журнал событий

Под словом «очередь» скрываются две разные модели.

Очередь задач (RabbitMQ, SQS) Журнал событий (Kafka)
Что после чтения Сообщение удаляется после ack Остаётся в логе на срок хранения
Кто читает Одну задачу получает один воркер Много независимых групп читают одно и то же
Перечитать прошлое Нельзя Можно: сдвинуть offset назад
Порядок Не гарантирован при нескольких воркерах Гарантирован внутри партиции
Маршрутизация Гибкая (exchange, routing key) Простая: топик и партиция
Типичное применение Фоновые задачи: письма, ресайз картинок Поток событий, аналитика, event sourcing
Пропускная способность Тысячи–десятки тысяч/с Сотни тысяч–миллионы/с

Коротко: RabbitMQ — это «сделай работу», Kafka — это «вот что произошло, читай кто хочет».

Партиции и порядок сообщений

Топик в Kafka делится на партиции. Каждая партиция — это упорядоченный лог, где у каждого сообщения есть номер (offset). Партиции дают параллелизм: их можно раскидать по разным брокерам и читать одновременно.

Порядок гарантирован только внутри одной партиции. Поэтому сообщения, порядок которых важен, отправляют с одинаковым ключом. Партиция выбирается как hash(key) % N. Если ключом сделать order_id, события «создан → оплачен → отправлен» одного заказа попадут в одну партицию и не перепутаются.

создан #70
оплачен #71
аналитика ↓ создан #92
отправлен #73
оплачен #94
уведомления ↓ создан #125
Партиция 0: события заказа #7 идут строго по порядку; две группы читают лог независимо, каждая со своим offset

Consumer groups

Consumer group — это набор консьюмеров, которые вместе читают топик.

  • Внутри группы каждая партиция достаётся ровно одному консьюмеру. Так работа делится между ними.
  • Разные группы читают топик независимо, у каждой свой offset. Аналитика может отставать на час, а уведомления работать в реальном времени.
  • Консьюмеров в группе больше, чем партиций, ставить бессмысленно: лишние будут простаивать. Поэтому число партиций закладывают с запасом, ведь увеличивать его потом больно: меняется hash % N, и ломается порядок.

Dead Letter Queue

Бывает, что сообщение невозможно обработать: битый JSON, удалённый пользователь, баг в коде. Если бесконечно возвращать его в очередь, получится poison message: он блокирует партицию или бесконечно жжёт CPU.

Решение такое. После N неудачных попыток с экспоненциальной паузой сообщение перекладывают в DLQ, отдельную очередь для «проблемных». На неё ставят алерт, разбирают руками и после фикса переигрывают.

retry:
  max_attempts: 5
  backoff: exponential   # 1s, 2s, 4s, 8s, 16s
dead_letter_queue: orders.dlq

Как это звучит на собесе

«Создание заказа делаю асинхронным: API кладёт событие в Kafka и отвечает 202. Ключ партиционирования — order_id, чтобы сохранить порядок событий одного заказа. Партиций 32, с запасом на рост воркеров. Доставка at-least-once, поэтому консьюмеры идемпотентны: проверяют, обработан ли уже этот event_id. Неудачные сообщения после 5 ретраев уходят в DLQ с алертом. Воркеры масштабирую по consumer lag.»

Обязательно назови компромиссы:

  • Eventual consistency. Пользователь какое-то время видит статус «обрабатывается».
  • Сложность. Появляются ещё один компонент, мониторинг лага и отладка распределённых цепочек.
  • Дубли. Сообщение может прийти дважды, нужна идемпотентность.

Частые ошибки

  • Считать, что Kafka гарантирует глобальный порядок. Она гарантирует его только внутри партиции.
  • Забыть про идемпотентность. Exactly-once «из коробки» почти никогда не бывает: ретраи и ребалансировки приносят дубли.
  • Не продумать DLQ. Одно битое сообщение тогда может остановить всю обработку.
  • Ставить очередь везде. Если пользователю нужен ответ прямо сейчас (проверка логина, баланс), синхронный вызов честнее и проще.
  • Делать консьюмеров больше, чем партиций, и ждать роста скорости.

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