web-dev-qa-db-ja.com

マルチプロセッシング/ psycopg2 TypeError:_thread.RLockオブジェクトをピクルできません

Postgresデータベースに並列選択クエリを実装するために、以下のコードに従いました。

https://tech.geoblink.com/2017/07/06/parallelizing-queries-in-postgresql-with-python/

私の基本的な問題は、実行する必要のあるクエリが最大6kあることであり、これらの選択クエリの実行を最適化しようとしています。当初は、where id in (...)に6kの述語IDがすべて含まれている単一のクエリでしたが、実行したマシンで4GBを超えるRAMを使用して、クエリで問題が発生しました。そこで、6kの個別クエリに分割して、同期して安定したメモリ使用量を維持することにしました。ただし、時間的に実行するのに時間がかかるため、私のユースケースではそれほど問題にはなりません。それでも、削減しようとしています。可能な限りの時間。

これは私のコードがどのように見えるかです:

class PostgresConnector(object):
    def __init__(self, db_url):
        self.db_url = db_url
        self.engine = self.init_connection()
        self.pool = self.init_pool()

    def init_pool(self):
        CPUS = multiprocessing.cpu_count()
        return multiprocessing.Pool(CPUS)

    def init_connection(self):
        LOGGER.info('Creating Postgres engine')
        return create_engine(self.db_url)

    def run_parallel_queries(self, queries):
        results = []
        try:
            for i in self.pool.imap_unordered(self.execute_parallel_query, queries):
                results.append(i)
        except Exception as exception:
            LOGGER.error('Error whilst executing %s queries in parallel: %s', len(queries), exception)
            raise
        finally:
            self.pool.close()
            self.pool.join()

        LOGGER.info('Parallel query ran producing %s sets of results of type: %s', len(results), type(results))

        return list(chain.from_iterable(results))

    def execute_parallel_query(self, query):
        con = psycopg2.connect(self.db_url)
        cur = con.cursor()
        cur.execute(query)
        records = cur.fetchall()
        con.close()

        return list(records)

ただし、これを実行すると、次のエラーが発生します。

TypeError: can't pickle _thread.RLock objects

マルチプロセッシングやピクルス可能なオブジェクトの使用に関して同様の質問をたくさん読んだことがありますが、自分が何を間違っているのか理解できません。

プールは通常、プロセスごとに1つですが(これがベストプラクティスだと思います)、コネクタクラスのインスタンスごとに共有されるため、parallel_queryメソッドを使用するたびにプールが作成されることはありません。

同様の質問に対する一番の答え:

Pythonマルチプロセッシング からMySQL接続プールにアクセスする

Postgresの代わりにMySqlを使用することを除いて、私自身とほぼ同じ実装を示しています。

私は何か間違ったことをしていますか?

ありがとう!

編集:

私はこの答えを見つけました:

Python Postgres psycopg2 ThreadedConnectionPoolが使い果たされました

これは非常に詳細で、multiprocessing.PoolThreadedConnectionPoolなどの接続プールの違いを誤解しているように見えます。ただし、最初のリンクでは、接続プールなどが必要であるとは言及されていません。このソリューションは良いように見えますが、かなり単純な問題だと思うコードがたくさんあるようです。

編集2:

したがって、上記のリンクは別の問題を解決します。とにかく遭遇する可能性が高いので、それを見つけてうれしいですが、ピクルスエラーまでimap_unorderedを使用できないという最初の問題は解決しません。非常にイライラします。

最後に、これがHerokuで、ワーカーdynoで実行され、スケジューリング、バックグラウンドタスクなどにRedis rqを使用し、データベースとしてPostgresのホストされたインスタンスを使用することはおそらく注目に値すると思います。

9
JustinMoser

簡単に言うと、postgres接続とsqlalchemy接続プールはスレッドセーフですが、フォークセーフではありません。

マルチプロセッシングを使用する場合は、フォーク後の各子プロセスでエンジンを初期化する必要があります。

エンジンを共有する場合は、代わりにマルチスレッドを使用する必要があります。

psycopg2ドキュメントのスレッドとプロセスの安全性 を参照してください:

libpq接続は、フォークされたプロセスでは使用しないでください。したがって、マルチプロセッシングなどのモジュールまたはFastCGIなどのフォークWebデプロイ方法を使用する場合は、フォークの後に接続を作成してください。

Multiprocessing.Poolを使用している場合、各子プロセスでコードを1回実行するために使用できるキーワード引数初期化子があります。これを試して:

class PostgresConnector(object):
    def __init__(self, db_url):
        self.db_url = db_url
        self.pool = self.init_pool()

    def init_pool(self):
        CPUS = multiprocessing.cpu_count()
        return multiprocessing.Pool(CPUS, initializer=self.init_connection(self.db_url))

    @classmethod
    def init_connection(cls, db_url):
        def _init_connection():
            LOGGER.info('Creating Postgres engine')
            cls.engine = create_engine(db_url)
        return _init_connection

    def run_parallel_queries(self, queries):
        results = []
        try:
            for i in self.pool.imap_unordered(self.execute_parallel_query, queries):
                results.append(i)
        except Exception as exception:
            LOGGER.error('Error whilst executing %s queries in parallel: %s', len(queries), exception)
            raise
        finally:
            pass
            #self.pool.close()
            #self.pool.join()

        LOGGER.info('Parallel query ran producing %s sets of results of type: %s', len(results), type(results))

        return list(chain.from_iterable(results))

    def execute_parallel_query(self, query):
        with self.engine.connect() as conn:
            with conn.begin():
                result = conn.execute(query)
                return result.fetchall()

    def __getstate__(self):
        # this is a hack, if you want to remove this method, you should
        # remove self.pool and just pass pool explicitly
        self_dict = self.__dict__.copy()
        del self_dict['pool']
        return self_dict

ここで、XY問題に対処します。

最初は、(...)のwhere idに6kの述語IDがすべて含まれている単一のクエリでしたが、実行したマシンで4GBを超えるRAM)を使用してクエリで問題が発生しました、それで私はそれを6kの個別のクエリに分割することに決めました。これは、同期すると安定したメモリ使用量を維持します。

代わりに実行したいのは、次のいずれかのオプションです。

  1. すべての6000IDを生成するサブクエリを作成し、元のバルククエリでサブクエリを使用します。
  2. 上記と同じですが、サブクエリを [〜#〜] cte [〜#〜] と記述します。
  3. iDリストが外部ソースからのものである場合(つまり、データベースからのものではない場合)、6000 IDを含む一時テーブルを作成してから、一時テーブルに対して元の一括クエリを実行できます。

ただし、Pythonを介して6000 IDを実行することを主張する場合、最速のクエリは、一度にすべての6000 IDを実行する(メモリが不足する)ことも、6000の個別のクエリを実行することもありません。代わりに、クエリをチャンク化してみてください。たとえば、一度に500個のIDを送信します。チャンクサイズを試して、メモリバジェット内で快適に一度に送信できるIDの最大数を決定する必要があります。

4
Lie Ryan