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.Pool
とThreadedConnectionPool
などの接続プールの違いを誤解しているように見えます。ただし、最初のリンクでは、接続プールなどが必要であるとは言及されていません。このソリューションは良いように見えますが、かなり単純な問題だと思うコードがたくさんあるようです。
編集2:
したがって、上記のリンクは別の問題を解決します。とにかく遭遇する可能性が高いので、それを見つけてうれしいですが、ピクルスエラーまでimap_unordered
を使用できないという最初の問題は解決しません。非常にイライラします。
最後に、これがHerokuで、ワーカーdynoで実行され、スケジューリング、バックグラウンドタスクなどにRedis rqを使用し、データベースとしてPostgresのホストされたインスタンスを使用することはおそらく注目に値すると思います。
簡単に言うと、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の個別のクエリに分割することに決めました。これは、同期すると安定したメモリ使用量を維持します。
代わりに実行したいのは、次のいずれかのオプションです。
ただし、Pythonを介して6000 IDを実行することを主張する場合、最速のクエリは、一度にすべての6000 IDを実行する(メモリが不足する)ことも、6000の個別のクエリを実行することもありません。代わりに、クエリをチャンク化してみてください。たとえば、一度に500個のIDを送信します。チャンクサイズを試して、メモリバジェット内で快適に一度に送信できるIDの最大数を決定する必要があります。