上周對我們用Django+Django-rest-framework提供的一套接口進行了壓力測試。壓測的過程中,收到DBA通知——數據庫連接數過多,希望我們優化下程序。具體症狀就是,如果設置mysql的最大連接數為1000,壓測過程中,很快連接數就會達到上限,調整上限到2000,依然如此。
Django的數據庫連接
Django對數據庫的鏈接處理是這樣的,Django程序接受到請求之后,在第一訪問數據庫的時候會創建一個數據庫連接,直到請求結束,關閉連接。下次請求也是如此。因此,這種情況下,隨着訪問的並發數越來越高,就會產生大量的數據庫連接。也就是我們在壓測時出現的情況。
關於Django每次接受到請求和處理完請求時對數據庫連接的操作,最后會從源碼上來看看。
使用CONN_MAX_AGE減少數據庫請求
上面說了,每次請求都會創建新的數據庫連接,這對於高訪問量的應用來說完全是不可接受的。因此在Django1.6時,提供了持久的數據庫連接,通過DATABASE配置上添加CONN_MAX_AGE來控制每個連接的最大存活時間。具體使用可以參考最后的鏈接。
這個參數的原理就是在每次創建完數據庫連接之后,把連接放到一個Theard.local的實例中。在request請求開始結束的時候,打算關閉連接時會判斷是否超過CONN_MAX_AGE設置這個有效期。這是關閉。每次進行數據庫請求的時候其實只是判斷local中有沒有已存在的連接,有則復用。
基於上述原因,Django中對於CONN_MAX_AGE的使用是有些限制的,使用不當,會事得其反。因為保存的連接是基於線程局部變量的,因此如果你部署方式采用多線程,必須要注意保證你的最大線程數不會多余數據庫能支持的最大連接數。另外,如果使用開發模式運行程序(直接runserver的方式),建議不要設置CONN_MAX_AGE,因為這種情況下,每次請求都會創建一個Thread。同時如果你設置了CONN_MAX_AGE,將會導致你創建大量的不可復用的持久的連接。
CONN_MAX_AGE設置多久
CONN_MAX_AGE的時間怎么設置主要取決於數據庫對空閑連接的管理,比如你的MySQL設置了空閑1分鍾就關閉連接,那你的CONN_MAX_AGE就不能大於一分鍾,不過DBA已經習慣了程序中的線程池的概念,會在數據庫中設置一個較大的值。
優化結果
了解了上述過程之后,配置了CONN_MAX_AGE參數,再次測試,終於沒有接到DBA通知,查看數據庫連接數,最大700多。
最好的文檔是代碼
Django的文檔上只是簡單得介紹了原理和使用方式,對於好奇的同學來說,這個顯然是不夠的。於是我也好奇的看了下代碼,把相關的片段貼到這里。
首先是一次請求開始和結束時對連接的處理
#### 請求開始# django.core.handlers.wsgi.pyclass WSGIHandler(base.BaseHandler):initLock = Lock()request_class = WSGIRequestdef __call__(self, environ, start_response):# ..... 省略若干代碼# 觸發request_started這個Signalsignals.request_started.send(sender=self.__class__, environ=environ)try:request = self.request_class(environ)except UnicodeDecodeError:logger.warning('Bad Request (UnicodeDecodeError)',exc_info=sys.exc_info(),extra={'status_code': 400,})# 請求結束class HttpResponseBase(six.Iterator):"""An HTTP response base class with dictionary-accessed headers.This class doesn't handle content. It should not be used directly.Use the HttpResponse and StreamingHttpResponse subclasses instead."""def close(self):for closable in self._closable_objects:try:closable.close()except Exception:pass# 請求結束時觸發request_finished這個觸發器signals.request_finished.send(sender=self._handler_class)
這里只是觸發,那么在哪對這些signal進行處理呢?
# django.db.__init__.pyfrom django.db.utils import ConnectionHandlerconnections = ConnectionHandler()# Register an event to reset saved queries when a Django request is started.def reset_queries(**kwargs):for conn in connections.all():conn.queries_log.clear()signals.request_started.connect(reset_queries)# Register an event to reset transaction state and close connections past# their lifetime.def close_old_connections(**kwargs):for conn in connections.all():conn.close_if_unusable_or_obsolete()signals.request_started.connect(close_old_connections)signals.request_finished.connect(close_old_connections)
在這里對觸發的signal進行了處理,從代碼上看,邏輯就是,遍歷所有已存在的鏈接,關閉不可用的連接。
再來看ConnectionHandler代碼:
class ConnectionHandler(object):def __init__(self, databases=None):"""databases is an optional dictionary of database definitions (structuredlike settings.DATABASES)."""# databases來自settings對數據庫的配置self._databases = databasesself._connections = local()@cached_propertydef databases(self):if self._databases is None:self._databases = settings.DATABASESif self._databases == {}:self._databases = {DEFAULT_DB_ALIAS: {'ENGINE': 'django.db.backends.dummy',},}if DEFAULT_DB_ALIAS not in self._databases:raise ImproperlyConfigured("You must define a '%s' database" % DEFAULT_DB_ALIAS)return self._databasesdef __iter__(self):return iter(self.databases)def all(self):# 調用__iter__和__getitem__return [self[alias] for alias in self]def __getitem__(self, alias):if hasattr(self._connections, alias):return getattr(self._connections, alias)self.ensure_defaults(alias)self.prepare_test_settings(alias)db = self.databases[alias]backend = load_backend(db['ENGINE'])# 關鍵在這了,這個就是connconn = backend.DatabaseWrapper(db, alias)# 放到 local里setattr(self._connections, alias, conn)return conn
這個代碼的關鍵就是生成對於backend的conn,並且放到local中。backend.DatabaseWrapper繼承了db.backends.init.BaseDatabaseWrapper類的 close_if_unusable_or_obsolete() 的方法,來直接看下這個方法。
class BaseDatabaseWrapper(object):"""Represents a database connection."""def connect(self):"""Connects to the database. Assumes that the connection is closed."""# 連接數據庫時讀取配置中的CONN_MAX_AGEmax_age = self.settings_dict['CONN_MAX_AGE']self.close_at = None if max_age is None else time.time() + max_agedef close_if_unusable_or_obsolete(self):"""Closes the current connection if unrecoverable errors have occurred,or if it outlived its maximum age."""if self.connection is not None:# If the application didn't restore the original autocommit setting,# don't take chances, drop the connection.if self.get_autocommit() != self.settings_dict['AUTOCOMMIT']:self.close()return# If an exception other than DataError or IntegrityError occurred# since the last commit / rollback, check if the connection works.if self.errors_occurred:if self.is_usable():self.errors_occurred = Falseelse:self.close()returnif self.close_at is not None and time.time() >= self.close_at:self.close()return
參考
https://docs.djangoproject.com/en/1.6/ref/databases/#persistent-database-connections https://github.com/django/django/blob/master/django/core/handlers/wsgi.py#L164 https://github.com/django/django/blob/master/django/http/response.py#L310 https://github.com/django/django/blob/master/django/db/init.py#L62 https://github.com/django/django/blob/master/django/db/utils.py#L252 https://github.com/django/django/blob/master/django/db/backends/init.py#L383
