コネクションプール#
コネクションプール は、複数のコネクションをまとめて管理し、コネクションが必要なときに関数が利用できるようにするオブジェクトです。新しいコネクションを確立するための時間は相対的に長いため、コネクションをオープンにしておくことでレイテンシを削減できます。
このページでは、psycopg のコネクションプールの動作について、いくつかの基本的な概念を説明します。プールの操作に関する詳細は、ConnectionPool
オブジェクト API を参照してください。
注釈
コネクションプールオブジェクトはメインの psycopg
パッケージとは別のパッケージとして配布されています。psycopg_pool
パッケージを使えるようにするには、pip install "psycopg[pool]"
または pip
install psycopg_pool
を使ってください。詳しくは コネクションプールのインストール を参照してください。
プールのライフサイクル#
プールを使うシンプルな方法は、次のようにグローバルオブジェクトとして単一のプールのインスタンスを作り、プログラムの他の場所でこのオブジェクトを使い、他の関数・モジュール・スレッドがプールを使えるようにするという方法です。
# プログラム内の db.py モジュール
from psycopg_pool import ConnectionPool
pool = ConnectionPool(conninfo, **kwargs)
# プールは直後に接続を開始する
# 他のモジュール内
from .db import pool
def my_function():
with pool.connection() as conn:
conn.execute(...)
理想的には、プールの使用が完了したときに close()
を呼び出したいかもしれませんが、プログラムの最後で close()
の呼び出しに失敗することは、それほど悪いことではありません。おそらくいくつかの警告が stderr に出力されるだけでしょう。しかし、それがいい加減なことだと感じる場合は、atexit
モジュールを使用してプログラムの最後に close()
を呼び出すこともできます。
import 時にデータベースへの接続の開始を避け、アプリケーションの準備ができるのを待ちたい場合には、open=False
を使用してプールを作り、条件が正しいときに open()
と
close()
メソッドを呼び出せます。特定のフレームワークはプログラムの開始と停止したときにトリガされるコールバックを提供しています (たとえば、FastAPI の startup/shutdown イベント)。これらは、プール操作の開始と終了に最適です。
pool = ConnectionPool(conninfo, open=False, **kwargs)
@app.on_event("startup")
def open_pool():
pool.open()
@app.on_event("shutdown")
def close_pool():
pool.close()
単一のプールをグローバル変数をして作成することは必須ではありません。プログラムでは、2つ以上のプールを作成できます。これは、2つ以上のデータベースに接続したり、異なる種類のコネクション、たとえば readwrite と read-only のコネクションを別に提供する場合に便利です。また、プールはコンテクスト マネージャとして振る舞うため、コンテクスト ブロックに入るときと出るときに、必要に応じてオープンしたりクローズしたりします。
from psycopg_pool import ConnectionPool
with ConnectionPool(conninfo, **kwargs) as pool:
run_app(pool)
# ここでプールはクローズしている
プールがオープンすると、プールのバックグラウンドワーカーが要求された min_size
のコネクションを作成を開始し、コンストラクタ (または open()
メソッド) はすぐに返ります。これにより、ターゲットのデータベースが起動する前にプログラムを開始するための余裕が生まれます。しかし、アプリケーションが正しく設定されていなかったり、ネットワークがダウンしている場合、プログラムが起動できたとしても、コネクションをリクエストしているスレッドが connection()
のタイムアウトが切れた後にのみ PoolTimeout
とともに失敗するということです。この動作が望ましくない場合 (周囲の状況が正しくない場合には他の何かがプログラムを再起動するため、プログラムが激しく早くクラッシュしてほしい場合)、プールの作成後に wait()
メソッドを呼ぶか、open(wait=True)
を呼ぶ必要があります。これらのメソッドはプールがフルになるまでブロックするか、もしプールが割り当てられた時間内に ready にならなかった場合は PoolTimeout
例外を発生させます。
コネクションのライフサイクル#
プールのバックグラウンド ワーカーは、ConnectionPool
コンストラクタに渡された conninfo
、kwargs
、connection_class
のパラメータに従って、connection_class(conninfo,
**kwargs)
のように実行することでコネクションを作成します。コネクションが一度作成されると、もし与えられた場合には configure()
コールバックにも渡され、その後、コネクションはプールに入れられます (または、もし誰かがすでにドアをノックしているなら、コネクションをリクエストしているクライアントに渡されます)。
コネクションが期限切れになった場合 (max_lifetime
を超えた場合) や、壊れた状態でプールに戻された場合、check()
によってクローズしていることがわかった場合、プールはそのコネクションを破棄し、新しいコネクションの開始をバックグラウンドで試みます。
プールからコネクションを使用する#
プールは複数のスレッドや並行タスクからコネクションをリクエストするために使えます――ほとんど役に立たないでしょう! プール内で使用できるより多くのコネクションがリクエストされた場合、リクエストしているスレッドはキューに入れられ、別のクライアントが使用を完了したか、プールの拡大が許可されているので (max_size
> min_size
の場合) 新しいコネクションの準備ができる場合、利用可能になるとすぐにコネクションが提供されます。
プールの主な使用方法は、次のように Connection
またはそのサブクラスを返す connection()
コンテクストを使用してコネクションを取得することです。
with my_pool.connection() as conn:
conn.execute("what you want")
connection()
コンテクストは Connection
オブジェクトのコンテクストのように振る舞います。ブロックの終わりでもしオープンなトランザクションが存在する場合、トランザクションはコミットされるか、コンテクストが例外とともに終了した場合はロールバックされます。
ブロックの終わりで、コネクションはプールに返され、そのコネクションを取得したコードにはもう使用されません。もしプールのコンストラクタで reset()
関数が指定されていた場合、プールに返される前にコネクションで呼ばれます。reset()
関数は、コネクションを使用したスレッドが遅くならならずに実行を続けられるようにするため、ワーカースレッド内で呼ばれることに注意してください。
プールのコネクションとサイズ#
プールは固定サイズにすることも (max_size
を設定しない、または max_size
= min_size
に設定する)、動的なサイズにすることもできます (max_size
> min_size
に設定する)。いずれの場合でも、プールが作成されるとすぐに、バックグラウンドで min_size
のコネクションの獲得を試行します。
コネクション作成の試行が失敗した場合、新しい試行が直後に行われます。この時、試行の時間間隔は、指数的バックオフ (exponential backoff) を使用して、reconnect_timeout
の最大値に到達するまで増やされます。最大値に到達してしまった場合、もし提供されていればプールが reconnect_failed()
関数を呼び出し、そのまま新しいコネクションの試行が始まります。この関数はアラートの送信やプログラムの中断のために使用することができ、これにより残りのインフラストラクチャが再起動できるようになります。
min_size
より大きな数のコネクションが並行してリクエストされた場合、新しいコネクションは最大 max_size
まで作られます。コネクションは常にバックグラウンド ワーカーによって作らるのであっれ、コネクションをリクエストしているスレッドで作られるわけではないことに注意してください。もしクライアントが新しいコネクションをリクエストしたら、1つ前のクライアントは新しいコネクションの準備ができる前にジョブを終了します。この動作は、コネクションを使用する時間に対して、コネクションを確立するための時間のほうが支配的なシナリオで特に役に立ちます (たとえば、この分析 を参照してください)。
プールが min_size
を超えて大きくなっても、その後その使用量が減少した場合には、最終的には多数のコネクションが閉じられます。つまり、プール コンストラクターで指定された max_idle
時間が経過した後にコネクションが未使用であれば、そのたびに1つずつコネクションが閉じられます。
プールの正しいサイズは何ですか?#
巨大な質問です。誰にもわからないでしょう。しかし、おそらく想像するほど大きくはありません。何かアイデアを得るためには この分析 を見てください。
何か役に立つことができるとしたら、おそらく get_stats()
メソッドを使ってプログラムの動作をモニタリング
し、設定のパラメータをチューニングすることでしょう。プールのサイズは resize()
メソッドを使えばランタイム時にも変更できます。
Null コネクション プール#
バージョン 3.1 で追加.
ときには、コネクションプールを使うか使わないかの選択を、アプリケーションの設定パラメータに委ねたいことがあります。たとえば、アプリケーションの「大きなインスタンス」をデプロイするときにはプールを使い、アプリケーションを複数のコネクションに割り当てられるようにしたくなるかもしれません。逆に、アプリケーションをロードバランサーの背後の複数のインスタンスにデプロイする場合や、PgBouncer のような外部のコネクション プール プロセスを使用する場合には、プールを使用しなくないかもしれません。
ConnectionPool
の API は通常の connect()
関数と異なるため、また、プールは追加のコネクション設定 (configure
パラメータ内で) を実行でき、プールが削除された場合には一部でアプリケーションの異なるコードパスを実行する必要があるため、プールの使用と未使用を切り替えるには何らかのコード変更が必要になります。
psycopg_pool
3.1 パッケージは新たに NullConnectionPool
クラスを導入しています。このクラスは ConnectionPool
と同じインターフェイスを持ち、そしてほとんど同じ動作をしますが、事前にコネクションを1つも作成しません。コネクションが返されると、他のクライアントがすでに待機していない限り、そのコネクションは直ちにクローズされ、プールの中に入れられた状態のままにはなりません。
null プールは、設定の利便性のためだけではなく、クライアントプログラムによるサーバーへのサクセスを制限するためにも利用できます。max_size
が 0 より大きい値に設定された場合、プールは最大 max_size
のコネクションが作成されることを常に保証します。クライアントがさらにコネクションを要求した場合には、通常のプールと同じように、クライアントはキューに入れられ、前のクライアントがコネクションを使い終わったらすぐにコネクションが与えられます。クライアントのリクエストをスロットルする他の仕組み (timeout
や max_waiting
など) も尊重されます。
注釈
キューに入れられたクライアントは、前のクライアントがコネクションの使用を完了したら (そして、プールがコネクションをアイドル状態に戻し、必要な場合にはコネクションで reset()
を呼んだら) すぐに、すでに確立されたコネクションで処理されます。
通常 (つまり、キューに入れられていない限り)、すべてのクライアントには新しいコネクションが与えられるため、コネクションを獲得する時間はクライアントの待機によってすでに償却されており、普通はバックグラウンド ワーカーは新しいコネクションの取得には関与しません。
コネクションの品質#
コネクションの状態は、コネクションがプールに返ってきたときに検証されます。コネクションが使用中に壊れた場合は、変換時に破棄されて新しいコネクションが作られます。
警告
プールがクライアントにコネクションを渡すときには、コネクションの状態は確認されません。
なぜ確認しないのでしょうか? なぜなら、確認するには追加のネットワークラウンドトリップが必要になってしまうからです。そのレイテンシから救いたいのです。レイテンシに怒りが湧いてしまう前に、プログラムがコネクションを使用している間、いつでもコネクションが失われる可能性があると考えてください。プログラムはすでに処理中のコネクションの喪失に対処できるはずなので、コネクションが壊れても耐えられるはずです。喜ばしくないことですが、世界の終わりではありません。
警告
コネクションがプール内にあるときには、コネクションの状態は確認されません。
プールは、プール内のコネクションの品質に常に目を光らせ続けるのでしょうか? いいえ、そうではありません。なぜでしょうか? なぜなら、あなたが代わりに確認してくれるからです! あなたのプログラムが、コネクションがまだ生きていることを確認するための手段として利用されるのです……。(Your program is only a big ruse to make sure the connections are still alive...)
(完全な) 冗談ではありません。コネクションプールを使用している場合は、コネクションの使用と返却がよいペースで行われることが想定されています。仮にプログラムが気づくより前に、プールが壊れたコネクションの品質をチェックする必要があったとしたら、プログラムが使用するよりもさらに早く、各コネクションをポーリングしなければなくなってしまいます。もしそのようなことをしたら、データベース サーバーは喜ばないでしょう……。
何かもっとよいことはできないのでしょうか? もちろんできます。ポーリングよりよい方法は常にあります。コネクションの切断の検知 と同じレシピが使えるのです。つまり、コネクションを予約して、スレッドを使用してスレッド上のアクティビティをモニタリングするという方法です。もしアクティビティが検出されれば、プールの check()
メソッドが呼べます。このメソッドは、プール内の各コネクションに素早いチェックを実行し、壊れた状態であることがわかったコネクションを削除し、バックグラウンド ワーカーを使用して新しいコネクションと置換します。
データベース コネクションが一時的に失われる場合に備えて、プログラム内で同様のチェックをセットアップした場合は、プールからすでにコネクションを取り出したスレッドに対してできることは何もありませんが、他のスレッドに壊れたコネクションが与えられることもないはずです。なぜなら、check()
がプールを空にして、コネクションが利用できるようになったらすぐに、機能するコネクションでプールを補填するためです。
「ポール」と口で言うよりも早いでしょう。あるいは「プール」と。
プールと idle_session_timeout
設定#
コネクションプールの使用は、コネクション上に idle_session_timeout を設定することと根本的に相容れません。プールはまさにコネクションを idle にしておき、素早く利用可能にするようにデザインされているためです。
現在の実装は idle_session_timeout
を考慮していません。そのため、もしこの設定が使われたら、クライアントは壊れたコネクションを提供される可能性があり、terminating connection due to idle-session timeout (idle セッション タイムアウトが原因でコネクションが終了している) のようなエラーで失敗するかもしれません。
この問題を避けるためには、プール コネクションに対して idle_session_timeout
を無効化してください。たとえサーバーが idle_session_timeout
のデフォルトを 0 以外に設定したとしても、たとえば次のように options
キーワード引数を使用することで、タイムアウトなしにプール コネクションを獲得できることに注意してください。
p = ConnectionPool(conninfo, kwargs={"options": "-c idle_session_timeout=0"})
警告
現在 max_idle
パラメータは、未使用のコネクションが存在した場合にプールを縮小するためにのみ使われています。コネクションをクローズするように設定されたサーバーと戦うためには設計されていません。
プールの統計#
プールで get_stats()
または pop_stats()
メソッドを使うと、プール自身の使用に関する情報を返せます。どちらのメソッドも同じ値を返しますが、後者は使用後にカウンターをリセットします。値は Graphite や Prometheus などのモニタリングシステムに送信できます。
以下の値が提供されるはずですが、厳格なインターフェイスだとは考えないでください。将来、変更される可能性があります。値が 0 のキーは返されないことがあります。
メトリック |
意味 |
---|---|
|
|
|
|
|
現在プールで管理されているコネクションの数 (プール内の数, クライアントに与えられた数, 準備中の数) |
|
プール内で現在アイドル状態のコネクションの数 |
|
コネクションを受け取るために現在キューの中で待機しているリクエスト数 |
|
プール外でコネクションの合計使用時間 |
|
プールにリクエストされたコネクションの数 |
|
コネクションがプールで直ちに利用可能ではなかったためにキューに入れられたリクエスト数 |
|
キューの中でのクライアントの合計待機時間 |
|
結果がエラーになったコネクションのリクエスト数 (タイムアウト, キューがフル...) |
|
悪い状態でプールに返却されたコネクションの数 |
|
プールからサーバーに試みられたコネクションの数 |
|
サーバーとコネクションを確立するために消費された合計時間 |
|
失敗したコネクションの試行数 |
|
|