Web 開発のヒント: スレッド化された Python データベースのアクセスに antipool.py を使う

データベース接続を共有し、保持する

データベースは、多くの並行リクエストを適切に処理してくれます (そのように動作することは、ほとんどデータベースの定義にあるとおりです)。しかしアクティブなプロセス (スレッド化された、あるいはフォークされたプロセス) は、ほとんど必然的に、貴重なリソース、つまりデータベース接続を使い尽くします。Martin Blais の antiorm ライブラリーにある antipool.py モジュールは、接続のプールや再利用をプログラマーには見えないようにし、しかも RDBMS に依存することなくそれを行うという便利なものです。

David Mertz, Ph.D (mertz@gnosis.cx), Author, Gnosis Software, Inc.

Photo of David MertzDavid Mertz氏は多くの分野で活躍しています。ソフトウェア開発や、それについて著述もしています。その他、学術政策理念について分野を問わず、関係する雑誌に記事も書いています。かなり以前には、超限集合論、ロジック、モデル理論などを研究していました。その後、労働組合組織者として活動していました。そして、David Mertz氏自身は人生の半ばにもまだ達していないと思っているので、これから何かほかの仕事をするかもしれません。



2007年 8月 21日

はじめに

長期間実行する Webサーバー・アプリケーションは、特にトランザクション型のアプリケーションでは、非常にさまざまな目的でバックエンドの RDBMS を頻繁に利用します。実際私は、もっと多くの Web サービスが、実際に使用している以上にデータベースを使用するべきだと思います。しかし、RDBMS を構成する作業には必要以上の「負担」がかかると、開発者は通常感じているのです (しかしこれは別の記事で扱うべき話題です)。データベースを利用すると非常に多くの利点がもたらされますが、データベースへのアクセスには少なくとも 1 つのボトルネックがあります。それが接続です。

接続は、2 つの異なる面でボトルネックとなります。直接的には、ネットワーク上で接続をネゴシエートするために少しばかり帯域幅が必要であり、また各接続を割り当てるために RDBMS マシンのわずかな CPU リソースとメモリー・リソースを必要とします。これらのリソースは最小限ですが、無視できるほどではありません。しかしもっと重要なことは、RDBMS が提供できる接続の数は有限だということです。RDBMS のメインのクライアントが Web アプリケーションである状況では、接続数の制限がクライアントごとの制限なのか合計としての制限なのかは、現実的にはあまり違いがありません。1 つのデータベース・クライアント (Web サーバー) がデータベースの動作の大部分を占めるからです。

このヒントでは、優れたアダプターであるpsycopg2 を使いますが、DBAPI を使用する Python データベースも基本的には同じ動作をします。ここで 1 つ注意すべき点は、ORM (object-relational mapping) は、私の意見ではデータベース・アクセスで実際に起きていることの詳細をあまりにも隠しすぎ、人工的な制約を強制し、そして一般的にはプログラミングを楽にするよりも妨げになることの方がはるかに多いという点です。


プールを使わない場合

(コネクション・プーリングを使わない場合の) 理想的なケースでは、すべてのスレッドやプロセスは、その内部で Web サーバー・アプリケーションがデータベースへの接続を行い、即座に接続を確立してカーソルを作成したら、ある程度高速な操作をいくつか行い、操作をコミットあるいはロールバックし、そして接続を閉じます。すべてが順調にいけば、非常に簡単です。例えば、下記を見てください。

リスト 1. Web サーバーが RDBMS に書き込む
def AddData(foo, bar):
    "This function is generally called in its own thread"
    from psycopg import connect
    conn = connect("dbname=transact user=web host=server.mine")
    cur = conn.cursor()
    cur.execute("INSERT INTO userdata VALUES (%r, %r)" % (foo, bar))
    conn.commit()
    conn.close()

この例は、考えられるコードとして最高のものではありません。私は connect() をコールしている前後を try/'except' で囲むかもしれず、また conn.close() が必ずコールされるように try/finally で囲むかもしれないからです。また私は、いつもとは違って DBAPI の値のエスケープも使っていません。いずれにせよ、AddData() は典型的なデータベース・コールの「形」は持っています。


どんな不都合が起きるのか (そしてどのように修正するか)

上記のようなコードでは、いくつかの不都合が起こりがちです。1 つの問題は、多くのスレッドが一度に起動されると、その中のいくつかは、たとえデータベースの制限に達していなくても、実際には接続の確立に失敗することです。理論的には、この問題は起こるはずがありませんが、実際には発生します。しかしもう一方の問題は、もしデータベース・プールの制限に達した場合 (通常はスレッドの完了に予想よりも長い時間がかかる、あるいは接続を閉じる前にスレッドが例外を上げるため起こります) には、接続の確立に失敗したことは非常にわかりにくくなります。

ローカルのスレッド・プールを作成すると、両方の問題に対応することができます。コネクション・プールは単に事前割り当てされたデータベース接続の集合であり、各「仮想接続」スレッドによって再利用され、また解放されるものです。つまり、実際のデータベース接続は比較的稀にしか閉じられませんが、プールされた接続のプロキシーはトランザクションごとに取得され、解放されます。

一方では、事前割り当てされた接続をローカル・プールから引き出すことはメモリー内での操作であり、(接続を使い尽くしてしまう以外) ほとんど失敗する可能性はありません。ネットワークやデータベースの待ち時間の問題は発生しないからです。その一方、プールを使うことで、接続の制限を正確にローカルで管理することができます。もし接続を制限するためにデータベースに依存するとすると、接続がいくつ使用されており、いくつ利用できるのかをアプリケーション・レベルで直接判断する方法がありません。開いているスレッドの数で非常に大まかに概算することはできますが、それは当てにならない方法です。antipool.py モジュールは、このモジュールにプールされている (実際の) 接続をバックグラウンドで増減するため、実際の接続を必要以上に保持してしまうことを避けることができます。しかしユーザーにとっては、プールされた接続は真のデータベース接続と同じに見えます。異なる点は、プールされた接続では、利用できる接続数をローカルで知ることができることと、最大接続数がデータベースのパラメーターではなくコネクション・プールのパラメーターであることです。

antipool.py を使った場合も、さらにプーリング機能を薄くラップすると便利であり、毎回必ず ConnectionPool の全オプションを使わなくてもよいことに私は気が付きました。さらに、実際の接続制限に達した最も一般的なケースでも、最終的に新しい接続を取得するためには、通常は単純な sleep() で十分です。もし大量のスレッドが本当に不適切な動作をしている (決して閉じない) 場合は、引き続き問題は残りますが、少なくともデバッグが容易になります。下記は私が使用しているラッパーです。

リスト 2. webapp_pool.py
"""USAGE:
from webapp_pool import get_connection
conn, cur = get_connection()   # Might hang, but never raises
cur.execute(SQL)
conn.commit()
conn.release()    # 'conn.release()' not 'conn.close()'
"""
import psycopg2
from time import sleep
from antipool import ConnectionPool
from database import host, database, user, MAXCONNECTIONS
conn_pool = ConnectionPool(psycopg2,
                           host=host,
                           database=database,
                           user=user,
                           options={'maxconn':MAXCONNECTIONS})
def get_connection():
    got_connection = False
    while not got_connection:
        try:
            conn = conn_pool.connection()
            cur = conn.cursor()
            got_connection = True
        except psycopg2.OperationalError, mess:
            # Might log exception here
            sleep(1)
        except AttributeError, mess:
            # Might log exception here
            sleep(1)
    return conn, cur

まとめ

このヒントでは、データベースに依存しない antipool.py モジュールを使いました。私が使ってみたところ、antipool.py は非常に柔軟なことがわかりました。このモジュールは、この短いヒントでは紹介しなかった、よく考え抜かれたさらなる機能も持っています。一方で、psycopg2 などのツールに組み込まれたプーリング機能を使う場合には、コネクション・プーリングの使い方に関する一般的な原則は同じままで変わりません。

参考文献

学ぶために

  • Martin Blais による antiorm パッケージの全体を見てください。
  • IBM の DB2 RDBMS にも Python アダプターがあり、(ご想像の通り) PyDB2 という名前です。このモジュールにはコネクション・プールは含まれていませんが、antipool.py を使って完璧に動作するはずです。
  • IBM の最先端の DB2 RDBMS を扱いたい場合には、自由にダウンロードできるDB2/Viper のベータを必ずチェックしてください。
  • psycopg2 を入手するためには Trac ホームページを使ってください。必ず 2.x シリーズを使うようにし、推奨ではなくなった 1.x を使わないでください。
  • developerWorks の試用版ダウンロードの一覧を見てください。
  • Web 開発に関する developerWorks のニュースレターを購読してください。
  • Web development ゾーンの技術ライブラリーには、他にも実際的なハウ・ツー記事が豊富に用意されています。

議論するために

コメント

developerWorks: サイン・イン

必須フィールドは(*)で示されます。


IBM ID が必要ですか?
IBM IDをお忘れですか?


パスワードをお忘れですか?
パスワードの変更

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


お客様が developerWorks に初めてサインインすると、お客様のプロフィールが作成されます。会社名を非表示とする選択を行わない限り、プロフィール内の情報(名前、国/地域や会社名)は公開され、投稿するコンテンツと一緒に表示されますが、いつでもこれらの情報を更新できます。

送信されたすべての情報は安全です。

ディスプレイ・ネームを選択してください



developerWorks に初めてサインインするとプロフィールが作成されますので、その際にディスプレイ・ネームを選択する必要があります。ディスプレイ・ネームは、お客様が developerWorks に投稿するコンテンツと一緒に表示されます。

ディスプレイ・ネームは、3文字から31文字の範囲で指定し、かつ developerWorks コミュニティーでユニークである必要があります。また、プライバシー上の理由でお客様の電子メール・アドレスは使用しないでください。

必須フィールドは(*)で示されます。

3文字から31文字の範囲で指定し

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


送信されたすべての情報は安全です。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Web development
ArticleID=257970
ArticleTitle=Web 開発のヒント: スレッド化された Python データベースのアクセスに antipool.py を使う
publish-date=08212007