Паралельно розподілене завдання селери з багатопроцесорною обробкою


80

У мене завдання процесора з процесором Celery. Я хотів би використати всю обчислювальну потужність (ядра) для багатьох екземплярів EC2, щоб швидше виконати цю роботу ( я думаю, паралельно розподілене завдання селери з багатопроцесорною обробкою ) .

Терміни, потокові роботи , багатопроцесорність , розподілені обчислення , розподілена паралельна обробка - це всі терміни, які я намагаюся зрозуміти краще.

Приклад завдання:

  @app.task
  for item in list_of_millions_of_ids:
      id = item # do some long complicated equation here very CPU heavy!!!!!!! 
      database.objects(newid=id).save()

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


Я думав, MapReduce розроблено для вашого типу додатків: console.aws.amazon.com/elasticmapreduce/vnext/… :
AStopher

Відповіді:


120

Ваші цілі:

  1. Поширюйте свою роботу на багатьох машинах (розподілені обчислення / розподілена паралельна обробка)
  2. Розподіліть роботу на певній машині між усіма процесорами (багатопроцесорні / потокові)

Обидва ці селери можна зробити для вас досить легко. Перше, що слід зрозуміти, це те, що кожен працівник селери за замовчуванням налаштований на те, щоб виконувати стільки завдань, скільки ядер центрального процесора доступно в системі:

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

Номер паралельності за замовчуванням - це кількість процесорів на цій машині (включаючи ядра) , ви можете вказати власний номер, використовуючи опцію -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 #long complicated equation here
    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 #long complicated equation here
        database.objects(newid=id).save() # Still adding one id at a time, but you don't have to.

І для початку завдань:

from tasks import process_ids

jobs = process_ids.chunks(list_of_millions_of_ids, 30) # break the list into 30 chunks. Experiment with what number works best here.
jobs.apply_async()

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


Отже, частина, в якій я виконую "складне завдання з важким процесором (можливо, 3D-рендерінг)", буде автоматично розподілена паралельно обробленою, тобто 1 завдання буде використовувати стільки обробної потужності, скільки доступно для всіх екземплярів - і все це поза -коробка? справді? Ого. PS гарна відповідь спасибі, що мені це краще пояснили.
Прометей

3
@Spike Не зовсім. Як написано в даний час, завдання можуть використовувати лише одне ядро. Щоб індивідуальне завдання використовувало більше одного ядра, ми б ввели threadingабо multiprocessing. Замість цього ми маємо, щоб кожен працівник селери породив стільки завдань, скільки ядер на машині (це відбувається за замовчуванням у селери). Це означає, що у всьому вашому кластері кожне ядро ​​може бути використано для обробки вашого list_of_million_ids, використовуючи кожне завдання з одним ядром. Тому замість того, щоб одне завдання використовувало багато ядер, ми маємо багато завдань, кожне з яких використовує одне ядро. Чи має це сенс?
dano

1
Msgstr "Щоб індивідуальне завдання використовувало більше одного ядра, ми повинні ввести threadingабо multiprocessing". Припускаючи, що ми не можемо розділити цю важку задачу на кілька, як би ви використали різьбу або багатопроцесорну обробку, щоб селера розділила завдання між кількома екземплярами? подяка
Трістан

@Tristan Це залежить від того, що завдання насправді робить. Однак у більшості випадків я б сказав, що якщо ви не можете розділити саме завдання на підзадачі, вам, мабуть, буде важко, multiprocessingщоб розділити роботу всередині самого завдання, оскільки обидва підходи в кінцевому підсумку вимагають виконання те саме: розподіл завдання на менші завдання, які можна виконувати паралельно. Ви насправді змінюєте лише точку, в якій ви робите розкол.
дано 03.03.15

1
@PirateApp Це питання говорить про те, що ви не можете використовувати multiprocessing всередині завдання Celery . Сам Celery використовує billiard( multiprocessingвиделку) для запуску ваших завдань в окремих процесах. Вам просто не дозволено використовувати multiprocessingвсередині них.
dano

12

У світі розповсюдження є одне, про що слід пам’ятати перш за все:

Передчасна оптимізація - корінь усього зла. Д. Кнут

Я знаю, що це звучить очевидно, але перед розповсюдженням подвійної перевірки ви використовуєте найкращий алгоритм (якщо він існує ...). Сказавши це, оптимізація розподілу є рівновагою між 3 речами:

  1. Запис / зчитування даних з постійного носія,
  2. Переміщення даних із середовища А у середовище В,
  3. Обробка даних,

Комп’ютери виготовляються так, що чим ближче ви наближаєтесь до свого процесора (3), тим швидше та ефективніше (1) та (2). Порядок у класичному кластері буде такий: мережевий жорсткий диск, локальний жорсткий диск, оперативна пам’ять, усередині території процесорного блоку ... Сьогодні процесори стають досить досконалими, щоб їх можна було розглядати як сукупність незалежних апаратних блоків обробки, якими зазвичай називають ядра, ці ядра обробляють дані (3) через потоки (2). Уявіть, що ваше ядро ​​настільки швидке, що коли ви надсилаєте дані одним потоком, ви використовуєте 50% потужності комп’ютера, якщо ядро ​​має 2 потоки, ви будете використовувати 100%. Два потоки на ядро ​​називаються гіперпотоками, і ваша ОС бачитиме 2 процесори на ядро ​​гіперпотоків.

Управління потоками в процесорі зазвичай називають багатопотоковістю. Управління процесорами з ОС зазвичай називають багатопроцесорною. Керування паралельними завданнями в кластері зазвичай називають паралельним програмуванням. Управління залежними завданнями в кластері зазвичай називають розподіленим програмуванням.

То де твоє вузьке місце?

  • У (1): Спробуйте продовжувати та передавати потоки з верхнього рівня (того, що знаходиться ближче до вашого процесора, наприклад, якщо мережевий жорсткий диск спочатку працює повільно, збережіть на локальному жорсткому диску)
  • У (2): Це найпоширеніший, намагайтеся уникати комунікаційних пакетів, не потрібних для розповсюдження, або стискати пакети "на льоту" (наприклад, якщо HD працює повільно, збережіть лише повідомлення "пакетно обчислене" і зберігайте результати посередницької оперативної пам'яті).
  • У (3): Ви закінчили! Ви використовуєте всю потужність обробки, що є у вашому розпорядженні.

А як щодо Селери?

Селера - це система обміну повідомленнями для розподіленого програмування, яка використовуватиме модуль брокера для зв'язку (2) та модуль серверної системи для постійності (1), це означає, що ви зможете, змінивши конфігурацію, уникнути більшості вузьких місць (якщо можливо) на у вашій мережі та лише у вашій мережі. Спочатку сформулюйте свій код, щоб досягти найкращої продуктивності на одному комп'ютері. Потім використовуйте селеру у своєму кластері із конфігурацією за замовчуванням та встановіть CELERY_RESULT_PERSISTENT=True:

from celery import Celery

app = Celery('tasks', 
             broker='amqp://guest@localhost//',
             backend='redis://localhost')

@app.task
def process_id(all_the_data_parameters_needed_to_process_in_this_computer):
    #code that does stuff
    return result

Під час виконання відкрийте ваші улюблені інструменти моніторингу, я використовую за замовчуванням для rabbitMQ і квітка для селери та верхівку для cpus, ваші результати будуть збережені у вашому сервісі. Прикладом вузького місця в мережі є така велика черга завдань, що вони затримують виконання, ви можете приступати до зміни модулів або конфігурації селери, якщо ваше вузьке місце не в іншому місці.


9

Чому б не використовувати group для цього завдання із селери?

http://celery.readthedocs.org/en/latest/userguide/canvas.html#groups

В основному, ви повинні розділити idsна шматки (або діапазони) і дати їм купу завданьgroup .

Для більш складних, таких як збір результатів певних завдань із селери, я успішно використовував chord завдання з подібною метою:

http://celery.readthedocs.org/en/latest/userguide/canvas.html#chords

Збільшувати settings.CELERYD_CONCURRENCY до розумного числа, і ви можете собі це дозволити, тоді ці працівники селери продовжуватимуть виконувати ваші завдання в групі або акорді, поки не закінчать.

Примітка: через помилку kombu минулому виникали проблеми з повторним використанням робітників для великої кількості завдань, я не знаю, чи виправлено це зараз. Можливо, це так, але якщо ні, зменшіть CELERYD_MAX_TASKS_PER_CHILD.

Приклад на основі спрощеного та модифікованого коду, який я запускаю:

@app.task
def do_matches():
    match_data = ...
    result = chord(single_batch_processor.s(m) for m in match_data)(summarize.s())

summarizeотримує результати всіх single_batch_processorзавдань. Кожне завдання виконується будь-яким працівником Celery, який kombuкоординує це.

Тепер я розумію: single_batch_processorІ summarizeТАКОЖ мають бути завдання із селери, а не звичайні функції - інакше, звичайно, це не буде розпаралелено (я навіть не впевнений, що конструктор акордів прийме його, якщо це не завдання із селери).


На моє розуміння, це розділило б завдання, але не використовувало паралельно розподілене завдання селери із багатопроцесорною обробкою. тобто просто використовувати всю безкоштовну потужність процесора на всіх хмарних машинах.
Прометей

Я не впевнений, чому це могло б статися - Селера працює так, як у вас є купа робітників, незалежно від того, де вони знаходяться, вони навіть можуть бути розташовані на іншій машині. Звичайно, вам потрібно мати більше одного працівника. chord.
LetMeSOThat4U

Це ДІЙСНО ПОГОРИЙ приклад коду. Завдання do_matchesбуде заблоковано очікуванням акорду. Це може призвести до часткового або повного глухого кута, оскільки багато / усі працівники можуть чекати підзавдання, жодне з яких не буде виконано (оскільки працівники чекають підзавдань замість того, щоб важко працювати).
Прісакарі Дмитро

@PrisacariDmitrii То яке тоді буде правильним рішенням?
LetMeSOThat4U

4

Додавання більше працівників селери, безсумнівно, прискорить виконання завдання. У вас може бути ще одне вузьке місце: база даних. Переконайтесь, що він може обробляти одночасні вставки / оновлення.

Щодо вашого запитання: Ви додаєте працівників селери, призначивши інший процес у своїх примірниках EC2 як celeryd. Залежно від того, скільки працівників вам потрібно, ви можете додати ще більше примірників.


> Додавання більше працівників селери, безумовно, прискорить виконання завдання. --- Робить це? Отже, ваша приказка, що селера розподілить це одне завдання серед усіх моїх примірників, і мені не доведеться його розрізати?
Прометей

Зачекайте сукунду. Я просто прочитав ваш код ще раз, і оскільки це лише одне завдання, це не допоможе. Ви можете запускати одне завдання на ідентифікатор (або фрагменти ідентифікаторів). Або ви дотримуєтеся порад Джона Доу в іншій відповіді. Тоді ви зможете нажитися на кількості працівників селери. І так, у цьому випадку вам не потрібно багато робити. Тільки переконайтеся, що працівники споживають однакові черги.
Торстен Енгельбрехт
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.