Прямой ответ
Очередь уведомлений отделяет создание уведомления от отправки и управляет повторами при сбоях канала. Три элемента определяют поведение: отсрочка повтора, классификация отказа и место для неразобранных сообщений.
Отсрочка растёт с каждой попыткой:
def next_delay(attempts: int, base: int = 60, cap: int = 3600) -> int:
return min(cap, base * (2 ** (attempts - 1)))
Первая попытка через минуту, вторая через две, третья через четыре, далее до часа. Рост ограничен сверху, поскольку бесконечное увеличение интервала откладывает доставку на срок, когда уведомление теряет смысл.
Классификация отказа определяет, выполняется ли повтор:
| Тип отказа | Пример | Действие |
|---|---|---|
| Временный | Таймаут, код 5xx, ограничение частоты | Повтор с отсрочкой |
| Постоянный | Недействительный адрес, код 4xx | Прекращение попыток |
| Неопределённый | Разрыв соединения без ответа | Повтор с проверкой |
Захват записи для отправки выполняется условно, поэтому параллельные обработчики не отправляют одно уведомление дважды:
UPDATE notifications
SET status = 'sending', attempts = attempts + 1,
updated_at = now()
WHERE id = (
SELECT id FROM notifications
WHERE status = 'queued' AND scheduled_at <= now()
ORDER BY scheduled_at
LIMIT 1 FOR UPDATE SKIP LOCKED
)
RETURNING id, channel, payload;
Пропуск заблокированных строк позволяет запускать несколько обработчиков без дублей. О гарантиях доставки в очередях — в статье про надёжность доставки.
Очередь недоставленных сообщений и разбор отказов
Повтор с отсрочкой покрывает временные сбои, но часть сообщений не удаётся отправить независимо от числа попыток. Бесконечные повторы создают нагрузку и скрывают причину, поэтому после исчерпания попыток сообщение переходит в очередь недоставленных сообщений.
Три требования к обработке неразобранных сообщений.
Сохранение исходного содержания. Сообщение сохраняется полностью, включая состав данных и причину отказа, поскольку повторная обработка требует исходного контекста.
Отдельный разбор. Сообщения в очереди недоставленных разбираются отдельно от обычного потока, поскольку причина отказа требует анализа, а не автоматического повтора.
Ограничение срока хранения. Сообщение, не разобранное в течение заданного срока, теряет актуальность, поэтому фиксируется как неразрешимое.
О механизме очереди недоставленных сообщений — в статье про dead letter queue.
Практическое следствие: число попыток ограничивается типом уведомления. Транзакционное уведомление требует больше попыток, поскольку доставка критична, а маркетинговое — меньше, поскольку задержка снижает ценность.
Отдельный аспект — ограничение частоты отправки в канал. Провайдер ограничивает число запросов в единицу времени, поэтому превышение приводит к отказам, которые трактуются как временные и провоцируют повторы. Ограничение на стороне отправителя предотвращает каскад повторов.
Метрики очереди показывают состояние системы; о мониторинге отставания — в статье про метрики очередей.
SELECT status, COUNT(*) AS cnt,
MAX(EXTRACT(EPOCH FROM (now() - created_at)) / 60)::int AS max_age_min
FROM notifications GROUP BY status;
Приоритет уведомлений в очереди
Единая очередь для всех уведомлений приводит к тому, что массовая рассылка задерживает срочные сообщения. Разделение по приоритету устраняет влияние объёма на своевременность.
Высокий приоритет. Транзакционные уведомления и сообщения о безопасности обрабатываются первыми, поскольку задержка снижает ценность и приводит к обращениям.
Обычный приоритет. Продуктовые уведомления, где задержка в минуты не влияет на результат.
Низкий приоритет. Массовые рассылки, выполняемые в период низкой нагрузки.
Разделение реализуется отдельными очередями с независимыми обработчиками либо одной очередью с приоритетом сообщения. Первый подход проще в эксплуатации, поскольку отказ одного обработчика не блокирует остальные.
Практическое следствие: массовая рассылка не задерживает подтверждение заказа, поскольку обрабатывается отдельным потоком. Доля мощности, выделяемая каждому приоритету, определяется долей трафика и требованием к своевременности.
Отдельный аспект — ограничение одновременной обработки. Число параллельных обработчиков ограничивается лимитом провайдера, поэтому увеличение числа обработчиков сверх лимита приводит к отказам по ограничению частоты, а не к росту скорости.
Практика: настройка очереди уведомлений
1. Захватывайте записи с пропуском заблокированных. Параллельные обработчики не отправляют одно уведомление дважды.
2. Растите отсрочку повтора с ограничением сверху. Немедленный повтор при временном сбое неэффективен, а неограниченный рост откладывает доставку.
3. Различайте временные и постоянные отказы. Повтор выполняется только при временной причине, постоянный прекращает попытки.
4. Ограничивайте число попыток по типу уведомления. Транзакционное сообщение требует больше попыток, чем маркетинговое.
5. Переносите неразобранные сообщения в отдельную очередь. Бесконечные повторы скрывают причину отказа и создают нагрузку.
6. Сохраняйте причину отказа вместе с сообщением. Разбор требует контекста, который теряется при повторной обработке.
7. Ограничивайте частоту отправки в канал. Превышение лимита провайдера приводит к отказам и каскаду повторов.
Рекомендации:
- Не отправляйте уведомление в запросе пользователя. Задержка канала влияет на время ответа, поэтому отправка выполняется из очереди.
- Планируйте отправку по времени. Отложенное уведомление сохраняется с временем отправки, а не отправляется немедленно; о разделении записи и отправки — в статье про архитектуру уведомлений.
- Учитывайте идемпотентность отправки. Повторное чтение записи не должно приводить к повторной отправке в канал.
- Мониторьте возраст записей в очереди. Рост возраста указывает на сбой отправки либо на недостаточную производительность обработчиков.
- Проверяйте поведение в тестах. Имитация временного и постоянного отказа показывает корректность классификации.
Практический сценарий: каскад повторов при ограничении частоты провайдера
Контекст: после роста числа уведомлений часть сообщений не доставлялась, а нагрузка на провайдера многократно превысила расчётную.
- Диагностика. Провайдер возвращал код ограничения частоты для части запросов. Обработчик трактовал отказ как временный и немедленно повторял отправку, что увеличивало число запросов и усиливало ограничение.
- Причина. Отсутствие отсрочки повтора и ограничения частоты отправки на стороне сервиса. Каждый отказ провоцировал немедленный повтор, поэтому фактическое число запросов превышало число уведомлений в несколько раз.
- Действия. (а) Ввели экспоненциальную отсрочку повтора с ограничением сверху; (б) добавили ограничение числа отправок в канал в единицу времени на стороне сервиса; (в) отнесли отказ по ограничению частоты к временным с увеличенным интервалом повтора; (г) добавили метрику числа повторов на уведомление и доли отказов по типам; (д) ограничили число попыток с переносом неразобранных сообщений в отдельную очередь.
- Результат. Нагрузка на провайдера соответствует расчётной, а уведомления доставляются с отсрочкой вместо накопления отказов.
Вывод: немедленный повтор при отказе по ограничению частоты создаёт каскад, усиливающий отказ. Отсрочка повтора и ограничение частоты отправки на стороне сервиса устраняют каскад, а метрика числа повторов выявляет проблему до обращения пользователей.
FAQ
Сколько попыток отправки делать? Зависит от типа уведомления. Транзакционное сообщение требует больше попыток с растущей отсрочкой, маркетинговое — две-три.
Что делать с сообщениями, которые не удалось отправить? Перенести в очередь недоставленных сообщений с сохранением причины отказа и разобрать отдельно.
Как избежать дублей при параллельной обработке? Захватом записи с пропуском заблокированных строк и условным обновлением состояния.
Нужно ли ограничивать частоту отправки, если провайдер ограничивает сам? Да. Ограничение на стороне сервиса предотвращает каскад повторов, который усиливает отказ провайдера.
Источники: RabbitMQ confirms, RabbitMQ dead letter exchanges, PostgreSQL explicit locking — ссылки в шапке.