經過CONN_MAX_AGE優化Django的數據庫鏈接

上週對咱們用Django+Django-rest-framework提供的一套接口進行了壓力測試。壓測的過程當中,收到DBA通知——數據庫鏈接數過多,但願咱們優化下程序。具體症狀就是,若是設置mysql的最大鏈接數爲1000,壓測過程當中,很快鏈接數就會達到上限,調整上限到2000,依然如此。mysql

Django的數據庫鏈接

Django對數據庫的連接處理是這樣的,Django程序接受到請求以後,在第一訪問數據庫的時候會建立一個數據庫鏈接,直到請求結束,關閉鏈接。下次請求也是如此。所以,這種狀況下,隨着訪問的併發數愈來愈高,就會產生大量的數據庫鏈接。也就是咱們在壓測時出現的狀況。git

關於Django每次接受到請求和處理完請求時對數據庫鏈接的操做,最後會從源碼上來看看。github

使用CONN_MAX_AGE減小數據庫請求

上面說了,每次請求都會建立新的數據庫鏈接,這對於高訪問量的應用來講徹底是不可接受的。所以在Django1.6時,提供了持久的數據庫鏈接,經過DATABASE配置上添加CONN_MAX_AGE來控制每一個鏈接的最大存活時間。具體使用能夠參考最後的連接。sql

這個參數的原理就是在每次建立完數據庫鏈接以後,把鏈接放到一個Theard.local的實例中。在request請求開始結束的時候,打算關閉鏈接時會判斷是否超過CONN_MAX_AGE設置這個有效期。這是關閉。每次進行數據庫請求的時候其實只是判斷local中有沒有已存在的鏈接,有則複用。數據庫

基於上述緣由,Django中對於CONN_MAX_AGE的使用是有些限制的,使用不當,會事得其反。由於保存的鏈接是基於線程局部變量的,所以若是你部署方式採用多線程,必需要注意保證你的最大線程數不會多餘數據庫能支持的最大鏈接數。另外,若是使用開發模式運行程序(直接runserver的方式),建議不要設置CONN_MAX_AGE,由於這種狀況下,每次請求都會建立一個Thread。同時若是你設置了CONN_MAX_AGE,將會致使你建立大量的不可複用的持久的鏈接。django

CONN_MAX_AGE設置多久

CONN_MAX_AGE的時間怎麼設置主要取決於數據庫對空閒鏈接的管理,好比你的MySQL設置了空閒1分鐘就關閉鏈接,那你的CONN_MAX_AGE就不能大於一分鐘,不過DBA已經習慣了程序中的線程池的概念,會在數據庫中設置一個較大的值。多線程

優化結果

瞭解了上述過程以後,配置了CONN_MAX_AGE參數,再次測試,終於沒有接到DBA通知,查看數據庫鏈接數,最大700多。併發

最好的文檔是代碼

Django的文檔上只是簡單得介紹了原理和使用方式,對於好奇的同窗來講,這個顯然是不夠的。因而我也好奇的看了下代碼,把相關的片斷貼到這裏。app

首先是一次請求開始和結束時對鏈接的處理

  1. #### 請求開始
  2. # django.core.handlers.wsgi.py
  3. class WSGIHandler(base.BaseHandler):
  4. initLock = Lock()
  5. request_class = WSGIRequest
  6. def __call__(self, environ, start_response):
  7. # ..... 省略若干代碼
  8. # 觸發request_started這個Signal
  9. signals.request_started.send(sender=self.__class__, environ=environ)
  10. try:
  11. request = self.request_class(environ)
  12. except UnicodeDecodeError:
  13. logger.warning('Bad Request (UnicodeDecodeError)',
  14. exc_info=sys.exc_info(),
  15. extra={
  16. 'status_code': 400,
  17. }
  18. )
  19. # 請求結束
  20. class HttpResponseBase(six.Iterator):
  21. """
  22. An HTTP response base class with dictionary-accessed headers.
  23. This class doesn't handle content. It should not be used directly.
  24. Use the HttpResponse and StreamingHttpResponse subclasses instead.
  25. """
  26. def close(self):
  27. for closable in self._closable_objects:
  28. try:
  29. closable.close()
  30. except Exception:
  31. pass
  32. # 請求結束時觸發request_finished這個觸發器
  33. signals.request_finished.send(sender=self._handler_class)

這裏只是觸發,那麼在哪對這些signal進行處理呢?測試

  1. # django.db.__init__.py
  2. from django.db.utils import ConnectionHandler
  3. connections = ConnectionHandler()
  4. # Register an event to reset saved queries when a Django request is started.
  5. def reset_queries(**kwargs):
  6. for conn in connections.all():
  7. conn.queries_log.clear()
  8. signals.request_started.connect(reset_queries)
  9. # Register an event to reset transaction state and close connections past
  10. # their lifetime.
  11. def close_old_connections(**kwargs):
  12. for conn in connections.all():
  13. conn.close_if_unusable_or_obsolete()
  14. signals.request_started.connect(close_old_connections)
  15. signals.request_finished.connect(close_old_connections)

在這裏對觸發的signal進行了處理,從代碼上看,邏輯就是,遍歷全部已存在的連接,關閉不可用的鏈接。

再來看ConnectionHandler代碼:

  1. class ConnectionHandler(object):
  2. def __init__(self, databases=None):
  3. """
  4. databases is an optional dictionary of database definitions (structured
  5. like settings.DATABASES).
  6. """
  7. # databases來自settings對數據庫的配置
  8. self._databases = databases
  9. self._connections = local()
  10. @cached_property
  11. def databases(self):
  12. if self._databases is None:
  13. self._databases = settings.DATABASES
  14. if self._databases == {}:
  15. self._databases = {
  16. DEFAULT_DB_ALIAS: {
  17. 'ENGINE': 'django.db.backends.dummy',
  18. },
  19. }
  20. if DEFAULT_DB_ALIAS not in self._databases:
  21. raise ImproperlyConfigured("You must define a '%s' database" % DEFAULT_DB_ALIAS)
  22. return self._databases
  23. def __iter__(self):
  24. return iter(self.databases)
  25. def all(self):
  26. # 調用__iter__和__getitem__
  27. return [self[alias] for alias in self]
  28. def __getitem__(self, alias):
  29. if hasattr(self._connections, alias):
  30. return getattr(self._connections, alias)
  31. self.ensure_defaults(alias)
  32. self.prepare_test_settings(alias)
  33. db = self.databases[alias]
  34. backend = load_backend(db['ENGINE'])
  35. # 關鍵在這了,這個就是conn
  36. conn = backend.DatabaseWrapper(db, alias)
  37. # 放到 local裏
  38. setattr(self._connections, alias, conn)
  39. return conn

這個代碼的關鍵就是生成對於backend的conn,而且放到local中。backend.DatabaseWrapper繼承了db.backends.init.BaseDatabaseWrapper類的 close_if_unusable_or_obsolete() 的方法,來直接看下這個方法。

  1. class BaseDatabaseWrapper(object):
  2. """
  3. Represents a database connection.
  4. """
  5. def connect(self):
  6. """Connects to the database. Assumes that the connection is closed."""
  7. # 鏈接數據庫時讀取配置中的CONN_MAX_AGE
  8. max_age = self.settings_dict['CONN_MAX_AGE']
  9. self.close_at = None if max_age is None else time.time() + max_age
  10. def close_if_unusable_or_obsolete(self):
  11. """
  12. Closes the current connection if unrecoverable errors have occurred,
  13. or if it outlived its maximum age.
  14. """
  15. if self.connection is not None:
  16. # If the application didn't restore the original autocommit setting,
  17. # don't take chances, drop the connection.
  18. if self.get_autocommit() != self.settings_dict['AUTOCOMMIT']:
  19. self.close()
  20. return
  21. # If an exception other than DataError or IntegrityError occurred
  22. # since the last commit / rollback, check if the connection works.
  23. if self.errors_occurred:
  24. if self.is_usable():
  25. self.errors_occurred = False
  26. else:
  27. self.close()
  28. return
  29. if self.close_at is not None and time.time() >= self.close_at:
  30. self.close()
  31. 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

相關文章
相關標籤/搜索