Очереди, backpressure и борьба с потерянными сообщениями
Для кого: инженеры, которым надоело слушать «просто поставь Kafka». Если у вас не настроены retry, DLQ и мониторинг lag — вы не управляете очередями. Этот урок заставит вас перестать терять сообщения.
Провокация №1: вы знаете, сколько сообщений сейчас в очереди?
- Если вы не можете в течение 60 секунд назвать
queue_depthиconsumer_lag, значит, вы живёте в надежде. Очередь может взорваться в любой момент. - Чек: в Grafana должны быть панели
messages_ready,messages_unacked(RabbitMQ) илиconsumer_lag(Kafka). Нет? Начинайте с этого.
1. Модели доставки: at-most-once, at-least-once, exactly-once
| Модель | Что гарантирует | Цена |
|---|---|---|
| At-most-once | сообщение будет доставлено максимум один раз, может потеряться | быстро, но опасно |
| At-least-once | доставим, пока не подтвердите | дубликаты, нужна идемпотентность |
| Exactly-once | доставим один раз (Kafka transactional, SQS FIFO) | дорого, сложнее, ограничения throughput |
Правило: по умолчанию используйте at-least-once + идемпотентность. Exactly-once только там, где ошибка > стоимости архитектуры.
Провокация №2: вы знаете свой max throughput и lag?
consumer_lag > Xминут? Значит, вы уже не контролируете SLA. В бизнес-час пик пользователь увидит сообщения через час.
2. Backpressure и управление нагрузкой
2.1 Метрики
messages_ready(RabbitMQ) /queue_depth(SQS)consumer_lag(Kafka)processing_time(worker)retry_count,DLQ_rate
2.2 Действия при росте lag
| Lag | Что делаем |
|---|---|
| < 1 мин | норма |
| 1–5 мин | масштабируем воркеры, проверяем hot partition |
| > 5 мин | включаем backpressure (отказываем продюсерам), включаем лидерских инженеров |
| > 30 мин | авария: уведомляем бизнес, режем входящий трафик, DLQ анализируем каждую минуту |
2.3 Backpressure механизмы
- HTTP продюсеры → 429 (Too Many Requests), когда lag > X.
- Limit зкном принятия задач (Bull/BullMQ
limiter). - Kafka: динамический
pause/resumeconsumption.
if (lag > 300000) {
producer.rejectWith429();
consumer.pause();
}Провокация №3: у вас есть idempotent handlers?
- Если нет — at-least-once превращается в «удвоенные платежи». Необсуждаемое требование: каждый handler должен быть идемпотентным.
3. Idempotency паттерны
- Deduplication table:
CREATE TABLE processed_events ( event_id TEXT PRIMARY KEY, processed_at TIMESTAMPTZ DEFAULT now() ); - Внешние ключи: операции обновления по
ON CONFLICT DO NOTHING. - Idempotent APIs: PATCH вместо POST,
INSERT ... ON CONFLICT.
Провокация №4: DLQ — это не помойка
- Dead Letter Queue не для того, чтобы «складировать» сообщения. Там живут баги. Если вы не выгребаете DLQ в течение дня — вы хороните бизнес-события.
4. DLQ дисциплина
| Правило | Описание |
|---|---|
| Timer | очистка DLQ не реже 1 раза в сутки |
| Протокол | каждая запись → JIRA/incident |
| Alarm | DLQ_rate > 0 больше 10 минут — PagerDuty |
4. Практические очереди
4.1 RabbitMQ
- Подтверждения (
ack) обязательны. - Prefetch =
channel.prefetch(10)— иначе один worker задушит others. - Durable queues + persistent messages (но медленнее).
- Management API → мониторинг
messages_ready,messages_unacked.
4.2 Kafka
- Multi-partition = параллельность.
- Consumer group → балансировка.
- Lag =
latest_offset - committed_offset. - Transactions = exactly-once, но цена — сложность.
acks=all,min.insync.replicas=2для durability.
4.3 Bull/BullMQ (Node.js)
- Redis-backed, хорош для задач до 10k/s.
limiter→ ограничение скорости, retry с backoff.stalleddetection: воркер умер → job возвращается в очередь.
Провокация №5: вы тестировали потерю сообщений?
- Убейте воркер на середине обработки. Сообщение вернулось? Если нет — вы теряете данные.
Инцидентная симуляция
# RabbitMQ: имитируем network partition
iptables -A INPUT -p tcp --dport 5672 -j DROP
sleep 30
iptables -D INPUT -p tcp --dport 5672 -j DROPЦель: после восстановления нет «зависших» сообщений, DLQ пуст.
Провокация №6: вы отслеживаете throughput per partition?
- Kafka: одна «горячая» партиция может положить весь consumer. Мониторьте
records-lag-maxпо partition.
5. Архитектурные паттерны
5.1 Producer-Consumer
- Не публикуйте сообщения «на глаз». Логируйте
event_id, входные параметры.
5.2 CQRS + Event sourcing
- Event store → projections → eventual consistency. Нужны реплеи.
5.3 Outbox pattern
- Transactions: запись в БД + событие в outbox → воркер публикует в очередь.
INSERT INTO payments (id, status) VALUES ($1, 'NEW');
INSERT INTO outbox (event_id, payload, status) VALUES ($1, $2, 'pending');Воркер:
const events = await db.query(
"SELECT * FROM outbox WHERE status = $1 LIMIT 100",
["pending"]
);
for (const event of events.rows) {
await publish(event);
await db.query("UPDATE outbox SET status = $1 WHERE event_id = $2", [
"sent",
event.event_id,
]);
}Провокация №7: у вас есть план деградации?
- Очередь начинает расти, вы не успеваете. Что делает API? Отказывает? «Кладёт к себе»? Ответ должен быть заранее готов.
Практика
- Метрики:
- Настройте Grafana:
queue_depth,consumer_lag,DLQ_rate. - Алерт:
consumer_lag > 300s→ PagerDuty.
- Настройте Grafana:
- Idempotency:
- Добавьте таблицу
processed_events. - Покройте handler тестами: две доставки → один результат.
- Добавьте таблицу
- DLQ drill:
- Намеренно киньте невалидное сообщение.
- Проверьте: ушло в DLQ, создан тикет, есть перезапуск.
- Backpressure test:
- Сымитируйте рост lag (замедлите consumer).
- Убедитесь, что продюсер начинает 429/ограничивать отправку.
- Outbox:
- Реализуйте outbox + воркер. Остановите воркер на 1 минуту, потом запустите. Убедитесь, что события публикуются без пропусков.
Безопасность: инцидентные тесты (lag, network drop) проводите на staging. Session tokens, личные данные — никогда не логируйте в DLQ. Удаляйте/обезличивайте перед анализом.
Что дальше
Следующая глава — event-driven архитектура: там заставим вас проектировать события, схемы, contract-тесты и Debezium. Очереди без правильной событийной модели — просто трубы. Готовьтесь.