• Контекст:

У меня есть рукописный сервер заданий (думаю, очень ограниченный эквивалент чего-то вроде Gearman), который использует AMQP и RabbitMQ в качестве брокера обмена сообщениями. Я использую Rabbit 3 и могу обновить его по мере необходимости. Я запускаю много экземпляров этого сервера, большинство из которых используют одну и ту же рабочую очередь.

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

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

  1. Получите сообщение.
  2. Распакуйте / проанализируйте / проверьте содержимое сообщения.
  3. Подтвердите сообщение.
  4. Выполните операцию, указанную в сообщении.
  5. GOTO step 1.

Требование неповторяемости работы настолько важно, что риски, присущие описанному выше сценарию (сообщения падают на пол, если сервер заданий останавливается между шагами 3 и 4), являются приемлемыми.

  • Проблема:

Иногда шаг 4 занимает очень много времени. Поскольку клиент AMQP уже подтвердил свое единственное сообщение (при условии, что qos из 1), RabbitMQ отправляет ему другое сообщение, если оно существует, и это сообщение остается в состоянии unacked до тех пор, пока сервер заданий не завершит выполнение своего текущего сообщения. Это очень плохо, потому что выполняемая работа имеет очень высокий приоритет и должна выполняться как можно скорее, и почти всегда есть доступные потребители, которые не в процессе выполнения работы, которые могут немедленно выполнить запрошенные операции.

Отсюда мой вопрос:

Оставаясь потребителем в очереди, как я могу гарантировать, что сообщение будет обработано только один раз, без использования / распаковки каких-либо других сообщений на этапе обработки?

  • Что я пробовал:

1. Первым делом Itried установил для QoS (иногда называемого размером предварительной выборки, но это неправильное название, поскольку здесь не используется «выборка», а просто отправка сервером) на 1 или 0. Установка на 1 уменьшает серьезность этой проблемы, но не решает ее, так как одно сообщение все еще может ожидать завершения обработки предыдущего сообщения. Установка QoS на 0 не ограничивает размер локального буфера, что значительно усугубляет проблему.

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

3. Наивное решение записывать запись в какое-то центральное состояние каждый раз при запуске сообщения (т. е. повторная реализация блокировки сообщения) оказывается слишком медленным.

4. Я попытался отменить потребление сразу после подтверждения сообщения, заблокировать все в локальном буфере и восстановить потребление для следующего сообщения. Это было очень медленным для клиентов, генерировало намного больше сетевого трафика и (что хуже всего) заставляло сервер Rabbit испытывать очень высокую нагрузку и постепенно снижать производительность. Управляющий API / пользовательский интерфейс также стал вялым и трудным в использовании, мои федеративные обмены начали зависать, а HA начала рассинхронизироваться.

5. Я подумал о сложном топологическом решении: во-первых, я бы обеспечил, чтобы все сообщения имели ключ маршрутизации с уникальным идентификатором (позже я мог бы использовать обмен заголовками и поле message_id AMQP , но это более простая реализация). Все сообщения будут публиковаться в тематическом обмене xA, альтернативным обменом которого является xB. xB будет прямым обменом и будет напрямую связываться с qC, рабочей очередью. Я бы также создал qD, который будет содержать только те работы, которые не удалось обработать или были отклонены из-за сбоя на экземпляре сервера заданий. Затем я мог бы запустить где-нибудь специализированный демон / постоянно работающую программу, которая просматривает все содержимое qC, подтверждает их все и вручную повторно публикует каждую в xA. Все сообщения в любом случае попадают в систему через xA, а в состоянии по умолчанию они попадают в qC независимо от их ключей маршрутизации.

Тогда жизненный цикл сообщения для каждого клиента будет выглядеть так:

  1. Получите сообщение.
  2. Распакуйте / проанализируйте / проверьте содержимое сообщения.
  3. Если сообщение имеет повторно доставленный бит устанавливать:
    • Подтвердите сообщение.
    • Переиздайте его как есть до xA.
    • Подождите подтверждения.
    • Удалите привязку от xA к qD с помощью ключа маршрутизации полученного сообщения (UID).
  4. Else :
    • Добавьте привязку от xA к qD с ключом маршрутизации полученного сообщения (UID).
    • Выполните операцию, указанную в сообщении.
    • Подтвердить сообщение.
    • НАЙТИ 1.

Таким образом, если сообщение не удалось во время фазы «выполнения работы» и было неявно отклонено с помощью requeue=1, будет установлен бит redelivered. Следующий экземпляр сервера заданий, который получит это повторно доставленное сообщение, подтвердит его и повторно опубликует на xA, который направит его на qD. После подтверждения публикации (сообщение попало в qD) привязка будет удалена.

Это решение решило бы проблему лучше, чем любые другие теории, которые у меня были. Однако я думаю, что огромное количество привязок, которые потенциально могут быть созданы (и количество запросов на добавление / удаление привязки / вторая скорость), очень вероятно, серьезно повлияет на производительность Rabbit. Эта ветка обсуждения, похоже, подтверждает эту теорию. В идеале мне нужно решение, которое не отправляет больше сетевого трафика на сервер кролика и с него, чем это абсолютно необходимо, и не создает на сервере больше постоянных объектов (временные сообщения, очереди, привязки и т. Д.), Чем это абсолютно необходимо.

2
Zac B 7 Янв 2014 в 20:07

1 ответ

Лучший ответ

Этот ответ соответствует любой реализации AMQP, но у меня только опыт работы с RabbitMQ. Есть два возможных способа сделать то, что вы хотите. У них обоих есть свои преимущества и недостатки, поэтому здесь они не расположены в произвольном порядке:

basic.get в интервале

Возможно, вы не хотите продолжать употребление, пока не закончите с последним сообщением. AMQP имеет функцию basic.get, с помощью которой вы потребляют только одно сообщение. Обратной стороной этого решения является то, что basic.get не блокирует и возвращает сообщение get-empty в пустой очереди, и в этом случае вам придется немного подождать, прежде чем пытаться снова. Это может быть для вас приемлемым, а может и неприемлемым. Идя по этому пути, я бы сделал следующее:

  1. basic.get
  2. Если get-empty, спать x миллисекунд, а затем перейти к 1
  3. Подтвердить сообщение
  4. Работать
  5. Перейти к 1

Обратите внимание, что вы спите только тогда, когда очередь пуста. Пока есть работа, никто не уснет.

Проверьте флаг redelivered

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

basic.consume с QoS равным 1.

  1. Потребляйте сообщение
  2. Если redelivered=0, перейдите к 5
  3. Отклонить сообщение с помощью requeue=false
  4. Перейти к 1
  5. Работать
  6. Подтвердить сообщение
  7. Перейти к 1

Бонус этого решения заключается в том, что вы можете настроить своего брокера на сохранение отклоненных сообщений в обмене недоставленными сообщениями Так они не заблудились. Если вы не используете обмен недоставленными сообщениями, вы можете выбрать только подтверждение сообщения на шаге 3, но reject имеет более четкое семантическое значение.

3
Emil Vikström 11 Фев 2014 в 14:46