Долговечность: durable и persistent
Перезапустили брокер — и очередь с сорока тысячами заказов исчезла бесследно. Разбираем два флага, которые работают только вместе.
durable — свойство очереди (и exchange): пережить перезапуск брокера. persistent (
delivery_mode=2) — свойство сообщения: быть записанным на диск. Одно без другого не спасает.
Начинающий разработчик обычно узнаёт про эти флаги в момент, когда админ обновляет RabbitMQ на проде. Локально всё работало годами — потому что локально брокер не перезапускался. Стоит контейнеру уехать в рестарт, как выясняется, что по умолчанию в RabbitMQ ничего не долговечно: и очередь, и сообщения живут в памяти, и оба флага надо просить явно.
Два независимых флага
Флаги независимы, поэтому у нас четыре комбинации — и три из них ничего не гарантируют:
| Очередь | Сообщение | Что останется после рестарта брокера |
| transient (по умолчанию) | transient (по умолчанию) | Ничего: ни очереди, ни сообщений |
| transient | persistent | Ничего. Очередь исчезла — сообщения ушли вместе с ней, persistent сам по себе бесполезен |
| durable | transient | Очередь на месте, но пустая. Классическое «куда делись заказы?» |
| durable | persistent | Очередь на месте, сообщения на месте |
Логика простая, если помнить, что это про разные сущности. durable у очереди — это про метаданные: имя, аргументы, привязки. Брокер записывает их в свою служебную базу схемы и восстанавливает при старте. Про содержимое очереди durable не говорит вообще ничего — за содержимое отвечает флаг на каждом отдельном сообщении.
Как объявить правильно
Полная «цепочка долговечности» на стороне продюсера:
import pika
connection = pika.BlockingConnection(pika.ConnectionParameters("localhost"))
channel = connection.channel()
channel.exchange_declare(exchange="orders", exchange_type="direct", durable=True)
channel.queue_declare(queue="orders.new", durable=True)
channel.queue_bind(queue="orders.new", exchange="orders", routing_key="new")
channel.confirm_delivery() # publisher confirms: ждём подтверждения от брокера
channel.basic_publish(
exchange="orders",
routing_key="new",
body=b'{"order_id": 42, "sum": 1990}',
properties=pika.BasicProperties(
delivery_mode=pika.DeliveryMode.Persistent, # то же самое, что delivery_mode=2
message_id="order-42",
content_type="application/json",
),
mandatory=True,
)
print("брокер подтвердил, что сообщение записано")
Что здесь важно и почему:
exchange_declare(..., durable=True). Про exchange забывают чаще всего. Если он транзиентный, после рестарта его просто не будет, и продюсер начнёт публиковать в несуществующий exchange — а брокер, если не попросить иначе, молча выбросит такие сообщения.queue_declare(..., durable=True). Очередь и её привязки переживут перезапуск.delivery_mode=pika.DeliveryMode.Persistent— это константа со значением 2 (1 —Transient). Она говорит брокеру: тело сообщения обязано лечь на диск.confirm_delivery()включает на канале режим publisher confirms. С нимbasic_publishвBlockingConnectionблокируется до ответа брокера и бросает исключение, если сообщение не принято или некуда маршрутизировать. Для persistent-сообщения в durable-очереди брокер присылает confirm после того, как реально сохранил сообщение.mandatory=True— если по routing key не подошла ни одна очередь, брокер вернёт сообщение продюсеру, а не выбросит. Опечатка в routing key перестаёт быть невидимой.
Почему одного persistent мало
Полезно держать в голове всю цепочку, по которой идёт сообщение: продюсер → сеть → exchange → очередь → диск → консьюмер. Долговечность — это свойство самого слабого звена. Транзиентный exchange, транзиентная очередь, публикация без confirms, auto_ack у консьюмера — каждый из этих пунктов в одиночку обнуляет все остальные усилия. «Мы поставили delivery_mode=2, а сообщения всё равно теряются» почти всегда означает, что где-то в цепочке дыра.
Реплики: кворумные очереди
Диск одного узла тоже смертен. Если брокер собран в кластер, вместо классической очереди берут quorum queue — реплицированную очередь на алгоритме Raft:
channel.queue_declare(
queue="orders.new",
durable=True,
arguments={"x-queue-type": "quorum"}, # реплика на большинстве узлов кластера
)
Кворумная очередь всегда durable и всегда пишет на диск, а подтверждение продюсеру уходит только после записи на большинство узлов. delivery_mode для неё теряет смысл — она хранит всё. Плата — больше дисков, больше сети и меньше пропускная способность. Классические очереди остаются там, где важна скорость, а не сохранность.
Как это работает
Persistent-сообщение попадает в message store — общий журнал на диске, а ссылка на него пишется в индекс очереди. При старте брокер читает индекс и восстанавливает очередь. Здесь есть три неочевидных момента.
Запись не равна fsync. Брокер копит записи и сбрасывает их на диск батчами — раз в несколько сотен миллисекунд. Между «данные ушли в страничный кэш ОС» и «данные физически на диске» есть окно. Выдернули питание в это окно — persistent-сообщение всё-таки может пропасть. Именно эту дыру закрывают publisher confirms: confirm приходит после fsync, поэтому «продюсер получил подтверждение» и есть настоящая граница долговечности.
Транзиентное сообщение тоже может оказаться на диске. Под давлением памяти брокер выгружает тела сообщений на диск (page out), чтобы не словить memory alarm. Это своп, а не долговечность: при рестарте такие сообщения не восстановятся. Не обманывайтесь, увидев рост дискового I/O на транзиентной очереди.
Флаги очереди неизменяемы. Нельзя «дообъявить» существующую transient-очередь как durable. Попытка объявить очередь с параметрами, отличными от текущих, закрывает канал:
Channel closed by broker: 406 (PRECONDITION_FAILED) - inequivalent arg 'durable'
for queue 'orders.new' in vhost '/': received 'true' but current is 'false'
Варианта два: удалить очередь (разобравшись, что делать с её содержимым) или завести новую под другим именем и переключить консьюмеров. На проде это отдельная мини-операция, поэтому durable-флаги стоит ставить сразу, а не «потом, когда пойдёт нагрузка».
Цена в производительности
Долговечность оплачивается диском. Порядок величин такой: классическая очередь с транзиентными сообщениями легко держит десятки тысяч сообщений в секунду, та же очередь с persistent — обычно единицы тысяч. Точные числа зависят от размера сообщения, диска и того, ждёте ли вы confirm на каждое сообщение, — меряйте у себя (в поставке есть rabbitmq-perf-test). Что помогает не платить лишнего:
- Не ждать confirm на каждое сообщение синхронно. Это добавляет полный round-trip до брокера к каждой публикации. Для потоков публикуйте пачками и подтверждайте пачками (в pika — асинхронный
SelectConnectionс колбэками confirm). - Не ставить persistent «на всякий случай» везде. Лог кликов и метрики отлично живут транзиентными.
- Держать очереди короткими. Очередь-свалка на миллионы сообщений заставляет брокер работать с диском, а не с памятью, и деградирует всё. Быстрый консьюмер надёжнее любых флагов.
Частые ошибки
- Поставили
durable=Trueу очереди и забылиdelivery_mode. Самый популярный сценарий «очередь есть, сообщений нет». - Забыли
durable=Trueу exchange. После рестарта продюсер публикует в пустоту, а безmandatory=True— ещё и молча. - Переобъявили очередь с новыми флагами. Получили
406 PRECONDITION_FAILEDи закрытый канал. Параметры очереди на лету не меняются. auto_delete=Trueилиexclusive=Trueна «важной» очереди. Такая очередь исчезнет, как только отключится последний консьюмер, —durableей не поможет.- Полагаться на persistent без publisher confirms. Успешный
basic_publishсам по себе означает лишь «данные записаны в сокет». Брокер мог не принять сообщение, а вы об этом не узнаете. - Забыть про свободное место на диске. Когда его остаётся меньше
disk_free_limit(по умолчанию 50 МБ), брокер поднимает alarm и блокирует всех продюсеров — приложение намертво зависает наbasic_publish. Свободное место на диске брокера — обязательная метрика в мониторинге. - Считать, что durable = «ничего никогда не потеряется». Долговечность — это цепочка: durable exchange, durable queue, persistent message, publisher confirms, ручной ack на стороне консьюмера. И реплики, если не готовы терять данные вместе с диском одного узла.
Итоги
- По умолчанию в RabbitMQ ничего не переживает рестарт: и очереди, и сообщения транзиентны.
durable=Trueу очереди и exchange сохраняет метаданные;delivery_mode=2(Persistent) сохраняет тело сообщения. Нужны оба — по отдельности они бесполезны.- Publisher confirms (
confirm_delivery()) — граница, после которой сообщение действительно на диске. Без них persistent даёт лишь надежду. - Флаги очереди нельзя изменить после создания: неверное объявление приводит к
406 PRECONDITION_FAILED. - Нужна устойчивость к смерти узла — берите quorum queue: реплика на большинстве узлов кластера.
- Долговечность стоит производительности. Включайте её осознанно и там, где потеря сообщения дороже задержки.