Транспорт бэкенда
RabbitMQ: AMQP, exchanges, очереди
В 2013 году Instagram обрабатывал 1000 фотографий в секунду в пике. Их архитектура: Celery + RabbitMQ - каждая загруженная фото уходила в очередь, где обрабатывалась отдельно: ресайзинг, фильтры, уведомления подписчиков. RabbitMQ за этим стоял потому что AMQP давал гибкость маршрутизации, которую Kafka тогда не умела.
- **Zalando** использует RabbitMQ для распределения заказов между складами в 17 европейских странах: topic exchange с routing key по стране и типу товара направляет каждый заказ на ближайший склад
- **GitLab CI/CD** исторически строился на Sidekiq + RabbitMQ: каждый пайплайн-job уходил в очередь, runner'ы конкурировали за задачи через competing consumers
- **Trivago** маршрутизирует поисковые запросы к 400+ поставщикам отелей через fanout exchange: один запрос пользователя размножается в N параллельных запросов к разным системам
AMQP-модель
RabbitMQ построен на протоколе AMQP 0-9-1 - и его модель принципиально отличается от того, что большинство разработчиков интуитивно ожидает. Producer никогда не пишет напрямую в очередь. Сообщение попадает в exchange (обменник), который по routing rules решает, в какие очереди его поместить. Очереди хранят сообщения. Consumer читает из очередей. Это разделение - exchange как router, queue как buffer - даёт RabbitMQ невероятную гибкость маршрутизации.
Ключевые сущности AMQP: **Exchange** - маршрутизатор сообщений; получает от producer, раскидывает по очередям по правилам. **Queue** - хранилище сообщений; consumer подписывается на очередь. **Binding** - связь exchange-queue с опциональным routing key или аргументами. **Routing key** - строка-метка, которую producer ставит на сообщение; exchange использует её для выбора очереди. **Virtual Host (vhost)** - изоляция в рамках одного сервера, как schema в PostgreSQL.
Producer публикует сообщение в exchange с routing key 'payment.success'. К этому exchange привязаны две очереди: одна с binding key 'payment.success', другая с 'payment.*'. Exchange типа direct. Куда попадёт сообщение?
Типы exchanges
Четыре типа exchanges в RabbitMQ - это четыре разные стратегии маршрутизации. Direct - точное совпадение ключа (один получатель или competing consumers). Fanout - рассылка всем привязанным очередям без ключей (broadcast событий). Topic - wildcard по routing key через '.' и '*'/'#' (гибкая фильтрация). Headers - matching по заголовкам сообщения вместо ключа (редко, дорого). Выбор типа exchange определяет топологию системы.
Wildcard-правила для **topic exchange**: звёздочка `*` заменяет ровно одно слово (segment), `#` заменяет ноль или более слов. Например, routing key `order.europe.paid`: подходит к binding `order.*.paid` (одно слово вместо `europe`) и к `order.#` (любые суффиксы). Не подходит к `order.paid` (нет среднего сегмента). Topic exchange - самый популярный в production, потому что гибко выражает смысловую иерархию событий.
Нужно раздать одно событие 'user.registered' трём сервисам: email, CRM, analytics. Все три всегда должны получать копию. Какой тип exchange?
Ack, Nack и Prefetch
RabbitMQ не удаляет сообщение из очереди в момент отправки consumer'у - оно остаётся в состоянии 'unacknowledged' до получения явного подтверждения. Это критически важно: если consumer упал с сообщением в обработке, брокер переотправит его другому worker'у. Три исхода обработки: **ack** - успех, удалить; **nack** - провал, переотправить (requeue=true) или отправить в DLQ (requeue=false); **reject** - синоним nack для одного сообщения.
**Prefetch (basic.qos)** - ключевой параметр backpressure: ограничивает количество unacknowledged сообщений на channel. `prefetch=1` - самый безопасный (fair dispatch между worker'ами), но минимальный throughput. `prefetch=50-100` - баланс для большинства случаев. `prefetch=0` - без ограничений (максимальный throughput, но возможен OOM при медленной обработке). Правило: `prefetch * avg_processing_time < desired_latency`.
Consumer получил сообщение, при обработке возникло сетевое соединение (временная ошибка). Какое действие правильное?
Dead Letter Queues
Dead Letter Queue (DLQ) - это карантин для сообщений, которые не удалось обработать после всех попыток. Без DLQ провалившееся сообщение либо зависает в очереди вечно (если requeue=true без ограничений), либо теряется (requeue=false). DLQ позволяет: анализировать причины сбоев, перезапускать обработку вручную после исправления бага, не блокировать основную очередь. Netflix использует DLQ для каждой очереди событий - инженеры ежедневно просматривают DLQ как индикатор здоровья системы.
Сообщение попадает в Dead Letter Exchange (DLX) при трёх условиях: **nack/reject** с requeue=false, **TTL истёк** (x-message-ttl), **очередь переполнена** (x-max-length). Настройка DLX на очереди: `x-dead-letter-exchange` и опционально `x-dead-letter-routing-key`. Паттерн retry с задержкой: основная очередь -> nack -> DLX -> retry queue с TTL -> снова в основной exchange. Так реализуется exponential backoff без дополнительного кода.
В какой из следующих ситуаций сообщение НЕ попадёт в Dead Letter Exchange?
Кластеризация и HA
Одна нода RabbitMQ - единая точка отказа. Для production нужен кластер: несколько нод разделяют метаданные (exchanges, bindings, users), но по умолчанию очереди реплицируются только на ноду-хозяина. Потеря ноды с очередью = потеря всех непрочитанных сообщений. Quorum Queues (RabbitMQ 3.8+) решают это через Raft-репликацию: большинство нод должны подтвердить запись сообщения, очередь выживает при отказе меньшинства нод.
Три режима очередей: **Classic Queue** - быстро, один хозяин, нет HA без дополнительных настроек. **Mirrored Queue (deprecated)** - репликация на все ноды, высокая нагрузка на сеть, устарело. **Quorum Queue** - Raft, N/2+1 нод для записи, рекомендуется для production. Для кластера минимум 3 ноды (чтобы Raft имел quorum при отказе одной). Правило: `replicas = 2 * tolerance + 1`, где tolerance - допустимое количество одновременных отказов.
RabbitMQ кластер автоматически обеспечивает высокую доступность очередей
Без явного указания x-queue-type: quorum очереди НЕ реплицируются - при отказе ноды-хозяина очередь теряется вместе с сообщениями
По умолчанию Classic Queue существует только на одной ноде. Кластер делит метаданные, но не данные очередей. Только явное создание Quorum Queue обеспечивает репликацию через Raft.
RabbitMQ кластер из 5 нод с Quorum Queues. Сколько нод могут одновременно упасть без потери данных?
Ключевые идеи
- **AMQP-модель**: producer -> exchange -> binding -> queue -> consumer; exchange как router определяет куда идёт сообщение - прямой записи в очередь нет
- **Четыре типа exchanges** покрывают все сценарии: direct (точное совпадение), fanout (всем), topic (wildcard-routing), headers (по заголовкам); topic - самый гибкий для production
- **Ack + DLQ + Quorum** - триада надёжности: ручное подтверждение защищает от потери при сбое consumer; DLQ сохраняет провалившиеся сообщения; Quorum Queues защищают от потери ноды кластера
Связанные темы
RabbitMQ как конкретная реализация концепций брокеров:
- Брокеры сообщений: зачем и когда — RabbitMQ реализует все три вида decoupling и поддерживает at-least-once и exactly-once семантику
- gRPC и Protocol Buffers — Часто RabbitMQ-сообщения сериализуют в Protobuf вместо JSON для эффективности
Вопросы для размышления
- Fanout exchange удобен для broadcast, но все подписчики получают одинаковую копию. Как реализовать сценарий, где разные микросервисы получают один event, но с разным routing: email-сервис получает все события, а analytics только события от premium-пользователей?
- Quorum Queues дают надёжность, но требуют подтверждения от большинства нод перед ack - это увеличивает latency. При какой бизнес-логике эта latency неприемлема, и что использовать вместо?
- DLQ накапливает сообщения с ошибками - как построить pipeline для их анализа и re-processing после исправления бага, не теряя original context (метаданные, время поступления, количество попыток)?