awesome-everything EN
↑ Обратно к восхождению

Очереди, потоки, события

SQS visibility timeout, DLQ и outbox pattern

Суть Как работает SQS visibility timeout per-message, heartbeat pattern для долгих consumer-ов, дисциплина DLQ redrive и outbox pattern для надёжной producer-side публикации.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на middle-высоте — в небе
◷ 11 min

DLQ тихо накопила 50 000 сообщений за три дня, пока on-call команда смотрела другой дашборд. Все они были edge-case заказами, требующими ручной проверки. Когда наконец запустили redrive-all без rate limit, source queue получила 50 000 сообщений за одну секунду и downstream сервис лёг.

Механика SQS visibility timeout

У SQS нет ack/reject RPC. Вместо этого — visibility timeout. Когда consumer получает сообщение, SQS записывает:

  • Per-message timestamp
  • Receipt handle consumer-а (уникальный токен для этой доставки)

В течение visibility timeout ни один другой consumer не может получить сообщение. Если consumer вызывает DeleteMessage с правильным receipt handle до истечения таймаута — сообщение исчезает. Если таймаут срабатывает первым, сообщение снова видно всем consumer-ам — at-least-once delivery.

Ключевой pitfall: visibility timeout сбрасывается на queue default при каждой redelivery, не на значение, установленное через ChangeMessageVisibility в предыдущей доставке. Если нормальный путь consumer-а расширяется до 90с, но queue default — 30с, каждое redelivered сообщение начинает с 30с и может истечь снова до того, как медленный consumer завершит.

Rule of thumb: queue default visibility = 6x среднее processing time.

Heartbeat pattern для переменного processing time

Для consumer-ов с processing time, варьирующимся от 1с до 5 минут в зависимости от payload, heartbeat pattern разделяет timeout от worst-case:

  1. При получении сообщения запусти background heartbeat thread.
  2. Каждые visibility_timeout / 3 секунд вызывай ChangeMessageVisibility(receipt_handle, new_timeout).
  3. Если consumer падает, heartbeat-ы останавливаются. Timeout истекает естественно. Брокер передоставляет.
  4. Если consumer завершает — отменяй heartbeat и DeleteMessage.

Queue default теперь покрывает только время между ReceiveMessage и первым heartbeat, а не всё processing. Heartbeat также детектирует deadlocked consumer-ов: если processing thread stuck (не упал), heartbeat-ы в конечном счёте останавливаются, и другой worker восстанавливает сообщение.

Dead-letter queues и дисциплина redrive

Без DLQ poison-pill сообщение — payload, постоянно крашащий consumer из-за бага или malformed данных — блокирует очередь вечно. SQS передоставляет бесконечно, сжигая ресурсы и мешая прогрессу.

DLQ — отдельная очередь, куда SQS переносит сообщения после maxReceiveCount провальных попыток:

  • Рекомендуемый maxReceiveCount: 5–10. Установка 1 или 2 отправляет transient failures (flaky downstream, brief DB timeout) в DLQ немедленно — почти всё выглядит как poison pill. Слишком высокий — Consumer молотит реальные poison pills долго до quarantine.
  • DLQ retention: максимум 14 дней (лимит SQS). Сообщения старше retention теряются навсегда.
Дисциплина DLQ redrive
1Snapshot DLQ в S3 (manifest + сообщения) до любого redrive
2Аудит 20 случайных сэмплов на исправленном коде на staging
3Rate-limited redrive: 1–10 msgs/s через StartMessageMoveTask
!Никогда не bulk-redrive без rate limit — риск stampede

Outbox pattern: producer-side надёжность

Даже публикация сообщения в брокер at-least-once: producer может ретраить на timeout и брокер получит две копии. Хуже — dual-write problem: приложение апдейтит БД и публикует сообщение как одну логическую операцию. Если БД коммитит, но publish в брокер падает, брокер никогда не видит событие. Тихая потеря данных.

Outbox pattern фиксит это транзакционной outbox-таблицей:

  1. В той же DB-транзакции, что и бизнес-апдейт, INSERT строку в таблицу outbox: (id, payload, status='pending').
  2. COMMIT: и бизнес-апдейт, и outbox row успешны атомарно, или оба откатываются.
  3. Отдельный Outbox Sender читает pending rows и публикует в брокер. При успехе — помечает status='sent'.
  4. Если sender падает после publish, но до mark-as-sent, строка остаётся pending. Следующий прогон sender-а re-publish (дубликат), но idempotent producer брокера или consumer dedup обрабатывает его.

CDC-based вариант (Debezium читает DB transaction log) — современная форма: без polling loop, меньше латентность, работает пока БД онлайн.

Викторина

SQS consumer вызывает ChangeMessageVisibility до 90с во время processing. Consumer потом падает. Когда SQS передоставляет сообщение?

Викторина

Приложение апдейтит Postgres строку и публикует Kafka сообщение двумя отдельными операциями. DB commit успешен, Kafka publish падает. Какое состояние данных?

Расставь шаги по порядку

Поставь шаги outbox pattern для надёжной публикации событий:

  1. 1 Приложение получает запрос обновить статус заказа
  2. 2 BEGIN DB-транзакцию
  3. 3 UPDATE orders SET status='paid' WHERE id=123
  4. 4 INSERT INTO outbox (payload='{order_paid, id:123}', status='pending')
  5. 5 COMMIT — и апдейт, и outbox row committed атомарно
  6. 6 Outbox Sender читает pending row и публикует в Kafka
  7. 7 Outbox Sender помечает row status='sent'
Вспомните перед уходом
  1. 01
    Что происходит со значением visibility timeout при SQS redelivery?
  2. 02
    Какой рекомендуемый maxReceiveCount для production SQS DLQ и почему не 1?
  3. 03
    Что такое dual-write проблема и как outbox pattern её решает?
Итог

SQS visibility timeout — per-message таймер, сбрасывающийся на queue default при каждой redelivery — установи его в 6x среднее processing time и используй ChangeMessageVisibility heartbeat-ы (каждые timeout/3 секунды) для переменных workload. Dead-letter queues помещают poison pills в карантин после maxReceiveCount сбоев (используй 5–10, не 1); redrive с rate limit 1–10 msgs/s во избежание stampede source queue. Outbox pattern решает producer-side dual-write проблему через INSERT event row в той же DB-транзакции, что и бизнес-апдейт — строка — это durable intent; отдельный sender публикует из неё, и любой publish failure ретраится из всё ещё pending строки без потери события.

Связанные уроки
встречается в178
Продолжить восхождение ↑Exactly-once в production: impossibility-доказательство, гибридные паттерны и реальные инциденты
хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources2
expand
  1. 01
  2. 02

Trademarks belong to their respective owners. Editorial reference only.