web-dev-qa-db-ja.com

Pythonビッグデータをマッピングするための共有メモリ辞書

Pythonでマルチプロセッシングを使用して大きなデータセット(2TB)を処理するために、大きな辞書(〜86GB、17.5億キー)を使用するのに苦労してきました。

コンテキスト:文字列を文字列にマッピングする辞書は、pickle化されたファイルからメモリにロードされます。ロードされると、ディクショナリ内の値をルックアップする必要があるが、コンテンツを変更しないワーカープロセス(理想的には> 32)が作成され、〜2TBのデータセットを処理します。データセットは並行して処理する必要があります。そうしないと、タスクに1か月以上かかります。

これが 6セブン8私が試した9つのアプローチ(すべて失敗):

  1. 辞書をグローバル変数としてPythonプログラムに格納してから、〜32個のワーカープロセスをフォークします。理論的には、辞書はnot変更されるため、LinuxでのforkのCOWメカニズムは、データ構造が共有され、プロセス間でコピーされないことを意味します。ただし、これを試みると、プログラムが内部のos.fork()でクラッシュします。 of multiprocessing.Pool.map from OSError: [Errno 12] Cannot allocate memory。これは、カーネルがメモリをオーバーコミットしないように構成されているためであると確信しています(/proc/sys/vm/overcommit_memory2に設定されており、構成できませんルートアクセス権がないため、マシン上のこの設定)。

  2. multiprocessing.Manager.dictを使用して辞書を共有メモリ辞書にロードします。このアプローチでは、クラッシュすることなく32ワーカープロセスをフォークできましたが、その後のデータ処理は、辞書を必要としない別のバージョンのタスクよりも桁違いに遅くなります(違いは辞書の検索がないことだけです)。これは、ディクショナリを含むマネージャプロセスと各ワーカープロセス間のプロセス間通信が原因であると理論付けています。これは、すべてのディクショナリルックアップに必要です。辞書は変更されていませんが、何度もアクセスされており、多くの場合、多くのプロセスによって同時にアクセスされています。

  3. 辞書をC++ std::mapにコピーし、LinuxのCOWメカニズムに依存してコピーされないようにします(C++の辞書を除くアプローチ#1と同様)。このアプローチでは、辞書をstd::mapにロードするのに長い時間がかかり、その後、以前と同じようにos.fork()ENOMEMからクラッシュしました。

  4. 辞書を pyshmht にコピーします。辞書をpyshmhtにコピーするには時間がかかりすぎます。

  5. SNAP のHashTableを使用してみてください。 C++の基礎となる実装では、共有メモリで作成および使用できます。残念ながら、Python APIはこの機能を提供していません。

  6. PyPyを使用します。クラッシュはまだ#1のように発生しました。

  7. multiprocessing.Arrayの上にpython)に独自の共有メモリハッシュテーブルを実装します。このアプローチでも、#1で発生したメモリ不足エラーが発生しました。

  8. 辞書を dbm にダンプします。辞書をdbmデータベースに4日間ダンプしようとし、「33日」のETAを確認した後、私はこのアプローチをあきらめました。

  9. 辞書をRedisにダンプします。 redis.msetを使用して辞書(86GBの辞書は1024個の小さい辞書からロードされます)をRedisにダンプしようとすると、ピアエラーによって接続がリセットされます。ループを使用してキーと値のペアをダンプしようとすると、非常に長い時間がかかります。

このディクショナリで値を検索するためにプロセス間通信を必要とせずに、このデータセットを効率的に並列処理するにはどうすればよいですか。この問題を解決するための提案を歓迎します!

1TBのRAMを搭載したマシンでUbuntuのAnacondaのPython 3.6.3を使用しています。


編集:最終的に機能したもの:

Redisを使用してこれを機能させることができました。 #9で発行された問題を回避するには、大きなKey-Value挿入クエリとルックアップクエリを「一口サイズ」のチャンクにチャンクして、バッチで処理できるようにする必要がありましたが、大きすぎるクエリでタイムアウトすることはありませんでした。 。これを行うことで、86GBディクショナリの挿入を45分で実行でき(128スレッドとある程度の負荷分散)、その後の処理はRedisルックアップクエリによってパフォーマンスが低下しませんでした(2日で終了)。

あなたの助けと提案をありがとうございました。

11
Jon Deaton

データベースのように、多くの異なるプロセスと大量のデータを共有することを目的としたシステムを使用する必要があります。

巨大なデータセットを取得し、そのスキーマを作成してデータベースにダンプします。別のマシンに置くこともできます。

次に、必要な数のホスト間で必要な数のプロセスを起動して、データを並列処理します。最新のデータベースのほとんどは、負荷を処理できる以上のものになります。

7
Brendan Abel

ポイント1でそのデータを単一のプロセスに正常にロードできる場合は、 https://bugs.python.org/で紹介されているgc.freezeを使用して、フォークがコピーを実行する問題を回避できる可能性があります。 issue31558

python 3.7+を使用し、フォークする前に(またはプロセスプールでマップを実行する前に)その関数を呼び出す必要があります。

これには、CoWが機能するためにメモリ全体の仮想コピーが必要なため、 設定のオーバーコミット でそれができることを確認する必要があります。

2
viraptor

データベースでそれを試してみて、Daskを使用して問題を解決してみてください。Daskに低レベルでのマルチプロセッシングの方法を気にかけてもらいましょう。その大きなデータを使用して、解決したい主な質問に集中できます。そして、これはあなたが見たいと思うかもしれないリンク Dask

2
ye jiawei

辞書を使用する代わりに、データを圧縮するが高速ルックアップを備えたデータ構造を使用します。

例えば:

  • keyvi: https://github.com/cliqz-oss/keyvi keyviは、スペースとルックアップ速度に最適化されたFSAベースのKey-Valueデータ構造です。 keyvi構造はメモリマップされており、共有メモリを使用するため、keyviから読み取る複数のプロセスはメモリを再利用します。ワーカープロセスはデータ構造を変更する必要がないので、これが最善の策だと思います。

  • marisa trie: https://github.com/pytries/marisa-trie marisa-trie C++ライブラリに基づくPythonの静的トライ構造。 keyviと同様に、marisa-trieもメモリマッピングを使用します。同じトライを使用する複数のプロセスは、同じメモリを使用します。

編集:

このタスクにkeyviを使用するには、最初にpip install pykeyviを使用してインストールできます。次に、次のように使用します。

from pykeyvi import StringDictionaryCompiler, Dictionary

# Create the dictionary
compiler = StringDictionaryCompiler()
compiler.Add('foo', 'bar')
compiler.Add('key', 'value')
compiler.Compile()
compiler.WriteToFile('test.keyvi')

# Use the dictionary
dct = Dictionary('test.keyvi')
dct['foo'].GetValue()
> 'bar'
dct['key'].GetValue()
> 'value'

marisa trieは単なるトライであるため、そのままではマッピングとして機能しませんが、たとえば、キーと値を区切る区切り文字を使用できます。

2
tomas

すでに述べたkeyvi( http://keyvi.org )は私にとって最良の選択肢のように思えます。なぜなら、「python共有メモリ辞書」はそれが何であるかを正確に説明しているからです。私はkeyviの作者であり、偏見があると呼んでいますが、説明する機会を与えてください。

共有メモリは、特にpython GILの問題により、スレッドではなくマルチプロセッシングを使用する必要がある場合に、スケーラブルになります。そのため、ヒープベースのインプロセスソリューションは拡張できません。共有メモリもメインメモリよりも大きくすることができ、パーツを交換することができます。

外部プロセスのネットワークベースのソリューションには、追加のネットワークホップが必要です。これは、keyviを使用することで回避できます。これにより、ローカルマシンでもパフォーマンスに大きな違いが生じます。問題は、外部プロセスがシングルスレッドであるため、ボトルネックが再び発生するかどうかでもあります。

あなたの辞書のサイズについて疑問に思います:86GB:keyviがそれをうまく圧縮する可能性は十分にありますが、データを知らずに言うのは難しいです。

処理に関して:keyviはpySpark/Hadoopでうまく機能することに注意してください。

ユースケースBTWは、大規模であっても、keyviが本番環境で使用されるものとまったく同じです。

Redisソリューションは、少なくとも一部のデータベースソリューションよりも優れているように聞こえます。コアを飽和させるには、いくつかのインスタンスを使用し、コンシステントハッシュを使用してキースペースを分割する必要があります。しかし、それでも、keyviを使用すると、拡張性が大幅に向上すると確信しています。タスクを繰り返す必要がある場合や、より多くのデータを処理する必要がある場合は、試してみてください。

大事なことを言い忘れましたが、あなたはウェブサイトで上記をより詳細に説明しているニースの資料を見つけます。

1
Hendrik Muhs

さて、Redisまたはデータベースが最も簡単で迅速な修正になると私は信じています。

しかし、私が理解したことから、2番目の解決策から問題を減らしてみませんか?つまり、最初に10億個のキーの一部をメモリにロードしようとします(たとえば5,000万個)。次に、マルチプロセッシングを使用して、2 TBファイルで作業するためのプールを作成します。行のルックアップがテーブルに存在する場合は、データを処理済み行のリストにプッシュします。存在しない場合存在しない場合は、リストにプッシュします。データセットの読み取りが完了したら、リストをピクルスにして、メモリから保存したキーをフラッシュします。次に、次の100万をロードし、リストから読み取る代わりにプロセスを繰り返します。完了したら完全に、すべてのピクルスオブジェクトを読んでください。

これはあなたが直面していた速度の問題を処理するはずです。もちろん、私はあなたのデータセットについてほとんど知識がなく、これが実現可能かどうかさえわかりません。もちろん、適切な辞書キーが読み取られなかった行が残る可能性がありますが、この時点でデータサイズは大幅に削減されます。

それが何か助けになるかどうかわからない。

1
Haris Nadeem

"データベースを使用する"の大多数の提案は賢明で証明されていますが、使用を避けたいと思われるかもしれません何らかの理由でデータベース(そしてデータベースへのロードが法外なものであることがわかっている)なので、本質的にはIOバウンドまたはプロセッサバウンド、あるいはその両方のようです。 1024個の小さいインデックスから86GBのインデックスをロードしているとのことです。キーが適度に規則的で、均等に分散されている場合、1024個の小さいインデックスに戻って辞書を分割することは可能ですか?つまり、たとえば、キーがすべて20文字の長さで、a〜zの文字で構成されている場合、26個の小さい辞書を作成します。1つは「a」で始まるすべてのキー用、もう1つは「b」で始まるキー用などです。この概念を、最初の2文字以上専用の多数の小さな辞書に拡張することができます。したがって、たとえば、「aa」で始まるキー用に1つの辞書、「ab」で始まるキー用に1つの辞書などをロードできるため、676個の個別の辞書が作成されます。同じロジックが、17,576個の小さい辞書を使用して、最初の3文字のパーティションに適用されます。基本的に、私がここで言っているのは、「最初に86GBの辞書をロードしないでください」だと思います。代わりに、データや負荷を自然に分散する戦略を使用してください。

0
Darren

読み取り専用辞書の作成のみを検討しているため、独自の単純なバージョンを作成することで、既成のデータベースよりも高速化できる可能性があります。おそらく、次のようなことを試すことができます。

import os.path
import functools
db_dir = '/path/to/my/dbdir'

def write(key, value):
    path = os.path.join(db_dir, key)
    with open(path, 'w') as f:
        f.write(value)

@functools.lru_cache(maxsize=None)
def read(key):
    path = os.path.join(db_dir, key)
    with open(path) as f:
        return f.read()

これにより、テキストファイルでいっぱいのフォルダが作成されます。各ファイルの名前は辞書キーであり、内容は値です。これを自分で計時すると、書き込みごとに約300usが得られます(ローカルSSDを使用)。これらの数値を使用すると、理論的には17.5億個の鍵を書き込むのにかかる時間は約1週間になりますが、これは簡単に並列化できるため、可能性がありますはるかに高速に実行できます。

読み取りの場合、ウォームキャッシュと5msのコールドキャッシュ(ここではOSファイルキャッシュを意味します)を使用すると、読み取りごとに約150usが得られます。アクセスパターンが繰り返される場合は、上記のようにlru_cacheを使用して処理中の読み取り関数をメモ化できます。

これだけ多くのファイルを1つのディレクトリに保存することは、ファイルシステムでは不可能であるか、OSにとって非効率的であることに気付くかもしれません。その場合は、.git/objectsフォルダーのように行うことができます。キーabcdをab/cdというファイル(つまり、フォルダーabのファイルcd)に保存します。

上記は、4KBのブロックサイズに基づいてディスク上で15TBのようなものを取ります。各ファイルが4KBのブロックサイズに近づくように、最初のn文字でキーをグループ化することにより、ディスク上およびOSキャッシュでより効率的にすることができます。これが機能する方法は、abcで始まるすべてのキーのキーと値のペアを格納するabcというファイルがあることです。最初に小さい辞書をそれぞれソートされたキー/値ファイルに出力し、次にデータベースに書き込むときにマージソートして、各ファイルを一度に1つずつ書き込む(繰り返し開いて追加するのではなく)と、これをより効率的に作成できます。 。

0
Oscar Benjamin

別の解決策は、必要に応じてページを割り当て/リタイアし、インデックスルックアップを迅速に処理できる既存のデータベースドライバを使用することです。

dbm 利用可能なNice辞書インターフェースがあり、ページの自動キャッシュを使用すると、ニーズに十分対応できる場合があります。何も変更されていない場合は、ファイル全体をVFSレベルで効果的にキャッシュできるはずです。

ロックを無効にし、同期されていないモードで開き、'r'でのみ開くことを忘れないでください。そうすれば、キャッシュ/同時アクセスに影響はありません。

0
viraptor