讓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被調用的時候判斷一下,如果連接存在就返回現有連接,不就完美了嗎?所以我們可以分分鐘寫下第一個解決方案:


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):
        if not self.connection:
           self.connection=self.db.connect(*argv,**kwargv)
           return _ConnectionWrapper(self.connection)
 
    def manage(module,keepalive=7*3600):
        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 這句,用下面代碼替換之

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對象也包裝了了。所以升級後的解決方案代碼如下:

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掉重新建立一個連接不就行了麼?所以最後解決方案的代碼如下:


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))

就此問題解決,世界終於清淨了
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章