Python で MySQL とのコネクションプーリングを行う方法と、そのハマりどころについて。
mysql-connector-python の MySQLConnectionPool の問題
まず、MySQL 公式の Python クライアント mysql-connector-python で単純にコネクションを作成する場合は以下のように書ける。
import mysql.connector
cnx = mysql.connector.connect(host="...", user="...", password="...", database="...")
この mysql-connector-python には MySQLConnectionPool というクラスが用意されており、これを使うと簡単にコネクションプーリングができる。1
from mysql.connector.pooling import MySQLConnectionPool
cnxpool = MySQLConnectionPool(host="...", user="...", password="...", database="...", pool_size=10)
cnx = cnxpool.get_connection()
cnx.close() # 実際にはコネクションは切断されず、コネクションプールにリリースされる
めでたしめでたし・・・とはいかない。
試しに以下のコードを実行してみる。
from mysql.connector.pooling import MySQLConnectionPool
cnxpool = MySQLConnectionPool(host="...", user="...", password="...", database="...", pool_size=3)
cnxpool.get_connection()
cnxpool.get_connection()
cnxpool.get_connection()
cnxpool.get_connection() #=> PoolError: Failed getting connection; pool exhausted
MySQLConnectionPool はプールのコネクションが枯渇した場合、例外を投げる仕様となっている。
このシンプルな仕様は嫌いではないのだけど、実際に Web アプリケーションなどで利用する際は「他のコネクションがリリースされるまで待つ」ような挙動であってくれたほうが嬉しいケースが多い。そうでないと、リクエスト処理の並列数が少しでもプールサイズを上回っただけでエラーが発生してしまう。
SQLAlchemy の QueuePool を使った解決策
この問題は、SQLAlchemy の QueuePool を使うと解決できる。2
from sqlalchemy.pool import QueuePool
cnxpool = QueuePool(lambda: mysql.connector.connect(host="...", user="...", password="...", database="..."), pool_size=10)
cnx = cnxpool.connect()
QueuePool の第一引数は「DB API に対応したコネクションオブジェクト」を返す関数ならなんでもいいらしい。汎用的ですごい。
SQLAlchemy というモジュール自体は ORM だが、今回は ORM 部分を全く使わずに QueuePool という仕組みだけを利用している。