> Как реализовать приоритеты в микросервисе рассылки уведомлений (Python)

Уровень: senior · Роль: backend · Язык: Python · Категория: Технические вопросы

Компании: Международный аэропорт Шереметьево

Стек: Python

> Пример ответа

Короткий ответ

Приоритизация в микросервисе рассылки реализуется через очередь сообщений с несколькими уровнями (например, RabbitMQ с несколькими exchange/queue или Kafka с партициями по приоритету) и пул воркеров, которые сначала обрабатывают сообщения с более высоким приоритетом. Дополнительно можно использовать weighted round-robin или отдельные consumer-группы для каждого уровня. Ключевой момент - не блокировать высокоприоритетные сообщения долгими low-priority задачами, поэтому часто применяют отдельные пулы воркеров или preemption.

Подробное объяснение

Основная задача - гарантировать, что уведомления с высоким приоритетом (например, SMS-коды подтверждения) обрабатываются раньше, чем массовые маркетинговые рассылки. Есть несколько подходов:

  1. Отдельные очереди по приоритету: создаётся N очередей (например, notifications.high, notifications.medium, notifications.low). Consumer подписывается на все очереди, но сначала полностью вычитывает high, потом medium, потом low. Минус - при постоянном потоке high-сообщений low может голодать.

  2. Одна очередь с приоритетом: RabbitMQ поддерживает x-max-priority на уровне очереди - сообщения сортируются внутри очереди по полю priority. Просто, но есть ограничение: приоритет работает только в пределах одного consumer, и нет гарантии строгого порядка при нескольких consumer.

  3. Kafka с партициями: каждая партиция - отдельный приоритет. Consumer-группа читает партиции в порядке приоритета, но нужно аккуратно управлять количеством партиций и consumer'ов.

  4. Weighted fair queueing: реализуется на уровне воркера - из каждой очереди берётся пропорциональное количество сообщений (например, 10 high, 5 medium, 1 low за цикл).

  5. Preemption: если high-сообщение появилось, воркер прерывает текущую low-задачу (если она не в транзакции) и берёт high. Сложно реализовать, обычно не нужно.

Для Python-стека типичный выбор - RabbitMQ с несколькими очередями или Celery с task_priority (но Celery использует одну очередь с сортировкой, что менее эффективно при больших объёмах).

На практике

На практике чаще всего используют комбинацию: отдельные очереди для критичных типов уведомлений (SMS, push) и одна общая очередь для email-рассылок. Воркеры настраиваются так: пул из 2-3 воркеров слушает только high-очередь, остальные - medium/low. Это гарантирует, что high-уведомления не ждут, даже если low-очередь забита.

Дополнительно стоит добавить:

  • метрики времени обработки по приоритетам;
  • алерты на задержку high-сообщений;
  • dead-letter queue для упавших сообщений с сохранением приоритета.

Пример кода

Пример на Python с RabbitMQ (pika) и несколькими очередями:

PYTHON
import pika
import threading
QUEUES = [
("notifications.high", 10), # (queue_name, weight)
("notifications.medium", 5),
("notifications.low", 1),
]
def process_message(ch, method, properties, body):
print(f"Processing {method.routing_key}: {body}")
ch.basic_ack(delivery_tag=method.delivery_tag)
def worker(connection_params):
connection = pika.BlockingConnection(connection_params)
channel = connection.channel()
for queue_name, _ in QUEUES:
channel.queue_declare(queue=queue_name, durable=True)
channel.basic_consume(queue=queue_name, on_message_callback=process_message)
# Простой weighted round-robin: читаем из high чаще
while True:
for queue_name, weight in QUEUES:
for _ in range(weight):
method, properties, body = channel.basic_get(queue=queue_name, auto_ack=False)
if method:
process_message(channel, method, properties, body)
else:
break
# Небольшая пауза, чтобы не крутить пустой цикл
connection.sleep(0.1)
# Запуск нескольких воркеров
for _ in range(3):
t = threading.Thread(target=worker, args=(pika.ConnectionParameters("localhost"),))
t.start()

Как отвечать на собеседовании

Начни с уточнения требований: какой объём, какие типы уведомлений, нужна ли строгая гарантия порядка. Затем опиши два-три подхода, сравни trade-off'ы. Обязательно упомяни проблему голодания low-приоритетных сообщений и способы её решения (weighted fair queueing, отдельные пулы). Покажи, что понимаешь ограничения инструментов: RabbitMQ priority - это сортировка внутри очереди, а не глобальный приоритет; Kafka - партиции, но consumer'ы читают их параллельно.

Что проверяет интервьюер

  • Понимание распределённых очередей и их ограничений.
  • Умение проектировать отказоустойчивые системы (dead-letter, метрики).
  • Способность видеть trade-off между простотой и гарантиями доставки.
  • Знание конкретных инструментов (RabbitMQ, Kafka, Celery) и их особенностей.

Типичные ошибки

  • Предложение использовать одну очередь с приоритетом без учёта того, что при нескольких consumer'ах порядок не гарантируется.
  • Игнорирование проблемы голодания low-приоритетных сообщений.
  • Отсутствие плана на случай переполнения high-очереди (нужен backpressure или масштабирование воркеров).
  • Забывают про idempotency и retry: при повторной отправке приоритет должен сохраняться.
  • Смешивание приоритета с типом уведомления - это разные вещи: тип определяет канал доставки, приоритет - срочность.

> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?

Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью