Устранение распространенных проблем конфигурации RabbitMQ

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

Устранение распространенных проблем конфигурации RabbitMQ

Большинство проблем конфигурации RabbitMQ на первый взгляд выглядят как ошибки в приложении. Издатель утверждает, что отправил сообщение. Потребитель говорит, что никогда его не видел. График очереди пуст или, что еще хуже, полон, и никто не знает почему. Самый быстрый способ выйти из ситуации — перестать гадать и следовать пути сообщения: издатель, обмен, привязка, очередь, потребитель, подтверждение.

RabbitMQ строг в отношении топологии. Прямой обмен не "почти" соответствует ключу маршрутизации. Очередь, объявленная как эксклюзивная, не будет вести себя как общая рабочая очередь. Сообщение, опубликованное как обязательное, может быть возвращено, в то время как такое же нерутированное сообщение без флага mandatory может быть просто отброшено обменом. Эти детали кажутся мелкими, пока не стоят вам целого дня.

Начните с фактического маршрута

При обычной публикации AMQP производитель отправляет сообщение в обмен с ключом маршрутизации. Обмен использует свой тип и привязки, чтобы решить, какие очереди должны получить сообщение. Затем потребители извлекают доставки из очередей и подтверждают их после обработки.

Когда сообщение исчезает, задайте четыре вопроса:

  • Публиковал ли производитель в тот обмен и виртуальный хост, которые вы думаете?
  • Существует ли этот обмен и является ли он тем типом, который вы думаете?
  • Есть ли привязка от этого обмена к предполагаемой очереди?
  • Соответствует ли ключ маршрутизации этой привязке для типа обмена?

Это звучит элементарно, но помогает выявить множество реальных инцидентов. Стенд и продакшн часто используют разные виртуальные хосты. Скрипт развертывания может объявить orders.created в одной среде и order.created в другой. Очередь может быть привязана к шаблону топика, который пропускает одно лишнее слово.

Используйте интерфейс управления или CLI для проверки работающего брокера, а не кода, который, как вы надеетесь, выполняется:

rabbitmqctl list_exchanges name type durable auto_delete
rabbitmqctl list_bindings source_name source_kind destination_name destination_kind routing_key
rabbitmqctl list_queues name durable auto_delete exclusive messages_ready messages_unacknowledged consumers

Если вы используете несколько виртуальных хостов, добавьте -p:

rabbitmqctl -p production list_bindings source_name destination_name routing_key

Несовпадение ключей маршрутизации

Прямые обмены требуют точного совпадения ключа привязки. Если очередь привязана с invoice.created, сообщение, опубликованное с invoices.created, не поступит. RabbitMQ не будет исправлять множественное число, регистр, точки или дефисы.

Топик-обмены используют * для одного слова и # для нуля или более слов. Разделителем слов является точка. Привязка logs.* соответствует logs.info, но не logs.app.info. Привязка logs.# соответствует обоим.

Полезный трюк для устранения неполадок — добавить временную диагностическую очередь с широкой привязкой, а затем опубликовать известное тестовое сообщение:

rabbitmqadmin declare queue name=debug.routing durable=false auto_delete=true
rabbitmqadmin declare binding source=events destination=debug.routing destination_type=queue routing_key='#'

Делайте это осторожно в продакшне и удалите диагностическую привязку после завершения. Цель — доказать, достигают ли сообщения обмена вообще.

Для важных издателей включите возвраты издателя с флагом mandatory, чтобы нерутированные сообщения были видны издателю. Подтверждения издателя говорят вам, что брокер принял публикацию; возвраты говорят, что обмен не смог направить ее ни в одну очередь. Они отвечают на разные вопросы.

Ошибки типа обмена

Изменения типа обмена являются частым источником путаницы, потому что объявление существующего обмена с другими свойствами завершается ошибкой. Если один сервис объявляет events как topic, а другой объявляет events как direct, второе объявление должно получить ошибку предусловия.

Эта ошибка — хороший знак. Она предотвращает молчаливое несогласие двух приложений о маршрутизации. Исправление заключается не в перехвате и игнорировании исключения. Исправление состоит в том, чтобы сделать владение топологией ясным. Обычно один шаг развертывания или модуль инфраструктуры должен объявлять общие обмены и очереди, в то время как приложения объявляют только частные очереди ответов или идемпотентно подтверждают ожидаемую топологию.

Обмены типа fanout игнорируют ключи маршрутизации. Обмены headers маршрутизируют по заголовкам, а не по ключам маршрутизации. Если ваше тестовое сообщение имеет правильный ключ маршрутизации, но ни одна очередь его не получает, проверьте тип обмена, прежде чем редактировать каждую привязку.

Свойства очередей, которые удивляют людей

Durable означает, что определение очереди переживет перезапуск брокера. Это не означает, что каждое сообщение внутри очереди переживет его. Чтобы сообщения пережили перезапуск, очередь должна быть долговечной, а сообщение должно быть опубликовано как persistent. Даже тогда издатели должны использовать подтверждения, если им нужно знать, когда RabbitMQ безопасно принял сообщение.

Очереди с auto-delete удаляются после того, как исчезнет их последний потребитель. Они полезны для временных подписок, но плохо подходят для общих рабочих очередей. Эксклюзивные очереди привязаны к соединению, которое их объявляет, и исчезают при закрытии этого соединения. Они полезны для очередей ответов и частных потребителей, но не для нескольких экземпляров рабочих процессов.

Если очередь "внезапно исчезает", проверьте эти флаги:

rabbitmqctl list_queues name durable auto_delete exclusive consumers

Также проверьте, объявляет ли код приложения очередь при запуске с другими аргументами, чем существующая очередь. RabbitMQ рассматривает аргументы очереди, такие как тип очереди, обмен недоставленных сообщений, максимальная длина и некоторые настройки, связанные с долговечностью, как часть контракта объявления. Несоответствие может закрыть канал с ошибкой предусловия.

Сообщения готовы, но потребители ничего не делают

Если messages_ready высок, а consumers равен нулю, RabbitMQ ждет. Приложение-потребитель может быть отключено, подключено к неправильному виртуальному хосту, использовать неправильное имя очереди или быть заблокировано разрешениями.

Если потребители подключены, но доставки не происходят, проверьте prefetch и емкость потребителя:

rabbitmqctl list_consumers queue_name channel_pid consumer_tag ack_required prefetch_count active

Потребитель с ручными подтверждениями и полным окном prefetch не будет получать больше сообщений, пока не подтвердит или не отклонит некоторые из уже имеющихся. Это часто выглядит так, как будто RabbitMQ перестал доставлять, хотя на самом деле потребитель держит неподтвержденную работу.

Если messages_unacknowledged высок, посмотрите на логи потребителя и нижестоящие системы. Медленная база данных, зависшая HTTP-зависимость или обработчик, который перехватывает исключения без подтверждения, могут создать стену неподтвержденных сообщений.

Ошибки подтверждения

Ручные подтверждения — обычный выбор для надежной обработки. Потребитель должен подтверждать только после завершения работы. Если она завершается ошибкой, он должен отклонить или nack с обдуманным решением о повторной постановке в очередь.

Опасный шаблон — auto_ack=true для работы, которая может завершиться ошибкой. При автоматических подтверждениях RabbitMQ считает сообщение обработанным сразу после доставки. Если потребитель выйдет из строя после получения, сообщение исчезнет из очереди.

Противоположная ошибка — никогда не подтверждать. Потребитель успешно обрабатывает сообщение, возможно, даже записывает в базу данных, но забывает basic_ack. RabbitMQ держит доставку неподтвержденной до закрытия канала, а затем повторно доставляет ее. Это создает дублирующую работу и растущее количество неподтвержденных сообщений.

Простая форма обработчика упрощает аудит:

def handle(ch, method, properties, body):
    try:
        process(body)
    except RetryableError:
        ch.basic_nack(method.delivery_tag, requeue=True)
    except Exception:
        ch.basic_nack(method.delivery_tag, requeue=False)
    else:
        ch.basic_ack(method.delivery_tag)

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

Разрешения и виртуальные хосты

Разрешения RabbitMQ действуют в рамках виртуального хоста. Пользователь может подключаться, но все еще не иметь прав на настройку, запись или чтение для очереди или обмена. Это может проявиться как исключение канала в логах клиента, не всегда как дружественная ошибка приложения.

Проверьте разрешения напрямую:

rabbitmqctl list_permissions -p production
rabbitmqctl list_user_permissions app_user

Для сервиса, который только публикует, предоставьте права на запись для необходимого шаблона обмена и избегайте широких прав на настройку. Для потребителя предоставьте чтение для очереди и запись, если ему нужно публиковать nack на пути недоставленных сообщений или использовать шаблоны ответов. Слишком широкие разрешения облегчают устранение неполадок сегодня, но усложняют проверки безопасности завтра.

Ошибки конфигурации недоставленных сообщений

Обмены недоставленных сообщений должны делать сбои видимыми. Неправильная настройка недоставленных сообщений делает обратное: сообщения терпят неудачу, отклоняются, а затем исчезают в обмене, у которого нет привязки.

Проверьте аргументы очереди, а не только имя очереди:

rabbitmqctl list_queues name arguments

Для очереди, которая должна отправлять недоставленные задания, вы должны увидеть аргументы, такие как x-dead-letter-exchange и, иногда, x-dead-letter-routing-key. Затем проверьте этот обмен и его привязки так же, как вы проверяете основной маршрут.

Распространенная ошибка — настроить обмен недоставленных сообщений с именем jobs.dlx, но привязать очередь недоставленных сообщений к jobs.failed на другом обмене. Другая — установить x-dead-letter-routing-key на значение, которому не соответствует ни одна привязка. RabbitMQ направит сообщение о недоставке через обмен недоставленных сообщений, как и любую другую публикацию. Если ничто не соответствует, сообщение не имеет полезного места назначения.

Очереди повторных попыток требуют такого же внимания. Если вы строите повторные попытки с TTL и недоставкой, нарисуйте маршрут на бумаге:

основная очередь -> reject -> обмен повторных попыток -> очередь повторных попыток -> TTL истекает -> основной обмен -> основная очередь

Затем проверьте каждый обмен, очередь, привязку и ключ маршрутизации. Циклы повторных попыток легко создать случайно. Установите ограничение на количество попыток в заголовках сообщений или состоянии приложения, чтобы одно сломанное сообщение не крутилось вечно.

Сюрпризы политик

Политики могут изменять поведение очереди без упоминания в коде приложения. Политика может установить тип очереди, максимальную длину, TTL, обмен недоставленных сообщений или другие необязательные аргументы. Это полезно для операций, но может запутать отладку, когда очередь ведет себя иначе, чем объявлено в коде.

Перечислите политики во время устранения неполадок:

rabbitmqctl list_policies

Посмотрите на шаблон и приоритет. Широкая политика, такая как .*, может повлиять на очереди, созданные позже несвязанными командами. Если очередь отбрасывает старые сообщения, проверьте настройки max-length или overflow. Если сообщения истекают раньше, чем ожидалось, проверьте TTL на уровне очереди и истечение срока действия для каждого сообщения.

Когда приложение объявляет один набор аргументов, а политика применяет другой, правила RabbitMQ зависят от настройки. Некоторые необязательные аргументы могут контролироваться политикой; другие должны соответствовать объявлению. Безопасная операционная привычка — держать поведение очереди в одном очевидном месте и документировать любую политику, которая намеренно переопределяет настройки приложения по умолчанию.

Когда производители и потребители объявляют топологию

Многие клиентские библиотеки позволяют каждому сервису легко объявлять обмены, очереди и привязки при запуске. Это может быть удобно в разработке. В продакшне это может создать проблемы с владением.

Если и производитель, и потребитель объявляют одну и ту же очередь, они должны согласовать каждое важное свойство. Если одно развертывание изменяет очередь с auto-delete на durable или изменяет аргумент недоставленных сообщений, следующий запущенный сервис может завершиться ошибкой предусловия. Это лучше, чем молчаливый дрейф, но все равно может сломать развертывание.

Для общей топологии предпочтите одного владельца: Terraform, Ansible, задание миграции или один четко ответственный сервис. Запуск приложения все еще может подтвердить, что ожидаемая топология существует, но не должен создавать общие очереди с настройками по умолчанию, которые никто не проверял.

Частная топология — другое дело. Сервис, создающий временную очередь ответов или эксклюзивную очередь подписки, может владеть этой очередью напрямую. Разница в том, зависит ли другой сервис от имени очереди и поведения.

Держите один известный рабочий путь публикации

Для важных систем держите крошечный диагностический издатель или команду runbook, которая отправляет безвредное сообщение через ожидаемый обмен, ключ маршрутизации и виртуальный хост. Он должен использовать тот же класс учетных данных, что и реальное приложение, или, по крайней мере, набор разрешений, достаточно близкий, чтобы выявить проблемы маршрутизации и доступа.

Этот известный рабочий путь полезен во время развертываний. Если диагностическое сообщение маршрутизируется, а сообщение приложения — нет, сравните фактический ключ маршрутизации, заголовки и виртуальный хост из приложения. Если диагностическое сообщение также терпит неудачу, проблема, вероятно, в топологии, разрешениях или состоянии брокера.

Практический контрольный список инцидентов

Когда сообщения отсутствуют или застряли, соберите текущее состояние перед перезапуском всего:

rabbitmqctl -p production list_queues name messages_ready messages_unacknowledged consumers state
rabbitmqctl -p production list_bindings source_name destination_name routing_key
rabbitmqctl -p production list_exchanges name type durable
rabbitmqctl -p production list_connections name user vhost state

Затем отправьте одно известное тестовое сообщение с уникальным идентификатором и проследите его через логи. Не тестируйте со случайным продакшн-сообщением, чей путь уже неясен.

Большинство проблем конфигурации RabbitMQ не являются загадочными, если вы выстроите объявленную топологию в соответствии с поведением издателя и потребителя. Брокер обычно делает именно то, что ему сказали. Работа заключается в том, чтобы найти место, где то, что ему сказали, отличается от того, что подразумевала команда.