Возьмём одно сообщение: order.paid с заказом 48213. Потребитель принял его из очереди, начал выставлять счёт в бухгалтерской системе, но подтверждение брокеру ещё не отправил. В этот момент кто-то в соседнем окне нажимает кнопку «переключаем». Где теперь сообщение? У брокера оно ещё числится, у потребителя оно уже в руках, а у бухгалтерии, возможно, уже наполовину превратилось в счёт. Вся миграция RabbitMQ из облака на свой сервер, по сути, состоит из того, чтобы в каждую секунду переезда понимать, кто отвечает за такое сообщение.
Перенос настроек и перенос сообщений в RabbitMQ решаются разными инструментами, и путаница между ними порождает большую часть неприятных сюрпризов. Ниже разберём, как их разделить, что происходит с сообщением на каждом отрезке пути, как осушить очередь и почему повторная доставка не равна повторной бизнес-операции.
Две разные посылки: схема и содержимое
Когда говорят «перенести брокер», обычно имеют в виду два самостоятельных груза.
Первый груз — топология. Это пользователи, виртуальные хосты, очереди, обменники и привязки. В документации CloudAMQP такой набор называется definitions: в них входят users, virtual hosts, queues, exchanges и bindings. Определения лёгкие, их можно выгрузить, посмотреть глазами, положить в репозиторий и загрузить в новый брокер сколько угодно раз на репетиции.
Второй груз — сами сообщения, то есть то, что лежит в очередях прямо сейчас. И здесь важная деталь: экспорт definitions сообщений очередей не содержит. Вы получите идеальную пустую декорацию: все очереди на месте, все привязки на месте, а внутри ничего. Если на этом остановиться, всё, что висело в очередях на момент выгрузки, останется в облаке. Там же документ уточняет, что описанные функции definitions относятся к dedicated-инстансам, а доступность зависит от конкретного сервиса, так что сначала посмотрите, что именно доступно в вашем плане и панели.
Практический вывод: у переезда два независимых плана с разными рисками. Схему можно переносить заранее и многократно, она не торопит. Сообщения переносятся в окне переключения и требуют дисциплины. Начинайте со схемы, потому что она позволяет отрепетировать всё остальное.
Репетиция на пустой копии
Разверните будущий брокер на VPS до того, как в проекте объявлено хоть одно окно работ. Для обычной очереди задач небольшому проекту хватит скромной конфигурации, но это ваша оценка по нагрузке, а не обещание ёмкости: размер ресурсов определяется только замером потока сообщений, их размера и числа одновременных потребителей. У MAATRIX есть тарифы от 1 vCPU и 2 ГБ RAM до 16 vCPU и 32 ГБ, диски NVMe, root-доступ и выбор локации среди UK, US, RU и FR; для брокера важно, что диск быстрый и что вы сами управляете настройками узла. Общую картину установки RabbitMQ на VPS мы разбирали в материале RabbitMQ на VPS: брокер сообщений, здесь к ней возвращаться не будем.
На репетиции вы делаете три вещи:
- загружаете выгруженные definitions в новый брокер и проверяете, что виртуальные хосты, очереди, обменники и привязки на месте;
- проверяете, что пользователи и их права соответствуют ожидаемым: схему легко перенести, а потом обнаружить, что у одного сервиса нет прав на запись в нужный обменник;
- запускаете тестовый поток и смотрите, дойдёт ли сообщение от публикации до потребителя по новым адресам.
Отдельно сверьте свойства очередей: тип, долговечность, аргументы вроде времени жизни сообщений или очереди недоставленных. Именно мелкие аргументы расходятся чаще всего, потому что очередь, объявленная приложением при старте, может конфликтовать с уже существующей очередью с другими аргументами. Если приложение объявляет очередь само, а в definitions она записана иначе, получите ошибку объявления в самый неподходящий момент.
Жизнь одного сообщения: временная линия
Теперь вернёмся к заказу 48213 и разложим его путь по событиям. Состояния ниже — общее поведение AMQP-очередей, обязательное для понимания переезда; детали вашего клиента и настроек prefetch могут менять картину, поэтому проверьте их на репетиции.
- t0. Публикация. Продюсер отправляет
order.paidв обменник. До тех пор пока брокер не подтвердил приём (при включённых publisher confirms), продюсер не должен считать, что сообщение принято. Ответственность на стороне продюсера. - t1. Сообщение в очереди, статус «готово». Оно лежит у брокера и ждёт потребителя. Если очередь долговечная и сообщение помечено как постоянное, оно переживёт перезапуск узла; иначе нет. Ответственность на стороне брокера.
- t2. Выдача потребителю. Брокер отдал сообщение, но ждёт подтверждения. В интерфейсе управления это видно как «unacked». Сообщение уже не доступно другим потребителям, но брокер его не удалил. Ответственность разделена: брокер помнит, потребитель держит.
- t3. Побочный эффект. Потребитель выставляет счёт. Это уже вне RabbitMQ: брокер не знает, что произошло в бухгалтерской системе.
- t4. Подтверждение. Потребитель отправляет ack, брокер удаляет сообщение. Ответственность за сообщение с этого момента полностью у приложения, которое выполнило работу.
Между t2 и t4 сообщение находится в самой уязвимой зоне. Если соединение потребителя оборвётся до подтверждения, брокер вернёт сообщение в очередь, и оно придёт снова, возможно другому потребителю. Это не сбой, а штатный механизм: система выбирает лучше доставить дважды, чем потерять. Отсюда следует честный вывод: в конструкции с подтверждениями дубли возможны по самой природе, и обещать «ноль дублей» при переезде нельзя ни вам, ни кому-либо ещё.
Разберём это на конкретной последовательности для заказа 48213. В 10:00:00 потребитель A получил order.paid и начал выставлять счёт. В 10:00:03 бухгалтерская система приняла запрос и создала счёт. В 10:00:04 соединение потребителя A оборвалось, не успев отправить ack. В 10:00:05 брокер увидел обрыв и вернул сообщение в очередь со статусом «готово». В 10:00:06 его получил потребитель B. С точки зрения брокера всё сделано правильно: у него не было подтверждения, значит, работа не считалась завершённой. С точки зрения бухгалтерии пришёл второй запрос на тот же заказ.
Обратите внимание: никто в этой цепочке не ошибся, но эффект удвоился. Именно поэтому переезд нельзя оценивать только по состоянию очередей. Любой обрыв соединения во время окна переключения, будь то остановка потребителей, смена адреса или сетевой сбой, создаёт ровно такую пару событий: работа сделана, а подтверждение не дошло. Чем больше сообщений в момент переключения находится в зоне выдачи, тем больше таких пар возможно, поэтому осушение начинают с остановки публикаций и ожидания нулевого числа неподтверждённых.
Кто за что отвечает на трёх участках
Переезд полезно смотреть через три роли, потому что у каждой своя точка, где она может подвести.
| Участок | Владелец | Что может пойти не так при переезде |
|---|---|---|
| До приёма брокером | Продюсер | Публикация ушла в старый брокер после того, как очередь уже сняли с учёта; ответ о приёме не пришёл, продюсер повторил отправку |
| Внутри брокера | Брокер | Сообщения остались в старой очереди, потому что перенесли только definitions; переносчик остановился с ошибкой |
| После выдачи | Потребитель | Сообщение получено, но не подтверждено в момент отключения; на новом брокере оно появится как новое |
Заметьте: ни на одном из участков вопрос не решается переключением адреса. Он решается порядком действий: сначала останавливаем источник новых публикаций, потом опустошаем очередь, потом переключаем потребителей. Нарушение порядка — главный способ получить потерянные или удвоенные сообщения.
Осушение: как остановить прилив
Осушить очередь означает довести число ожидающих и неподтверждённых сообщений до нуля, пока новые не поступают. Это самый спокойный способ переезда, если вы можете позволить окно простоя публикаций. Порядок такой.
- Остановите продюсеров или переведите их в режим, в котором они складывают исходящие события у себя (например, в таблицу outbox в своей базе), а не отправляют в брокер. Пока продюсеры пишут в старый брокер, очередь не опустеет.
- Дайте потребителям доработать очередь. Следите за двумя числами: «готово» и «неподтверждено». Достаточно, чтобы оба были нулевыми по каждой очереди, которую вы переносите.
- Проверьте, нет ли других путей, которыми сообщения попадают в очередь: отложенные повторы, очереди недоставленных, планировщики, которые публикуют по расписанию. Они могут вернуть сообщение в очередь уже после того, как вы решили, что она пуста.
- Только когда все очереди пусты и новых публикаций нет, переключайте адреса потребителей и продюсеров на новый брокер.
У осушения есть цена: время, пока работа не обрабатывается. Если очередь содержит платежи, которые нельзя задерживать, или на потребителях висят медленные задачи, окно может оказаться неприемлемым. Тогда нужен второй сценарий, где сообщения переезжают вместе с потоком.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации Россия, США, Франция и Великобритания. Оплата картой РФ и по СБП.
Когда очередь не опустеет: Shovel
Для таких случаев в RabbitMQ есть плагин Shovel. Он подключается к исходному и целевому брокерам, получает сообщения из источника и публикует их в назначение. Проще говоря, это лопата, которая переносит содержимое очереди старого брокера в обменник или очередь нового, пока вы готовите остальное.
Здесь снова работает та же логика ответственности, только лопата становится промежуточным владельцем. Она принимает сообщение у источника и публикует в приёмник, и от того, когда она подтверждает приём исходному брокеру, зависит судьба сообщения при сбое. Подтверждения получения и publisher confirms влияют на гарантии переноса, а сам Shovel может переподключаться после ошибки. Эти два факта дают нам простую рабочую модель: если подтверждения настроены так, что исходное сообщение снимается только после подтверждённой публикации в назначение, потеря маловероятна, но при обрыве между публикацией и подтверждением то же сообщение придёт ещё раз. Иначе говоря, надёжность без потерь покупается возможностью дубля. Какой режим подтверждений выбрать, смотрите в документации вашей версии и проверяйте на репетиции, не полагаясь на значения по умолчанию.
Сравните два возможных момента, когда лопата освобождает исходное сообщение. Если она снимает его у источника сразу после получения, то при падении между получением и публикацией в назначение сообщение исчезнет у обоих брокеров: на старом его уже нет, на новом оно не появилось. Если же она снимает сообщение только после подтверждённой публикации в новый брокер, то при обрыве в промежутке между публикацией и подтверждением сообщение останется в источнике и после переподключения будет перенесено ещё раз. Потеря в первом случае тихая, её никто не заметит без сверки, а дубль во втором виден по повторному ключу. Для заказа 48213 второй вариант предпочтительнее, потому что дубль мы умеем опознать по order.paid:48213, а пропавший заказ приходится искать по расхождению списков.
Отсюда практическое правило для репетиции: принудительно оборвите соединение лопаты посреди переноса тестовой очереди и посчитайте, сколько сообщений пришло дважды и пришли ли все. Результат этого опыта скажет о вашей конфигурации больше, чем любое описание режимов.
Практический порядок со Shovel:
- на новом брокере уже загружены definitions и всё готово принимать;
- продюсеры остановлены или переключены на новый брокер, чтобы старая очередь больше не пополнялась;
- Shovel переносит остаток из старой очереди в новую;
- потребители работают на новом брокере и читают как перенесённое, так и свежее;
- когда счётчик на старой стороне стабильно нулевой, лопату выключают.
Проверьте заранее, можно ли в вашем сервисе включать нужный плагин и достаёт ли облачный брокер из вашей сети: подключение лопаты идёт по сетевым адресам и учётным данным, а они должны быть разрешены с обеих сторон. На своём VPS с root-доступом включить плагин и открыть порт для брокера вы можете сами, а вот ограничения на стороне облачного плана нужно уточнять у поставщика.
Повторная доставка и повторная операция — разные вещи
Теперь главный практический поворот. Допустим, на переезде всё-таки случился дубль: сообщение order.paid с заказом 48213 пришло потребителю второй раз. Это повторная доставка: брокер, лопата или сеть честно отдали одно и то же сообщение дважды. Вопрос в том, превратится ли она в повторную бизнес-операцию, то есть в два счёта на одного покупателя.
Эти два уровня закрываются разными средствами. Доставкой занимаются подтверждения и повторы. Операцией занимается ваше приложение, и единственный надёжный способ тут один: у сообщения должен быть устойчивый идентификатор, который не меняется между попытками. В нашем примере это номер заказа и тип события, например order.paid:48213. Потребитель перед выполнением работы проверяет, обработано ли такое событие, и если да, просто подтверждает сообщение и ничего не делает.
Тонкость в слове «устойчивый». Если продюсер при каждой попытке отправки генерирует новый случайный идентификатор, то повтор получит другой ключ, и проверка его не узнает. Новый идентификатор попытки сам по себе идемпотентности не даёт; ключом должно служить то, что описывает бизнес-событие, а не техническую отправку. Сгенерировали id один раз при создании события, сохранили вместе с ним, повторили публикацию с тем же id.
Разницу удобно увидеть на двух вариантах одной и той же повторной публикации. В первом продюсер не получил подтверждения приёма и повторил отправку, сохранив ключ order.paid:48213. Потребитель записывает ключ, видит конфликт уникальности, подтверждает сообщение и ничего не выставляет. Во втором продюсер при повторе собрал сообщение заново и подставил новый случайный идентификатор, например идентификатор попытки. Таблица обработанных событий такого ключа не знает, запись проходит, и покупателю уходит второй счёт, хотя доставка и там и там была «правильной».
Поэтому при репетиции переезда проверяйте не только, что дубль пришёл, но и что произошло после него. Полезный контрольный вопрос: если это же сообщение сейчас придёт ещё три раза, изменится ли что-то в бухгалтерской системе после первого раза? Если ответ «нет», доставка может повторяться без вреда; если «да», такую очередь стоит осушать с остановленными публикациями и сверять особенно тщательно.
Как это выглядит у потребителя, на условном примере:
- Получил сообщение с ключом
order.paid:48213. - Попробовал записать этот ключ в таблицу обработанных событий с ограничением уникальности.
- Если запись удалась, выполняет работу и подтверждает сообщение.
- Если запись отвергнута как дубликат, работу не выполняет, но подтверждает, чтобы брокер забыл сообщение.
Остаётся последнее слабое место: что, если запись ключа и внешний вызов (выставление счёта в чужой системе) нельзя объединить в одну транзакцию. Своя транзакция базы не охватывает внешний API автоматически. Тогда надёжнее, чтобы внешняя система тоже принимала ваш ключ и сама отсекала повтор, либо чтобы вы фиксировали статус шагов и умели возобновить работу с середины. Если внешняя сторона такого не умеет, остаточный риск дубля нужно признать и закрыть процессом, например ручной сверкой на переезде.
Сверка: как понять, что очереди совпали
После переезда нужно доказать себе, что ничего не потерялось, причём «все очереди пусты» не является доказательством. Опустевшая очередь означает только, что сообщения ушли, а не что они дошли. Сверка строится по бизнес-событиям, а не по счётчикам брокера.
Практичный приём: выберите отрезок времени вокруг окна и сравните два списка. Первый — события, которые продюсер зафиксировал как отправленные (у outbox это записи со статусом «опубликовано»). Второй — события, которые потребитель отметил как обработанные в таблице ключей. Расхождение в одну сторону означает потерю, в другую — подозрительное появление события, которое никто не публиковал. Дубли на этом фоне видны как повторные попытки по одному ключу, и по ним полезно смотреть, не создали ли они двойной эффект.
Для самих очередей запишите перед началом три числа на каждую: готовые, неподтверждённые и общее число полученных потребителем за период. Они дают вам контрольные точки, а не доказательство: цифры брокера описывают его собственную работу, а смысл сообщения проверяется только по бизнес-данным.
Когда сворачиваться и возвращаться
Репетиция только тогда имеет смысл, когда заранее оговорено условие отката. Не «посмотрим по ситуации», а конкретные признаки. Например: потребители на новом брокере не достигли нужной скорости обработки за оговорённое время; лопата останавливается с ошибками чаще, чем успевает возобновиться; сверка нашла события, которые нигде не обработаны.
У возврата есть собственная ловушка: пока старая очередь не удалена и её потребители отключены, а не сломаны, вернуться легко. Если же вы уже отправили новые события в новый брокер, возврат означает ещё один перенос, теперь в обратную сторону, и те же самые вопросы про ответственность. Поэтому не удаляйте старую инфраструктуру, пока сверка не закрыта и пока не пройдёт период, достаточный, чтобы редкие сценарии (отложенные повторы, ночные расписания) хоть раз сработали на новом месте.
Заказ 48213: чей он на каждом отрезке
Вернёмся к началу. Сообщение с заказом 48213 принято потребителем и не подтверждено. Теперь у нас есть язык, чтобы на любой вопрос о нём ответить.
Пока оно не принято брокером, его владелец продюсер, и он обязан дождаться подтверждения приёма и при сомнении повторить с тем же ключом. Пока оно готово и лежит в очереди, его владелец брокер, а при переезде в этот момент вопрос решают осушение или Shovel. В зоне выдачи без подтверждения оно общее: брокер помнит, потребитель держит, и именно здесь рождается повторная доставка. После побочного эффекта владелец потребитель, а защитой от удвоенной операции служит устойчивый ключ. И только после ack оно полностью принадлежит бизнесу, а сверка по событиям показывает, что счёт один.
Если вы можете назвать владельца сообщения на каждом из этих отрезков и знаете, что произойдёт, если владелец подведёт, переезд из CloudAMQP на собственный сервер перестаёт быть прыжком в неизвестность и становится последовательностью проверяемых шагов. Дубли при этом полностью не исчезают, но перестают быть опасными, потому что каждый из них встречается с ключом, который его узнаёт.