webpy+DBUtils是如何将数据库连接数占满的


webpy简单易用,而且非常轻量。这些特点很容易让人忽略它作为web服务器的主要目标,而把它当作一个小模块使用。
现在就把它当作小模块,只使用它操作数据库的api.

下面是个小程序,目的就是访问数据库,然后关闭连接。

import web

def main():
    db = web.database(port=5432, host='localhost', dbn='postgres', db='tax',
                      user='postgres', pw='postgres')
    rs = db.query("select count(*) as count from pg_stat_activity")
    print list(rs)[0].count
    db.ctx.db.close()

if __name__ == '__main__':
    main()

在正常情况下,上面程序表现很正常。不会有任何问题.

如果变成周期程序,并且安装了DBUtils模块

import time
import web
import psycopg2

def main():
    db = web.database(port=5432, host='localhost', dbn='postgres', db='tax',
                      user='postgres', pw='postgres')
    rs = db.query("select count(*) as count from pg_stat_activity")
    print list(rs)[0].count
    db.ctx.db.close()

if __name__ == '__main__':
    while True:
        main()
        time.sleep(1)

输出的count值不断增加,最终将连接数占满。

为什么close没有效果,数据库连接数不断增加?

第一个问题:close关闭的是什么? 在有DBUitls时,close关闭时的PooledDB.connection()产生的连接. 在无DBUtils时,close关闭的时psycopg2.connect()产生的连接.

第二个问题: 在有DBUtils时close为什么不会减少连接数? 写了段代码,来看看close是怎么回事

import psycopg2
from DBUtils import PooledDB

def main():
    #mincached,maxconnections限制连接池中的连接数
    pool = PooledDB.PooledDB(psycopg2, port=5432, host='localhost', dbname='postgres', database='tax',
                             user='postgres', password='postgres',
                             mincached=2, maxconnections=2)
    # 从连接池中取一个连接
    conn = pool.connection()
    cur = conn.cursor()
    cur.execute("select count(*) as count from pg_stat_activity")
    rs = cur.fetchone()
    print list(rs)
    cur.close()
    # 不会影响这个连接池的连接数量. pool.close()关闭连接池,会关闭所有连接.
    conn.close()

if __name__ == '__main__':
    while True:
        main()
        time.sleep(1)

发现连接数始终不是变,单个连接的close,不会影响到这个连接池。

试试将上面cur.close() conn.close()注释掉,会导致连接数增加吗?
答案也是不会. pool是局部变量,变量销毁,连接会自动关闭。

得到结论: * 单个连接关闭,不会影响连接池
* 连接池变量释放,或者程序停止,连接会自动关闭

可推断webpy中定有全局变量,不释放而导致连接增加.

顺着这个思路阅读web源码,发现web.ctx是个TreadedDict类型

class ThreadedDict(threadlocal):
    """
    Thread local storage.

        >>> d = ThreadedDict()
        >>> d.x = 1
        >>> d.x
        1
        >>> import threading
        >>> def f(): d.x = 2
        ...
        >>> t = threading.Thread(target=f)
        >>> t.start()
        >>> t.join()
        >>> d.x
        1
    """
    # 原因就在这里
    _instances = set()

    def __init__(self):
        ThreadedDict._instances.add(self)

    def __del__(self):
        ThreadedDict._instances.remove(self)

    def __hash__(self):
        return id(self)
    ....

原来它有个静态属性ThreadedDict._instances保存每个实例,而每个实例中又保存web.database产生的PooledDB对象.
所以pool一直不释放。

所以原因就是: web.database每次新建一个PooledDB对象,而对象又保存在全局变量TreadedDict._instance中.
由于不断新建连接池,又不释放连接池,最终导致连接数不断增加。

webpy如何解决这个问题? 使用polling参数,禁止连接池

db = web.database(port=5432, host='localhost', dbn='postgres', db='tax',
                  user='postgres', pw='postgres', pooling=False)

这上面得出的结论对oracle和其它类型的数据库同样有效.



上篇: 线程里的全局变量threading.local 下篇: webpy+DBUtils是如何将数据库连接数占满的