Создание очереди сообщений с нуля: Архитектурный кейс для масштабируемого микросервиса уведомлений

Детальный разбор кейса проектирования и реализации самописной системы очередей сообщений на базе PostgreSQL (с SKIP LOCKED) и Node.js для обеспечения надежной асинхронной отправки уведомлений в микросервисной архитектуре.
В эпоху микросервисной архитектуры асинхронная коммуникация между сервисами через очереди сообщений (Message Queue) стала стандартом де-факто. Но что делать, если готовые решения вроде RabbitMQ или Kafka кажутся избыточными для конкретной, но критичной задачи, а облачные сервисы не подходят по соображениям стоимости или compliance? В этом кейсе мы разберем, как с нуля спроектировали и реализовали легковесную, но надежную систему очередей для сервиса массовых уведомлений (email, push, SMS) в условиях высоких, но пикообразных нагрузок.

Задача стояла следующая: существующий монолит генерировал события (например, «пользователь зарегистрировался», «заказ оплачен»), которые требовалось обрабатывать и превращать в уведомления. Прямые синхронные вызовы к SMTP-шлюзам и push-сервисам приводили к таймаутам основного API при их сбоях и не позволяли масштабировать отправку. Нужна была простая, самописная шина событий, которая гарантировала бы доставку, обеспечивала отложенную обработку и позволяла горизонтально масштабировать воркеры.

Первым и ключевым решением был **выбор хранилища**. Нам нужна была скорость, надежность и возможность легко сегментировать данные. Дискуссия велась между PostgreSQL с его SKIP LOCKED и Redis как in-memory хранилищем. Мы выбрали **PostgreSQL**. Почему? Надежность и ACID-гарантии были приоритетом (потерять уведомление о платеже — недопустимо). Механизм `SKIP LOCKED` в сочетании с `FOR UPDATE` позволял организовать конкурирующих воркеров без блокировок всей таблицы. Плюс, мы уже использовали Postgres, что упрощало эксплуатацию.

Схема базы данных была минималистичной. Основная таблица `messages`:
  • `id` (UUID, первичный ключ)
  • `queue_name` (VARCHAR, для сегментации: 'email', 'push_ios', 'sms')
  • `status` (ENUM: 'pending', 'processing', 'done', 'failed')
  • `payload` (JSONB с телом сообщения: адрес, шаблон, данные)
  • `created_at`, `updated_at` (timestamps)
  • `retry_count` (INT, счетчик попыток)
  • `next_retry_at` (TIMESTAMP для отложенных повторных попыток)
Отдельная таблица `dead_letters` хранила сообщения, которые не удалось обработать после N попыток, для последующего ручного разбора.

**Архитектура сервиса** состояла из трех основных компонентов, реализованных на Node.js:
  • **Producer API (REST endpoint):** Принимал события от монолита, валидировал payload и вставлял запись в таблицу `messages` со статусом `pending`. Это был быстрый и атомарный `INSERT`.
  • **Consumer Workers (Воркеры):** Набор независимых процессов (запущенных через PM2, позже переехали на Kubernetes Pods). Каждый воркер «слушал» определенную очередь (`queue_name`). Алгоритм работы воркера был сердцем системы:
* Запрос к БД: `SELECT * FROM messages WHERE queue_name = 'email' AND status = 'pending' AND (next_retry_at IS NULL OR next_retry_at латентность) и грамотном использовании возможностей современных БД можно построить эффективное и простое решение, идеально заточенное под конкретные бизнес-процессы.
231 2

Комментарии (8)

avatar
ehv0en 01.04.2026
А как насчет горизонтального масштабирования потребителей (consumers) в вашей реализации? Это раскрыто в полной статье?
avatar
592jjhbxq 01.04.2026
Отличный пример, когда понимание внутреннего устройства системы важнее использования готового black box.
avatar
dazkgij 01.04.2026
Стоило бы добавить сравнение производительности с тем же Redis Pub/Sub для полноты картины.
avatar
49ibbwcpr 03.04.2026
Для уведомлений, где возможна некоторая задержка, такой подход вполне оправдан. Главное — правильно оценить требования.
avatar
qpw5ub0g63c 03.04.2026
Практичный кейс для стартапов с ограниченным бюджетом. Иногда простое самописное решение — единственный выход.
avatar
ml0ut2 04.04.2026
Статья полезна, но хотелось бы больше деталей по гарантии доставки и обработке дублей сообщений.
avatar
daok0lsoc38c 04.04.2026
Ключевой вопрос — мониторинг и алертинг такой кастомной очереди. Как вы решали проблему visibility?
avatar
jpkvqyv 04.04.2026
Интересный подход, но не слишком ли мы изобретаем велосипед? RabbitMQ с его dead letter exchange решает многие проблемы надежности.
Вы просмотрели все комментарии