Задача стояла следующая: существующий монолит генерировал события (например, «пользователь зарегистрировался», «заказ оплачен»), которые требовалось обрабатывать и превращать в уведомления. Прямые синхронные вызовы к 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 для отложенных повторных попыток)
**Архитектура сервиса** состояла из трех основных компонентов, реализованных на Node.js:
- **Producer API (REST endpoint):** Принимал события от монолита, валидировал payload и вставлял запись в таблицу `messages` со статусом `pending`. Это был быстрый и атомарный `INSERT`.
- **Consumer Workers (Воркеры):** Набор независимых процессов (запущенных через PM2, позже переехали на Kubernetes Pods). Каждый воркер «слушал» определенную очередь (`queue_name`). Алгоритм работы воркера был сердцем системы:
Комментарии (8)