web-dev-qa-db-ja.com

マルチプロセッシングを備えたCelery並列分散タスク

CPUを集中的に使用する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を使用してこのタスクを分散する前の方法クラウド内の利用可能なすべてのマシンですべてのコンピューティングCPUパワーを利用して分割するタスクですか?

62
Prometheus

あなたの目標は:

  1. 作業を多くのマシンに分散します(分散コンピューティング/分散並列処理)
  2. 特定のマシンでの作業をすべてのCPUに分散します(マルチプロセッシング/スレッド)

セロリはあなたのためにこれらの両方をかなり簡単に行うことができます。最初に理解することは、各セロリワーカーが デフォルトで構成 であり、システムで使用可能なCPUコアと同じ数のタスクを実行することです。

同時実行性とは、タスクを同時に処理するために使用されるpreforkワーカープロセスの数です。これらのすべてが仕事で忙しい場合、新しいタスクは、タスクの1つが完了するまで待つ必要があります。

デフォルトの同時実行数は、そのマシンのCPU数(コアを含む)です。-cオプションを使用してカスタム数を指定できます。最適な数はいくつかの要因に依存するため、推奨値はありませんが、タスクの大部分がI/Oバウンドである場合は、それを増やすことができますが、2倍以上のCPUを追加することはほとんどありません効果的で、代わりにパフォーマンスを低下させる可能性があります。

つまり、個々のタスクは、マルチプロセッシング/スレッドを使用して複数のCPU /コアを使用することを心配する必要はありません。代わりに、セロリは、使用可能な各CPUを使用するのに十分なタスクを同時に実行します。

邪魔にならないように、次のステップは_list_of_millions_of_ids_のサブセットの処理を処理するタスクを作成することです。ここにはいくつかのオプションがあります-1つは各タスクに単一のIDを処理させることで、Nタスクを実行します(N == len(list_of_millions_of_ids))。これにより、作業がすべてのタスクに均等に分散されることが保証されます。なぜなら、1人のワーカーが早く終了し、ただ待機している場合は決してないからです。作業が必要な場合は、キューからIDを取得できます。セロリgroupを使用してこれを行うことができます(John Doeによると)。

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()
_

別のオプションは、リストをより小さな断片に分割し、その断片を労働者に配布することです。このアプローチでは、一部の作業者がまだ作業を行っている間に一部の作業者が待たされる可能性があるため、いくつかのサイクルを無駄にするリスクがあります。ただし、 celery documentation notes この懸念はしばしば根拠のないものです:

タスクをチャンクすると並列性が低下することを心配する人もいますが、これはビジーなクラスターにはほとんど当てはまらず、メッセージングのオーバーヘッドを回避しているため、パフォーマンスが大幅に向上する可能性があります。

そのため、メッセージングのオーバーヘッドが減少するため、リストをチャンク化し、各タスクにチャンクを分散する方がパフォーマンスが向上する場合があります。一度に1つのIDを実行するのではなく、各IDを計算してリストに保存し、完了したらリスト全体をDBに追加することで、おそらくデータベースの負荷を少し軽くすることもできます。チャンキングアプローチは次のようになります

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()
_

チャンクサイズが最良の結果をもたらすものを少し試してみることができます。メッセージングのオーバーヘッドを削減しつつ、他のワーカーよりもはるかに速くワーカーがチャンクを終了し、何もすることなく待機するだけのサイズにならないように、サイズを小さく維持するスイートスポットを見つけます。

108
dano

配布の世界では、何よりも覚えておくべきことが1つだけあります。

早すぎる最適化は、すべての悪の根源です。 D.クヌース

明らかなように聞こえますが、ダブルチェックを配布する前に、最適なアルゴリズムを使用しています(存在する場合)。そうは言っても、配信の最適化は3つのことの間のバランスをとる行為です。

  1. 永続メディアからのデータの書き込み/読み取り、
  2. メディアAからメディアBへのデータの移動、
  3. データの処理、

コンピューターは、処理ユニットに近づくほど(3)より速く、より効率的に(1)および(2)になるように作られています。クラシッククラスターの順序は次のとおりです。ネットワークハードドライブ、ローカルハードドライブ、RAM、内部処理ユニット領域...今日、プロセッサは、一般にコアと呼ばれる独立したハードウェア処理ユニットのアンサンブルと見なされるほど洗練されています。スレッド(2)を介したデータ(3)。コアが非常に高速であるため、1つのスレッドでデータを送信するときにコンピューターの電力の50%を使用し、コアに2つのスレッドがある場合は100%を使用することを想像してください。コアあたり2つのスレッドはハイパースレッディングと呼ばれ、OSはハイパースレッドコアあたり2つのCPUを認識します。

プロセッサでスレッドを管理することは、一般にマルチスレッドと呼ばれます。 OSからCPUを管理することは、一般にマルチプロセッシングと呼ばれます。クラスタ内の並行タスクの管理は、一般に並列プログラミングと呼ばれます。クラスター内の依存タスクの管理は、一般に分散プログラミングと呼ばれます。

だからあなたのボトルネックはどこですか?

  • In(1):永続化して、上位レベル(たとえば、ネットワークハードドライブが遅い場合は、ローカルハードドライブに最初に保存する場合など、処理ユニットに近い方)からストリーミングします。
  • (2):これは最も一般的なものです。配信に不要な通信パケットを回避するか、「オンザフライ」パケットを圧縮します(たとえば、HDが遅い場合は、「バッチ計算」メッセージのみを保存し、中間結果はRAMになります)。
  • In(3):完了です!あなたは自由にすべての処理能力を使用しています。

セロリはどうですか?

Celeryは分散プログラミング用のメッセージングフレームワークであり、通信用のブローカーモジュール(2)と永続性用のバックエンドモジュール(1)を使用します。つまり、設定を変更することで(可能な場合)ボトルネックを回避できます。ネットワーク上でのみ。最初にコードをプロファイリングして、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にはデフォルトを、セロリには花を、CPUにはtopを使用します。結果はバックエンドに保存されます。ネットワークのボトルネックの例としては、タスクキューが大きくなりすぎて実行が遅れる場合があります。ボトルネックがない場合は、モジュールまたはセロリの構成を変更してください。

11
tk.

これに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_processorsummarizeは、通常の関数ではなく、セロリのタスクである必要があります-そうでなければ、もちろん並列化されません(セロリのタスクでない場合、コードコンストラクターがそれを受け入れるかどうかもわかりません)。

9
LetMeSOThat4U

セロリ労働者を追加すると、タスクの実行が確実にスピードアップします。ただし、別のボトルネックがある可能性があります:データベース。同時挿入/更新を処理できることを確認してください。

あなたの質問に関して:EC2インスタンスに別のプロセスをcelerydとして割り当てることにより、セロリ労働者を追加しています。必要なワーカー数に応じて、さらにインスタンスを追加できます。

3