Недавно в приложении фоновых задач я столкнулся со сценарием, когда несколько машин потребляют одну и ту же очередь задач.Для ее решения необходимо ввести определенный механизм распределения задач.Поскольку подобные проблемы встречались ранее, вот несколько возможных идей. Мы общаемся и обсуждаем более разумные и эффективные решения.
задний план
Предположим, у нас есть кластер для обработки ряда разных задач, в это время нам нужно распределить задачи в определенной степени, чтобы каждая машина в кластере отвечала за часть задач.
Как правило, существуют следующие требования:
- Задача может быть выполнена не более одного раза (другими словами, она может быть назначена только одной машине).
- Нагрузку каждой машины в кластере можно балансировать при выполнении задач
В этом случае, как разработать план назначения задач?
Чтобы облегчить последующее расширение, сначала ограничим некоторые выражения:
-
Source
: используется для указания источника задачи -
Cluster
: используется для представления всего кластера -
Task
: используется для представления абстрактных задач -
Worker
: используется для представления конкретной единицы (например, физической машины), которая фактически выполняет задачу.
Отношения между четырьмя могут быть представлены на следующем рисунке:
Идея 1: Простое присваивание по модулю
Самое простое, но тоже очень эффективное решение, требует предварительного уведомления перед постановкой задачОпределить количество машин N, пронумеруйте каждую задачу (или используйте ее идентификатор напрямую) и номер (0,1,2...) для каждого экземпляра машины, на которой выполняется задача.
то есть по следующей формуле:
Worker = TaskId % Cluster.size()
Если задача не имеет идентификатора id, то задача может быть распределена случайными числами.Когда количество задач достаточно велико, можно гарантировать баланс распределения, то есть:
Worker = random.nextInt() % Cluster.size()
Преимущество простого распределения по модулю в том, что оно достаточно простое.Хотя эффект балансировки нагрузки относительно грубый, с его помощью можно быстро достичь желаемого эффекта, что более полезно при разгрузке ветвей для экстренных задач. Однако в долгосрочной перспективе необходимо поддерживать обновление в реальном времени и количество машин N, а при изменении количества машин внутри кластера могут возникать кратковременные несоответствия. чувствительны к этому, требуется дальнейшая оптимизация.
Идея 2: распределенное управление блокировкой
Чтобы достичь цели «каждая задача выполняется только одной машиной», можно рассмотреть механизм распределенной блокировки. Когда несколько рабочих процессов используют задачу, только первый рабочий, захвативший блокировку, может выполнить задачу.
Теоретически говоря, воркеры, которые каждый раз захватывают блокировку, являются случайными, поэтому балансировка нагрузки приблизительно достигается; исходя из зрелых зависимостей промежуточного программного обеспечения, реализовать распределенную блокировку несложно (можно использовать реализацию контроля параллелизма системы кэширования) , и не рассматривать проблему изменения количества машин.
Однако и у этого решения есть много недостатков.Во-первых, процесс конкуренции за блокировки будет потреблять ресурсы воркеров.Кроме того, из-за невозможности предсказать, какие воркеры могут конкурировать за блокировки задач, баланс нагрузки всего кластера не может быть гарантировано.
Я лично считаю, что эта схема подходит только для распределения задач с очень простым содержанием, большим количеством задач и очень высокой частотой выполнения одновременно (аналогично многопоточным сценариям кэширования чтения и записи).
Идея 3: централизованное планирование маршрутизации
Если вы хотите добиться более тонкой балансировки нагрузки, лучше всего настроить набор правил распределения задач в соответствии с состоянием кластера и характеристиками самой задачи, а затем реализовать планирование задач через центральный уровень маршрутизации, а именно:
- Источник отправляет задачи на маршрутизатор
- Роутер принимает решения по правилам и назначает Задание определенному Воркеру
- (Если задача должна вернуть результат) Router перенаправляет результат, возвращенный соответствующим Worker, в Source
Простое и выполнимое правило распределения состоит в том, чтобы рассчитать загрузку процессора, памяти и других нагрузок рабочего перед планированием, рассчитать вес и выбрать машину с наименьшей нагрузкой для выполнения задачи; кроме того, ее можно разделить в зависимости от сложности. самой задачи.
Самая большая проблема с этим решением заключается в том, что стоимость независимой реализации уровня маршрутизации относительно высока, и существует риск проблем с одной точкой (если уровень маршрутизации зависнет, планирование задач в целом будет полностью парализовано).
Идея 4: на основе очереди сообщений
Это аналогия решения распределенной базы данных, основанного на очередях сообщений, которые мы видели ранее (оригинальный), с надежным брокером мы можем легко построить модель производитель-потребитель.
Все задачи, созданные источником, будут помещены в очередь сообщений, а нижестоящие рабочие процессы будут получать задачи и выполнять (потреблять) их. Преимущество этого заключается в том, что блокировка уменьшается, а стратегия повторных попыток может быть настроена в соответствии с результатом выполнения Worker (если выполнение завершается неудачно, он снова помещается в очередь). Но полагаться только на Broker для распределения задач нельзя решить две проблемы, с которых мы начали, поэтому нам также необходимо:
-
Механизм для предотвращения повторного использования сообщений
Поскольку логика передачи большинства брокеров очереди сообщений заключается в том, чтобы «гарантировать, что сообщение будет доставлено хотя бы один раз», очень вероятно, что задача будет получена несколькими работниками. «Выполнить один раз», тогда вам может потребоваться ввести упомянутый механизм блокировки. выше, чтобы предотвратить повторное потребление.
Но если вы выберете NSQ в качестве брокера, вам не придется рассматривать этот вопрос. Характеристики NSQ гарантируют, что сообщение должно потребляться только одним потребителем по одному и тому же каналу.
-
распределение задач
После построения модели производитель-потребитель все еще сложно ответить «какая задача должна выполняться на каком воркере», то есть механизм распределения задач, который существенно зависит от случайности потребительского действия потребителя. контроль.
Один из них заключается в том, чтобы рассчитать отношение сопоставления в соответствии с требуемыми правилами, прежде чем поместить его в очередь, а затем пометить Задание. Наконец, Рабочий процесс можно настроить так, чтобы он вступал в силу только для Задания с определенной меткой или распределялся в соответствии с отметка Задания с разными Темами.
Вместо этого расчет выполняется при извлечении очереди.В этом случае может потребоваться поддерживать уровень маршрутизации ниже по течению для пересылки, что ощущается немного больше, чем выигрыш.
В большинстве практических ситуаций достаточно полагаться на собственный механизм распространения сообщений брокера.
Идея 5: Потоковое противодавление
Обратитесь к концепции противодавления в реактивном программировании. Измените процесс задачи отправки на стороне источника на задачу извлечения на стороне исполнителя и «сначала реверсируйте клиента», чтобы добиться контроля скорости потока и балансировки нагрузки.
Проще говоря, нам нужно, чтобы Worker (и, возможно, Cluster) мог оценить количество задач, которые он может выполнить дальше, в соответствии со своей ситуацией, и передать их обратно Источнику, а затем Источник создает Задание и передает его рабочему (или кластеру).
Представьте себе осуществимое решение, расценивайте Источник как Сервер, а Работник как Клиент, тогда формируется обратный режим C/S.
Поведение рабочей стороны заключается в постоянном повторении цикла «запрос на получение задачи -> запуск задачи -> запрос на получение задачи». Всякий раз, когда Worker оценивает себя в состоянии «ожидания», он отправляет запрос к Source, чтобы получить Task и запустить его.
Исходная сторона относительно проста, нужно только реализовать интерфейс, всякий раз, когда приходит запрос, он возвращает задачу и помечает задачу для использования.
Хотя эта идея может лучше гарантировать, что нагрузка каждой рабочей машины находится в контролируемом диапазоне, есть также несколько проблем.
Первая — это проблема скорости потока, потому что скорость потребления всей очереди задач полностью контролируется самим воркером в этом режиме, а состояние очереди задач (сколько задач еще нужно обработать, какие задачи более срочные ..) невидим для воркера, поэтому может привести к накоплению задач на стороне Source.
Во-вторых, проблема задержки планирования задач, потому что исходная сторона не может предсказать, когда придет следующий запрос работника, поэтому для любой отправленной задачи нет гарантии, когда она будет выполнена. Это не большая проблема для фоновых задач, но очень фатальная для задач переднего плана.
Для решения вышеуказанных двух проблем необходимо внедрить разумный механизм распределения задач на стороне источника.В крайних случаях может также потребоваться, чтобы сторона источника могла принудительно распределять задачи.