让Django支持数据库长连接(可以提高不少性能哦)

书接上回 

上回我们说到:《在生产系统使用Tornado WebServer来代替FastCGI加速你的Django应用

那么现在很流行用一些高性能的nonblock的app server来host Django的应用,这些Server可以看做是一个单进程单线程的程序,然后用nginx在前端反向代理并且负载均衡到N多个后端工作进城来充分利用多CPU的性能,当然这部分的配置工作在上回已经说得很清楚了。但是对于Django来说有一个问题。因为Django的数据库连接是在查询的时候实时创建的,用完就会关掉,这样就会频繁的开闭连接。但是对于Tornado这种Server来说这种方式是低效的。这种Server最高效的工作模式是每个进程开启一个连接,并长期保持不关闭。本文的目的就是尝试使Django改变一贯的作风,采用这种高效的工作模式。本文基于Django1.3的版本,如果是低版本可以稍加更改一样可以使用。

Django的数据库可以通过配置使用专门定制的Backend,我们就从这里入手。

首先我们看看Django自带的Backend是如何实现的。在Django官网上可以看到自带MySql的Package结构,可以点击 此处 前往瞻仰。

通观源码我们可以发现,Django基本上是封装了MySQLdb的Connection和Cursor这两个对象。而且重头实现整个Backend既不实际而且也不能从根本上解决问题。所以我们可以换一个思路。所有的数据库操作都是从获取Connection对象开始的,而获取Connection对象只有一个入口,就是MySQLdb.connect这个函数。所以我们只需要包装MySQLdb这个模块,用我们自己的connect方法替代原本的,这样就从根源上解决了问题。我们在包装器内部维护MySQLdb的Connection对象,使其保持长连接,每次connect被调用的时候判断一下,如果连接存在就返回现有连接,不就完美了吗?所以我们可以分分钟写下第一个解决方案:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
proxies = {}
 
class _DbWrapper():
     def __init__( self ,module):
         self .connection = None #这个就是维护的长连接对象
         self .db = module           #这个是被包装的原生MySQLdb的module
 
     def __getattr__( self , key):
         return getattr ( self .db, key)   #代理所有不关心的函数
 
     def connect( self , * argv, * * kwargv):
         “”“
         替换原有的connection对象
         ”“”
         if not self .connection:
             self .connection = self .db.connect( * argv, * * kwargv)
         return _ConnectionWrapper( self .connection)
 
def manage(module,keepalive = 7 * 3600 ):
     “”“
     返回代替原生MySQLdb模块的对象
     ”“”
     try :
         return proxies[module]
     except KeyError:
         return proxies.setdefault(module,_DbWrapper(module))

 

把上面代码存到一个叫pool.py的文件里。然后把Django源码里的db/backend/mysql这个package拷贝出来,单独存到我们project目录里一个mysql_pool的目录里。然后修改其中的base.py,在顶上import的部分,找到 import MySQLdb as Database 这句,用下面代码替换之

?
1
2
3
4
5
6
try :
     import MySQLdb as Database
     Database = pool.manage(Database)
except ImportError, e:
     from django.core.exceptions import ImproperlyConfigured
     raise ImproperlyConfigured( "Error loading MySQLdb module: %s" % e)

  这样我们就用自己的模块替换了MySQLdb的,当要connect的时候判断到有连接的时候就不重新创建连接了。

把站点跑起来看,结果如何?刷新几次后报错了。Why?看看日志可以看到如下的错误:

Traceback (most recent call last):
File "/home/www/.virtualenvs/django13/lib/python2.7/site-packages/gevent/wsgi.py", line 114, in handle
result = self.server.application(env, self.start_response)
File "/home/www/.virtualenvs/django13/lib/python2.7/site-packages/django/core/handlers/wsgi.py", line 275, in __call__
signals.request_finished.send(sender=self.__class__)
File "/home/www/.virtualenvs/django13/lib/python2.7/site-packages/django/dispatch/dispatcher.py", line 172, in send
response = receiver(signal=self, sender=sender, **named)
File "/home/www/.virtualenvs/django13/lib/python2.7/site-packages/django/db/__init__.py", line 85, in close_connection
conn.close()
File "/home/www/.virtualenvs/django13/lib/python2.7/site-packages/django/db/backends/__init__.py", line 244, in close
self.connection.close()

看来我们光是包装了MySQLdb本身还不行,在connect后Django获取了Connection的对象,之后就能为所欲为,他用完后很自觉的关掉了,因为他直觉的以为每次connect都拿到了新的Connection对象。所以我们必须把Connection对象也包装了了。所以升级后的解决方案代码如下:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
proxies = {}
 
class _ConnectionWrapper( object ):
     """
     用来包装Connection的类
     """
     def __init__( self ,conn):
         self .conn = conn
 
     def close( self ):
         """
         屏蔽掉关闭连接的行为
         """
         pass
 
     def __getattr__( self ,key):
         """
         把其他属性都原封不动的代理出去
         """
         return getattr ( self .conn, key)
 
class _DbWrapper():
     """
     代理MySQLdb模块的对象
     """
     def __init__( self ,module):
         self .connection = None  #HOLD住的长连接
         self .db = module            #原始的MySQLdb模块
 
     def __getattr__( self , key):
         """
         代理除connect外的所有属性
         """
         return getattr ( self .db, key)
 
     def connect( self , * argv, * * kwargv):
         if not self .connection:
             self .connection = self .db.connect( * argv, * * kwargv)
         return _ConnectionWrapper( self .connection)
 
def manage(module):
     try :
         return proxies[module]
     except KeyError:
         return proxies.setdefault(module,_DbWrapper(module))

  我们增加了一个_ConnectionWrapper类来代理Connection对象,然后屏蔽掉close函数。把站点跑起来后发现不会出现之前的问题了,跑起来也顺畅不少。但是过了几个小时后问题又来了。因为MySQLdb的Connection有个很蛋痛的问题,就是连接闲置8小时后会自己断掉。不过要解决这个问题很简单,我们发现连接如果闲置了快8小时就close掉重新建立一个连接不就行了么?所以最后解决方案的代码如下:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import time
 
proxies = {}
 
class _ConnectionWrapper( object ):
     def __init__( self ,conn):
         self .conn = conn
 
     def close( self ):
         pass
 
     def __getattr__( self ,key):
         return getattr ( self .conn, key)
 
class _DbWrapper():
     def __init__( self ,module,max_idle):
         self .connection = None
         self .db = module
         self .max_idle = max_idle
         self .connected = 0
 
     def __getattr__( self , key):
         return getattr ( self .db, key)
 
     def connect( self , * argv, * * kwargv):
         if not self .connection or time.time() - self .connected> = self .max_idle:
             try :
                 if self .connection:
                     self .connection.close()
             except :
                 pass
             self .connection = self .db.connect( * argv, * * kwargv)
         self .connected = time.time()
         return _ConnectionWrapper( self .connection)
 
def manage(module,keepalive = 7 * 3600 ):
     try :
         return proxies[module]
     except KeyError:
         return proxies.setdefault(module,_DbWrapper(module,keepalive))

  就此问题解决,世界终于清净了


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值