Java & RabbitMQ - Queuing & Multithreading - или Couchbase как Job-Queue

У меня есть один Job Distributor, который публикует сообщения на разных Channels.

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

Назовите эти задачи (просто примеры):

  • FIBONACCI (генерирует числа фибоначчи)
  • RANDOMBOOKS (генерирует случайные предложения для написания книги)

Эти задачи работают до 2-3 часов, а должны быть разделены поровну на каждый Consumer.

Каждый пользователь может иметь x параллельные потоки для работы над этими задачами. Поэтому я говорю: (эти числа являются просто примерами и будут заменены переменными)

  • Машина 1 может использовать 3 параллельные задания для FIBONACCI и 5 параллельных заданий для RANDOMBOOKS
  • Машина 2 может использовать 7 параллельных заданий для FIBONACCI и 3 параллельных заданий для RANDOMBOOKS

Как я могу это достичь?

Нужно ли начинать x Threads для каждого Channel для прослушивания на каждом Consumer?

Когда мне это нужно?

Мой текущий подход только для одного Consumer: Start x Threads для каждой задачи - каждый поток - это реализация Defaultconsumer Runnable. В методе handleDelivery я вызываю basicAck(deliveryTag,false), а затем выполняю работу.

Далее: я хочу отправить некоторые задачи специальному потребителю. Как я могу достичь этого в сочетании с справедливым распределением, как указано выше?

Это мой код для publishing

String QUEUE_NAME = "FIBONACCI";

Channel channel = this.clientManager.getRabbitMQConnection().createChannel();

channel.queueDeclare(QUEUE_NAME, true, false, false, null);

channel.basicPublish("", QUEUE_NAME,
                MessageProperties.BASIC,
                Control.getBytes(this.getArgument()));

channel.close();

Это мой код для Consumer

public final class Worker extends DefaultConsumer implements Runnable {
    @Override
    public void run() {

        try {
            this.getChannel().queueDeclare(this.jobType.toString(), true, false, false, null);
            this.getChannel().basicConsume(this.jobType.toString(), this);

            this.getChannel().basicQos(1);
        } catch (IOException e) {
            // catch something
        }
        while (true) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                Control.getLogger().error("Exception!", e);
            }

        }
    }

    @Override
    public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] bytes) throws IOException {
        String routingKey = envelope.getRoutingKey();
        String contentType = properties.getContentType();
        this.getChannel().basicAck(deliveryTag, false); // Is this right?
        // Start new Thread for this task with my own ExecutorService

    }
}

В этом случае класс Worker запускается дважды: один раз для FIBUNACCI и один раз для RANDOMBOOKS

UPDATE

Как сказано в ответах, RabbitMQ не будет лучшим решением для этого, но лучше всего использовать подход Couchbase или MongoDB. Я новичок в этих системах, есть ли кто-нибудь, кто мог бы объяснить мне, как это будет достигнуто?

Ответ 1

Вот концептуальный взгляд на то, как я построю это на couchbase.

  • У вас есть несколько машин для обработки заданий и некоторое количество машин (возможно, те же самые), которые создают задания.
  • Вы можете создать документ для каждого задания в ведре в couchbase (и установить его тип в "job" или что-то, если вы смешиваете его с другими данными в этом ведре).
  • Каждое описание задания вместе с конкретными командами, которые должны быть выполнены, может включать в себя время, в которое оно было создано, время, в течение которого оно (при наличии определенного времени), и какое-то генерируемое рабочее значение. Это рабочее значение будет произвольным числом единиц.
  • Каждый потребитель рабочих мест знал бы, сколько рабочих единиц он может делать одновременно, и сколько доступно (потому что другие рабочие могут работать.)
  • Таким образом, машина с, скажем, 10 рабочими единицами мощности, которая имеет 6 рабочих единиц, будет выполнять запрос, ищущий работу с 4 рабочими единицами или менее.
  • В couchbase есть представления, которые поэтапно обновляются на карте/сокращении рабочих мест, я думаю, вам понадобится только фаза карты. Вы должны написать представление, которое позволяет запрашивать время, время, введенное в систему, и количество рабочих единиц. Таким образом, вы можете получить "самую просроченную работу из 4 рабочих единиц или меньше".
  • Этот вид запроса, поскольку пропускная способность освобождается, сначала получит самые просроченные рабочие места, хотя вы можете получить самую большую просроченную работу, а если нет, то самая большая непродуманная работа. (Где "просроченная" - это дельта между текущим временем и датой выполнения задания.)
  • Представления Couchbase допускают очень сложные запросы, подобные этому. И хотя они постепенно обновляются, они не идеальны в реальном времени. Таким образом, вы бы не искали ни одной работы, а список кандидатов на работу (заказывали, как хотите.)
  • Итак, следующим шагом было бы взять список кандидатов на работу и проверить второе место - возможно, ведро membase (например: RAM Cache, не стойкое) для файла блокировки. Файл блокировки будет иметь несколько фаз (здесь вы немного разбираетесь в логике разделения разделов, используя CRDT или любые методы, которые лучше всего подходят для ваших нужд.)
  • Так как это ведро - это баран, то он быстрее, чем представления, и будет иметь меньше отставания от общего состояния. Если нет файла блокировки, то создайте его со статусом "предварительный".
  • Если другой рабочий получает одно и то же задание и видит файл блокировки, он может просто пропустить этот кандидат на работу и сделать следующий в списке.
  • ЕСЛИ, как-то два работника пытаются создать файлы блокировки для одной и той же работы, будет конфликт. В случае конфликта вы можете просто пнуть. Или вы можете иметь логику, где каждый работник делает обновление файла блокировки (разрешение CRDT, таким образом, делает эти идемпотенты так, чтобы братья и сестры могли быть объединены), возможно, добавляя случайное число или некоторый показатель приоритета.
  • Через определенный промежуток времени (возможно, несколько секунд) файл блокировки проверяется работником, и если ему не пришлось участвовать в изменениях разрешения гонки, он изменяет статус файла блокировки с "предварительных", "взято"
  • Затем он обновляет саму работу со статусом "взято" или некоторым таким, чтобы он не отображался в представлениях, когда другие работники ищут доступные задания.
  • Наконец, вы захотите добавить еще один шаг, прежде чем делать запрос для получения этих кандидатов на работу, описанных выше, вы делаете специальный запрос для поиска заданий, которые были сделаны, но там, где замешанный работник умер. (например: просроченные задания).
  • Один из способов узнать, когда рабочие умирают, заключается в том, что файл блокировки, помещенный в ведро membase, должен иметь время истечения, которое в конечном итоге приведет к его исчезновению. Возможно, это время может быть коротким, и рабочий просто прикасается к нему для обновления срока действия (это поддерживается в API couchbase).
  • Если рабочий умирает, в конечном итоге его файлы блокировки будут рассеиваться, а потерянные задания будут помечены как "взятые", но без файла блокировки, что является условием поиска работниками, ищущими работу.

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

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

Другим способом может быть создание случайного значения между 1-N, где N - достаточно большое число, например 4X - число рабочих, и каждое задание должно быть помечено этим значением. Каждый раз, когда работник ищет работу, он может бросить кубики и посмотреть, есть ли какие-либо задания с этим номером. Если нет, он будет делать это снова, пока не найдет работу с этим номером. Таким образом, вместо нескольких работников, претендующих на несколько "старейших" или высокоприоритетных заданий, и более правдоподобность конкуренции блокировки, они будут распространяться... ценой времени в очереди более случайным, чем ситуация с FIFO.

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

Изменить для добавления:

На шаге 12, где я говорю "возможно, добавив случайное число", я имею в виду, что если работники знают приоритет (например: что нужно делать больше всего), они могут поместить цифру, представляющую это в файл, Если нет понятия "нуждаться" в задании, тогда они могут оба бросить кубики. Они обновляют этот файл своей ролью в кости. Тогда оба они могут посмотреть на него и посмотреть, что другой свернул. Если они проиграли, то они плуют, а другой рабочий знает, что это у него есть. Таким образом, вы можете решить, какой работник выполняет работу без большого количества сложных протоколов или переговоров. Я предполагаю, что оба работника нажимают один и тот же файл блокировки здесь, он может быть реализован с двумя файлами блокировки и запросом, который находит все из них. Если через какое-то время ни один работник не прокатил большее число (и новые работники, думающие о том, что его работа будет знать, что другие уже скатываются на него, чтобы они пропустили его), вы можете спокойно выполнять работу, зная, что вы единственный рабочий работаю над этим.

Ответ 2

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

Давайте немного сломаем его, потому что здесь много вопросов.

Разделение задач на разных потребителей

Ну, один из способов сделать это - использовать циклический подход, но это довольно грубо и не учитывает, что для выполнения разных задач может потребоваться другое время. Так что делать. Ну, один из способов сделать это - установить prefetch на 1. Предварительная выборка означает, что потребитель кэширует сообщения локально ( note: сообщение еще не потребляется). Установив это значение в 1, не произойдет предварительная выборка. Это означает, что ваш потребитель будет знать и только иметь сообщение, которое он в настоящее время работает в памяти. Это позволяет получать сообщения только тогда, когда рабочий не работает.

Когда для подтверждения

С помощью описанной выше процедуры можно прочитать сообщение из очереди, передать его одному из ваших потоков и затем подтвердить сообщение. Сделайте это для всех доступных потоков -1. Вы не хотите подтверждать последнее сообщение, потому что это означает, что вы откроете для получения другого сообщения, которое вы еще не сможете передать одному из ваших сотрудников. Когда один из потоков заканчивается, когда вы подтверждаете это сообщение, таким образом вы всегда будете иметь свои потоки, работающие с чем-то.

передача специальных сообщений

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

Я бы рекомендовал вам читать AMQP и RabbitMQ, это может быть хорошая начальная точка.

предостережений

В моем предложении и в вашем дизайне есть один серьезный недостаток, и мы хотим, чтобы мы ACK сообщение, прежде чем мы действительно закончили его обработку. Это означает, что когда (а не если) наше приложение судорожно, у нас нет возможности воссоздать сообщения ACKed. Это можно решить, если вы знаете, сколько потоков вы собираетесь начать заранее. Я не знаю, можно ли динамически изменять счет предварительной выборки, но почему-то я сомневаюсь в этом.

Некоторые мысли

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

Рекомендуемое чтение

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

Ответ 3

Вот мои мысли по твоему вопросу. Как отметил в своем ответе @Daniel, я считаю, что это скорее вопрос архитектурных принципов, чем воплощения. Как только архитектура станет понятной, реализация становится тривиальной.

Во-первых, я хотел бы затронуть вопрос, связанный с теорией планирования. Здесь у вас очень длительные задачи, и если они не запланированы должным образом, вы либо (а) закончите работу на своих серверах с максимальной пропускной способностью, либо (б) возьмите намного больше времени, чтобы завершить задачи, чем это возможно, Итак, у меня есть некоторые вопросы, связанные с вашей парадигмой планирования:

  • У вас есть возможность оценить, сколько времени займет каждая работа?
  • У заданий есть связанная с ними дата, и если да, то как она определяется?

Является ли RabbitMQ подходящим в этом случае?

Я не верю, что RabbitMQ - это правильное решение для отправки чрезвычайно длительных рабочих мест. На самом деле, я думаю, у вас возникают эти вопросы в результате того, что RabbitMQ не является подходящим инструментом для работы. По умолчанию у вас недостаточно информации о задачах, прежде чем вы удаляете их из очереди, чтобы определить, какие из них следует обрабатывать следующим образом. Во-вторых, как упоминалось в ответе @Daniel, вы, вероятно, не сможете использовать встроенный механизм ACK, потому что, вероятно, было бы плохо, если бы задание переустанавливалось в очередь при сбое соединения с сервером RabbitMQ.

Вместо этого я бы поискал что-то вроде MongoDB или Couchbase для хранения вашей "очереди" для заданий. Затем вы можете иметь полный контроль над логикой диспетчеризации, а не полагаться на встроенный циклический запуск, выполняемый RabbitMQ.

Другие соображения:

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

В этом случае я не думаю, что вы хотите использовать push-based потребитель. Вместо этого используйте систему на основе pull (в RabbitMQ это будет называться Basic.Get). Делая это, вы возьмете на себя ответственность за планирование работы

Потребитель 1 имеет 3 потока для FIBONACCI и 5 потоков для RANDOMBOOKS. Потребитель 2 имеет 7 потоков для FIBONACCI и 3 потока для RANDOMBOOKS. Как я могу достичь этого?

В этом случае я не уверен, что понимаю. У вас есть одно задание fibonacci, и вы каким-то образом выполняете его параллельно на своем сервере? Или вы хотите, чтобы ваш сервер выполнял MANY fibonacci задания одновременно? Предполагая последнее, вы должны создать потоки для работы на сервере, а затем назначьте им задания до тех пор, пока все ваши потоки не будут заполнены. Когда поток становится доступным, вы должны опросить очередь для запуска другого задания.

Другие вопросы, которые у вас были:

  • Нужно ли начинать x потоков для каждого канала для прослушивания на каждого пользователя?
  • Когда мне нужно это сделать?
  • Мой текущий подход только для одного Потребителя: Начало x Потоки для каждого
  • Задача - каждый поток - это DefaultConsumer, реализующий Runnable. В методе handleDelivery я вызываю basicAck (deliveryTag, false), а затем выполняю работу.
  • Далее: я хочу отправить некоторые задачи специальному потребителю. Как я могу достичь этого в сочетании с справедливым распределением, как указано выше?

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

Использование Couchbase

Хотя подробное объяснение того, как использовать Couchbase в качестве очереди, выходит за рамки этого вопроса, я могу предложить несколько указателей.

  • Сначала вы хотите прочитать Couchbase
  • Я рекомендую хранить задания в ковке Couchbase и полагаться на индексированное представление для отображения доступных заданий. Существует множество вариантов определения ключа для каждого задания, но сама работа должна быть сериализована в JSON. Возможно, используйте ServiceStack.Text
  • Когда задание вытягивается для обработки, должна быть какая-то логика, чтобы отметить статус задания в Couchbase. Вам нужно будет использовать метод CAS, чтобы убедиться, что кто-то еще не выполнил задание для обработки в то же время, что и у вас.
  • Вам потребуется какая-то политика для устранения неудавшихся и завершенных заданий из вашей очереди.

Резюме

  • Не используйте RabbitMQ для этого
  • Используйте параметры каждого задания для разработки интеллектуального алгоритма диспетчеризации. Я могу помочь вам в этом, когда узнаю больше о характере вашей работы.
  • Потяните задания в рабочие на основе алгоритма в # 2, а не отталкивайте их с сервера.
  • Придумайте свой собственный способ отслеживания состояния заданий в вашей системе (в очереди, запуске, неудачной, успешной и т.д.), а также при повторной отправке остановленных заданий.

Ответ 4

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

Пример кода из Spring Справочная документация AMQP

@Configuration
public class ExampleAmqpConfiguration {

    @Bean
    public MessageListenerContainer messageListenerContainer() {
        SimpleMessageListenerContainer container = new SimpleMessageListenerContainer();
        container.setConnectionFactory(rabbitConnectionFactory());
        container.setQueueName("some.queue");
        container.setMessageListener(exampleListener());
        return container;
    }

    @Bean
    public ConnectionFactory rabbitConnectionFactory() {
        CachingConnectionFactory connectionFactory = new CachingConnectionFactory("localhost");
        connectionFactory.setUsername("guest");
        connectionFactory.setPassword("guest");
        return connectionFactory;
    }

    @Bean
    public MessageListener exampleListener() {
        return new MessageListener() {
            public void onMessage(Message message) {
                System.out.println("received: " + message);
            }
        };
    }
}

Ответ 5

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

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

В Twitter появился вопрос о настройке этой конфигурации, позволяющей подключить пользовательский Executor к ConnectionFactory. Я хотел бы изложить, почему это сложно, обсудить возможную реализацию и посмотреть, есть ли большой интерес.

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

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

Когда мы вводим несколько потоков, мы должны гарантировать, что каждый потребитель соединен только с одним потоком. При использовании абстракции Executor это не позволяет каждой отправке обратного вызова быть обернутым в Runnable и отправлено Executor, потому что вы не можете гарантировать, какой поток будет использоваться.

Чтобы обойти это, Executor может быть настроен на выполнение "n" длительных задач (n - количество потоков в Executor). Каждая из этих задач выводит команды отправки из очереди и выполняет их. Каждый Потребитель соединен с одной диспетчерской очередью команд, вероятно, назначенной по принципу "круговой". Это не слишком сложно и обеспечит простую балансировку нагрузки отправки по потокам в Исполнителе.

Теперь все еще есть проблемы:

  • Количество потоков в Исполнителе необязательно фиксировано (как в ThreadPoolExecutor).
  • В Executor или ExecutorService нет способа узнать, сколько потоков существует. Таким образом, мы не можем знать, сколько команд команд отправки нужно создать.

Однако мы можем, конечно, ввести ConnectionFactory.setDispatchThreadCount(int). За кулисами это создаст Executors.newFixedThreadPool() и правильное количество очередей отправки и задачи отправки.

Мне интересно услышать, если кто-то думает, что я пропустил какой-то более простой способ решения этого вопроса, и действительно, если это даже стоит решить.