Ваши цели:
- Распределите свою работу на многих машинах (распределенные вычисления / распределенная параллельная обработка)
- Распределить работу на данной машине по всем ЦП (многопроцессорность / многопоточность)
Сельдерей может довольно легко сделать и то, и другое. Первое, что нужно понять, это то, что каждый работник сельдерея по умолчанию настроен на выполнение столько задач, сколько ядер ЦП доступно в системе:
Параллелизм - это количество рабочих процессов prefork, используемых для одновременной обработки ваших задач, когда все они заняты выполнением работы, новые задачи должны будут дождаться завершения одной из задач, прежде чем она сможет быть обработана.
Номер параллелизма по умолчанию - это количество процессоров на этом компьютере (включая ядра) , вы можете указать собственный номер, используя параметр -c. Рекомендуемого значения нет, так как оптимальное количество зависит от ряда факторов, но если ваши задачи в основном связаны с вводом-выводом, вы можете попытаться увеличить его, эксперименты показали, что добавление более чем в два раза количества ЦП редко бывает эффективно и, скорее всего, снизит производительность.
Это означает, что каждой отдельной задаче не нужно беспокоиться об использовании многопроцессорности / потоковой передачи для использования нескольких процессоров / ядер. Вместо этого сельдерей будет выполнять достаточно задач одновременно, чтобы использовать каждый доступный процессор.
После этого следующим шагом будет создание задачи, которая обрабатывает некоторую часть вашего list_of_millions_of_ids
. Здесь у вас есть несколько вариантов - один - чтобы каждая задача обрабатывала один идентификатор, поэтому вы запускаете N задач, где N == len(list_of_millions_of_ids)
. Это гарантирует, что работа будет равномерно распределена между всеми вашими задачами, поскольку никогда не будет случая, когда один работник закончит раньше и просто будет ждать; если ему нужна работа, он может вытащить идентификатор из очереди. Вы можете сделать это (как упоминал Джон Доу) с помощью сельдерея group
.
tasks.py:
@app.task
def process_id(item):
id = item
database.objects(newid=id).save()
И для выполнения поставленных задач:
from celery import group
from tasks import process_id
jobs = group(process_id.s(item) for item in list_of_millions_of_ids)
result = jobs.apply_async()
Другой вариант - разбить список на более мелкие части и раздать их своим работникам. Такой подход сопряжен с риском потери некоторых циклов, потому что в итоге некоторые работники могут ждать, пока другие все еще работают. Однако в документации на сельдерей отмечается, что это опасение часто необоснованно:
Некоторые могут беспокоиться, что разбиение ваших задач на части приведет к ухудшению параллелизма, но это редко бывает верно для загруженного кластера и на практике, поскольку вы избегаете накладных расходов на обмен сообщениями, это может значительно повысить производительность.
Таким образом, вы можете обнаружить, что разбиение списка на части и их распределение по каждой задаче работает лучше из-за уменьшения накладных расходов на обмен сообщениями. Вероятно, вы также можете немного облегчить нагрузку на базу данных, вычислив каждый идентификатор, сохранив его в списке, а затем добавив весь список в БД, как только вы закончите, вместо того, чтобы делать это по одному идентификатору за раз . Подход к фрагментам будет выглядеть примерно так
tasks.py:
@app.task
def process_ids(items):
for item in items:
id = item
database.objects(newid=id).save()
И для запуска задач:
from tasks import process_ids
jobs = process_ids.chunks(list_of_millions_of_ids, 30)
jobs.apply_async()
Вы можете немного поэкспериментировать с тем, какой размер фрагментов дает вам лучший результат. Вы хотите найти золотую середину, в которой вы сокращаете накладные расходы на обмен сообщениями, но при этом сохраняете достаточно маленький размер, чтобы работники не заканчивали свой кусок намного быстрее, чем другой работник, а затем просто ждал, ничего не делая.