Связываем кластеры RabbitMQ: Federation и Shovel

Асинхронная маршрутизация между кластерами RabbitMQ: чем federation отличается от shovel, как строить geo-распределение и DR-сценарии и когда что выбирать

В первой статье серии мы построили отказоустойчивый кластер с quorum-очередями: он переживает падение ноды, не теряя подтверждённых сообщений. Во второй разобрали гарантии доставки: publisher confirms и manual ack — на уровне клиентов, DLQ — на уровне брокера. Но оба механизма работают внутри одного кластера.

Что происходит, когда нужно связать два независимых кластера — в разных регионах, разных ДЦ или просто логически разделённых? Отдельный кластер в регионе B не знает ничего о потоке сообщений в регионе A. Именно здесь появляются Federation и Shovel.

В статье

Зачем связывать кластеры

Три основных сценария, при которых возникает потребность в межкластерном взаимодействии.

Geo-распределение. Сервисы работают в нескольких регионах. Каждый регион имеет свой кластер RabbitMQ — это правильно с точки зрения латентности и автономности. Но события, производимые в одном регионе, нужно консюмить в другом: заказы из EU-кластера обрабатываются сервисами в US-регионе. Растянуть один кластер на два региона — плохая идея: кворум через WAN даёт неприемлемую задержку на каждый publish.

Disaster Recovery. Нужен резервный кластер в другом ДЦ, который получает копию критичных сообщений. Если основной кластер полностью выходит из строя — DR-кластер подхватывает нагрузку.

Разгрузка и изоляция доменов. Отдельный кластер для конкретного сервиса или направления — и нужен мост между ними. Иногда это требование безопасности (изоляция PCI DSS-трафика), иногда — команды не хотят делить один брокер.

Оба инструмента — Federation и Shovel — реализованы как плагины RabbitMQ и работают поверх обычных AMQP-соединений. Это ключевое отличие от внутрикластерной репликации (Raft у quorum-очередей): Federation и Shovel работают между независимыми брокерами, не требуя Erlang-кластеризации.

Federation: подписка downstream на upstream

Концепция

Federation — это механизм репликации потока сообщений между брокерами на уровне exchange или queue. Основная идея: downstream-кластер подписывается на upstream и получает сообщения, которые публикуются там.

Несколько ключевых свойств, которые важно понять сразу:

  • Federation-link инициируется downstream’ом. Downstream-нода устанавливает AMQP-соединение к upstream и «вычитывает» сообщения. Upstream при этом ничего специально не настраивает — он просто принимает соединение.
  • Копирование, а не перемещение (exchange federation). Federation создаёт внутреннего consumer’а на upstream и копирует сообщения на downstream, когда у downstream-exchange есть связанные очереди с подписчиками. Сообщение на upstream при этом не удаляется — оно остаётся доступным для локальных потребителей upstream.
  • Асинхронность и устойчивость. Federation работает асинхронно. Если линк временно недоступен (сеть, рестарт upstream), downstream продолжает обслуживать локальных потребителей. При восстановлении соединения federation-link поднимается автоматически.

Exchange federation vs queue federation

RabbitMQ поддерживает два варианта.

Exchange federation — наиболее распространённый. Federated exchange на downstream «видит» сообщения, опубликованные в exchange того же имени на upstream. Federation создаёт внутреннего consumer’а на upstream и копирует только те сообщения, которые реально нужны downstream-потребителям. На upstream сообщение не удаляется — это именно копирование.

Queue federation — downstream-потребители получают сообщения непосредственно из upstream-очереди. В отличие от exchange federation, сообщения при этом уходят из upstream-очереди (как при обычном consume) — дублирования нет. Смысл не в work-queue между кластерами, а в том, чтобы перенести потребление ближе к downstream-потребителям: они читают через локальный брокер, который вычитывает с upstream. Используется реже exchange federation.

Настройка: upstream и политика

Federation настраивается двумя объектами.

Federation upstream — описывает, куда подключаться downstream-кластеру:

rabbitmqctl set_parameter federation-upstream my-upstream \
  '{"uri":"amqp://user:pass@upstream-host:5672","expires":3600000}'

Политика — определяет, на какие exchange или очереди распространяется federation:

rabbitmqctl set_policy \
  --vhost / \
  --apply-to exchanges \
  federate-orders \
  "^orders\." \
  '{"federation-upstream-set":"all"}'

Этой политикой все exchange с именем, начинающимся на orders., становятся federated. Downstream-брокер начинает тянуть сообщения с upstream автоматически.

Демо: federation-demo.sh

В demo-стенде есть скрипт scripts/federation-demo.sh, который показывает настоящую cross-cluster federation. Стенд проверен на RabbitMQ 4.3.

Топология стенда:

┌─────────────────────────────────────┐       AMQP Federation      ┌──────────────────────────┐
│  Кластер A (UPSTREAM)               │   amqp://rabbit1:5672/%2F  │  Кластер B (DOWNSTREAM)  │
│  rabbit1 / rabbit2 / rabbit3        │ ─────────────────────────► │  rabbit-b (одна нода)    │
│  AMQP: 5672/5673/5674               │                            │  AMQP: 5681              │
│  Management: 15672                  │                            │  Management: 15681       │
└─────────────────────────────────────┘                            └──────────────────────────┘

rabbit-bотдельный кластер с собственным Erlang cookie. Он не входит в кластер rabbit1/2/3, но подключён к той же docker-сети rabbitnet и поэтому может достучаться до rabbit1 по AMQP.

Что делает скрипт:

  1. Объявляет exchange demo.fed на upstream (rabbit1).
  2. На downstream (rabbit-b) создаёт тот же exchange demo.fed, quorum-очередь demo.fed.q и связывает их.
  3. На rabbit-b задаёт federation-upstream upstream-rabbit1 с URI amqp://demo:demo@rabbit1:5672/%2F. Нюанс: vhost / кодируется как %2F — в RabbitMQ 4.x trailing slash в URI трактуется как пустой vhost и даёт ошибку not_allowed.
  4. Применяет политику federate-fed-b на exchange demo.fed на rabbit-b — federation-link устанавливается за ~5 сек.
  5. Публикует сообщение в upstream (rabbit1) → оно реально появляется в demo.fed.q на downstream (rabbit-b). Это настоящая cross-cluster репликация: сообщение пересекает границу кластеров по AMQP-линку.
# Запустить демо (из папки ha-cluster):
bash scripts/federation-demo.sh

# Проверить статус federation-links на downstream (rabbit-b):
docker exec rabbit-b rabbitmqctl list_parameters
docker exec rabbit-b rabbitmqctl eval 'rabbit_federation_status:status().'

# Очереди на downstream после публикации:
docker exec rabbit-b rabbitmqctl list_queues name messages

# Удалить артефакты:
bash scripts/federation-demo.sh clean

Management UI обоих кластеров:

  • Upstream rabbit1: http://localhost:15672 (demo/demo)
  • Downstream rabbit-b: http://localhost:15681 (demo/demo)

Статус federation-links виден на downstream: Admin → Federation Status. Там отображаются активные линки: от какого upstream, на каких exchange/queue, состояние соединения (running).

Federation Links в Management UI RabbitMQ: статус активных federation-линков

flowchart LR subgraph upstream["Upstream — кластер A (rabbit1/2/3, порты 5672/15672)"] direction TB UE["orders.exchange\n(federated, durable)"] UQ["orders.queue\n(quorum)"] UP["Producer A"] UC["Consumer A"] UP -->|"publish"| UE UE --> UQ UQ -->|"consume"| UC end subgraph downstream["Downstream — кластер B (rabbit-b, порты 5681/15681)"] direction TB DE["orders.exchange\n(federated, durable)"] DQ["orders.queue\n(quorum)"] DC["Consumer B"] DE --> DQ DQ -->|"consume"| DC end downstream -->|"federation-link\n(downstream подключается к upstream, pull)"| upstream UE -.->|"сообщения\n(копия, только нужные Consumer B)"| DE style upstream fill:#f9f3e3,stroke:#8b7355 style downstream fill:#c9e4c5,stroke:#5b8a5e style UE fill:#e8d5c4,stroke:#a0785a style DE fill:#e8d5c4,stroke:#a0785a style UQ fill:#c9e4c5,stroke:#5b8a5e style DQ fill:#c9e4c5,stroke:#5b8a5e

flowchart LR
    subgraph upstream["Upstream — кластер A (rabbit1/2/3, порты 5672/15672)"]
        direction TB
        UE["orders.exchange\n(federated, durable)"]
        UQ["orders.queue\n(quorum)"]
        UP["Producer A"]
        UC["Consumer A"]
        UP -->|"publish"| UE
        UE --> UQ
        UQ -->|"consume"| UC
    end

    subgraph downstream["Downstream — кластер B (rabbit-b, порты 5681/15681)"]
        direction TB
        DE["orders.exchange\n(federated, durable)"]
        DQ["orders.queue\n(quorum)"]
        DC["Consumer B"]
        DE --> DQ
        DQ -->|"consume"| DC
    end

    downstream -->|"federation-link\n(downstream подключается к upstream, pull)"| upstream
    UE -.->|"сообщения\n(копия, только нужные Consumer B)"| DE

    style upstream fill:#f9f3e3,stroke:#8b7355
    style downstream fill:#c9e4c5,stroke:#5b8a5e
    style UE fill:#e8d5c4,stroke:#a0785a
    style DE fill:#e8d5c4,stroke:#a0785a
    style UQ fill:#c9e4c5,stroke:#5b8a5e
    style DQ fill:#c9e4c5,stroke:#5b8a5e
Exchange federation: downstream-кластер устанавливает AMQP-соединение к upstream и копирует сообщения для локальных потребителей

Два реальных кластера: как это выглядит на практике

Demo-стенд (federation-demo.sh) уже работает с двумя настоящими независимыми кластерами: rabbit1/2/3 (upstream, кластер A) и rabbit-b (downstream, кластер B). Это ровно та же модель, что применяется в production между кластерами в разных регионах или сетях — разница только в том, что в реальной инфраструктуре вместо docker-сети будет WAN и TLS.

На upstream-кластере ничего специального не нужно. Он просто принимает входящие AMQP-соединения — federation работает через стандартный протокол. Убедитесь, что порт 5672 (или 5671 для TLS) доступен с downstream, и есть пользователь с нужными правами.

На downstream-кластере три шага:

  1. Объявить federation-upstream — AMQP-URI удалённого кластера:

    # Синтаксис set_parameter стабилен, но конкретные параметры URI
    # могут меняться в зависимости от версии плагина — сверяйтесь с документацией.
    rabbitmqctl set_parameter federation-upstream region-a \
      '{"uri":"amqps://federation-user:secret@rabbit.region-a.example.com:5671",
        "expires":3600000}'
    
  2. Применить политику федерации на нужные exchange (или queue):

    rabbitmqctl set_policy \
      --vhost / \
      --apply-to exchanges \
      federate-orders \
      "^orders\." \
      '{"federation-upstream":"region-a"}'
    
  3. Проверить статус линка:

    rabbitmqctl eval 'rabbit_federation_status:status().'
    # или в Management UI: Admin → Federation Status
    

Главное отличие от demo: URI указывает на реальный хост другого кластера в другой сети. Всё остальное — та же модель: downstream устанавливает соединение, создаёт внутреннего consumer’а на upstream, копирует сообщения по мере необходимости.

Безопасность межкластерных соединений

Federation и Shovel ходят по AMQP между кластерами — нередко через интернет или между регионами. Несколько практических правил:

TLS обязателен. Используйте amqps:// вместо amqp:// для межкластерных URI. Передача сообщений в открытом виде между регионами — неприемлемый риск. RabbitMQ поддерживает TLS из коробки; настройка описана в официальной документации.

Отдельный пользователь для federation. Не используйте admin-аккаунт в URI federation-upstream или shovel. Заведите отдельного пользователя с минимально необходимыми правами: read на нужные exchange/queue на upstream, без доступа к management API. Если credentials утекут — радиус поражения ограничен.

Shovel: точечная перекачка сообщений

Shovel — другой плагин, другая модель. Если Federation — это «подписка», то Shovel — это «насос»: он вычитывает сообщения из источника (source) и публикует их в назначение (destination). Работает направленно: из точки A в точку B, без обратного пути.

Static vs dynamic shovel

Static shovel описывается в файле rabbitmq.conf или через definitions.json. Поднимается при старте брокера и не меняется без рестарта или обновления конфига. Подходит для постоянных, заранее известных маршрутов.

# rabbitmq.conf (фрагмент)
shovel.my-shovel.source.protocol = amqp091
shovel.my-shovel.source.uris.1   = amqp://upstream-host:5672
shovel.my-shovel.source.queue    = source.queue

shovel.my-shovel.destination.protocol = amqp091
shovel.my-shovel.destination.uris.1   = amqp://destination-host:5672
shovel.my-shovel.destination.queue    = destination.queue

Dynamic shovel настраивается через runtime-параметры (rabbitmqctl set_parameter), то есть без рестарта брокера. Удобен для временных задач: запустить, выполнить перекачку, отключить.

rabbitmqctl set_parameter shovel migrate-orders \
  '{
    "src-protocol": "amqp091",
    "src-uri": "amqp://source-host:5672",
    "src-queue": "legacy.orders",
    "dest-protocol": "amqp091",
    "dest-uri": "amqp://target-host:5672",
    "dest-queue": "orders"
  }'

Что делает Shovel с сообщениями

Shovel вычитывает сообщение из источника, публикует в назначение и только после успешного подтверждения публикации отправляет ack источнику. Это обеспечивает at-least-once семантику: если сбой произошёл между публикацией и ack’ом источника, сообщение будет доставлено повторно — но не потеряется.

Shovel работает между любыми двумя AMQP-брокерами — не только RabbitMQ. Это иногда используется как мост с другими системами.

Federation vs Shovel: когда что выбирать

Federation Shovel
Модель Downstream «подтягивает» нужные сообщения с upstream Насос: вычитывает из A, публикует в B
Инициатор Downstream устанавливает соединение к upstream Shovel-агент на брокере, где он запущен
Гранулярность Exchange или queue Конкретная очередь или exchange → очередь/exchange
Направление Upstream → downstream (pull) Источник → назначение (push)
Копирование vs перемещение Копирует (сообщение остаётся на upstream) Перемещает (удаляет из источника после успешной публикации)
Настройка Upstream + policy Static (конфиг) или dynamic (runtime-параметр)
Топология Многие-ко-многим через upstreams + policies Точка-точка
Типичный сценарий Geo-fan-out, DR, события между регионами Миграция, мост, разовая перекачка
Прозрачность Потребители downstream не знают об upstream Явный маршрут; потребители работают с назначением

Выбирайте Federation, когда:

  • нужно, чтобы сообщения с upstream-кластера были доступны потребителям в другом регионе автоматически;
  • хотите «логически объединить» несколько кластеров в единое пространство имён;
  • важно, чтобы сообщения оставались на upstream для локальных потребителей и копировались на downstream только при наличии там интереса;
  • топология «hub-and-spoke» или «mesh» между несколькими регионами.

Выбирайте Shovel, когда:

  • нужно переместить (не скопировать) сообщения из одного брокера в другой;
  • выполняете миграцию: переносите данные из старого кластера в новый;
  • нужен мост между двумя конкретными очередями с явным маршрутом;
  • задача разовая или условно временная — dynamic shovel проще включить и выключить без рестарта.

Geo-распределение и DR

Типовые топологии

Hub-and-spoke. Центральный кластер (hub) принимает сообщения, региональные кластеры (spoke) подписываются на него через federation. Производители пишут в hub, потребители в каждом регионе читают локально. Подходит, когда у вас один источник событий и много потребителей в разных точках.

Active-active. Каждый кластер самостоятелен: производители и потребители есть в каждом регионе. Federation работает в обе стороны: кластер A подписывается на B, B подписывается на A. Сложнее в конфигурации, но даёт полную автономию регионов. Важно: federation предотвращает циклы через механизм hop counting — каждое сообщение несёт счётчик прыжков, и federation не пересылает его при достижении max-hops (по умолчанию 1). Так сообщение, пришедшее из A в B, не вернётся обратно в A.

DR (резервный кластер). Один кластер — основной, второй — горячий резерв. Shovel или federation копирует поток в DR-кластер. При отказе основного переключение — ручное или автоматическое (зависит от вашей инфраструктуры; RabbitMQ сам не делает failover между кластерами).

Латентность и направление потока

Geo-распределение через federation или shovel всегда асинхронно. Это принципиальное ограничение: нельзя получить низкую латентность и сильную согласованность одновременно через WAN.

На практике это означает:

  • Производители пишут локально — в свой региональный кластер. Это даёт минимальную латентность на publish и отсутствие зависимости от WAN при публикации.
  • Federation-линк несёт задержку — сообщение появится на downstream с задержкой, определяемой скоростью WAN-соединения и объёмом трафика.
  • Порядок сообщений между регионами не гарантируется: разные federation-линки работают параллельно, порядок доставки может отличаться от порядка публикации.

Именно поэтому попытка растянуть один RabbitMQ-кластер на несколько географически разнесённых ДЦ — плохая идея. Quorum-очереди используют Raft, каждый publish ждёт подтверждения от большинства реплик. Через WAN это означает сотни миллисекунд на каждое сообщение. Federation решает задачу иначе: каждый регион работает независимо, WAN-соединение нужно только для репликации потока, но не для каждого отдельного подтверждения.

Вывод

Federation и Shovel закрывают разные задачи, и эта разница принципиальная.

Federation — это механизм прозрачной подписки downstream на upstream через AMQP-линк. Downstream тянет то, что ему нужно; сообщения могут оставаться на upstream для локальных потребителей. Это инструмент для постоянных топологий: geo-fan-out, DR-репликация, объединение кластеров в разных регионах.

Shovel — направленный насос: вычитывает из источника, публикует в назначение, удаляет из источника. Явный маршрут, явная семантика. Лучше подходит для миграций, точечных мостов между конкретными очередями и разовых задач перекачки.

Оба инструмента работают асинхронно поверх обычных AMQP-соединений и не требуют Erlang-кластеризации между брокерами. Это позволяет связывать кластеры в разных ДЦ, регионах и даже у разных провайдеров.

В следующей статье серии разберём мониторинг и продакшн-чеклист: какие метрики критичны, как настроить алерты и что проверить перед тем, как пустить кластер в бой — «RabbitMQ: мониторинг и продакшн-чеклист».

Документация и первоисточники

Обсуждение в Telegram

Присоединиться →

Комментарии