http://www.rainybowe.com/blog/2016/12/20/django%E6%95%B0%E6%8D%AE%E5%BA%93%E8%BF%9E%E6%8E%A5/index.html
最近總會遇到MySQL server has gone away
的報錯,然后就看了一下django的數據庫連接這一塊。
django數據庫連接
ORM中數據庫連接用到的connections
,從django.db
模塊引入,屬於ConnectionHandler
對象。
# django.db.__init__.py # django ORM中用到的數據庫連接來源 connections = ConnectionHandler() # 請求開始之前重置所有連接 def reset_queries(**kwargs): for conn in connections.all(): conn.queries_log.clear() signals.request_started.connect(reset_queries) # 請求開始結束之前遍歷所有已存在連接,關閉不可用的連接 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)
我理解的ConnectionHandler
類是一個數據庫連接管理器,負責根據不同數據庫后端創建數據庫連接,保存連接,給應用方提供連接,以及關閉所有連接。 這里通過django信號的方式,在請求開始之前以及請求結束之后關閉失效數據庫連接。
# django.db.utils.py class ConnectionHandler(object): def __init__(self, databases=None): # 獲取數據庫配置 self._databases = databases # 從當前線程變量獲取所有數據庫連接 self._connections = local() # 獲取數據庫連接關鍵邏輯 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']) # django.db.backends.mysql.base.DatabaseWrapper conn = backend.DatabaseWrapper(db, alias) setattr(self._connections, alias, conn) return conn
ConnectionHandler
中_connections
表示當前數據庫連接集合,是一個ThreadLocal
對象,是和線程綁定在一起的。在整個線程生命周期內,_connections
屬於全局變量,但是當線程一旦關閉,_connections
也消失了。
關鍵邏輯在於__getitem__
方法,當通過別名獲取數據庫連接時,首先從當前線程變量中獲取連接,獲取不到就根據別名創建新的數據庫連接,並將連接寫入ThreadLocal
。
通過CONN_MAX_AGE設置連接存活時間
django 1.6
開始支持持久數據庫連接,通過參數CONN_MAX_AGE
設置每個連接的最大存活時間。默認值是0,設置為None表示無限制的持久連接。
# django.db.backends.base.base.py class BaseDatabaseWrapper(object): def connect(self): self.in_atomic_block = False self.savepoint_ids = [] self.needs_rollback = False # 根據CONN_MAX_AGE參數設置連接的關閉時間 max_age = self.settings_dict['CONN_MAX_AGE'] self.close_at = None if max_age is None else time.time() + max_age ... ... def close_if_unusable_or_obsolete(self): if self.connection is not None: if self.get_autocommit() != self.settings_dict['AUTOCOMMIT']: self.close() return # 發生異常,檢查連接是否可用,不可用關閉連接 if self.errors_occurred: if self.is_usable(): self.errors_occurred = False else: self.close() return # 設置了超時時間,並且連接超時,關閉連接 if self.close_at is not None and time.time() >= self.close_at: self.close() return
數據庫連接在建立的時候會根據CONN_MAX_AGE
參數設置連接的close_at
屬性,表示連接失效時間。
再看上面👆django.db.__init__.py
的代碼,通過信號方式,每次請求開始以及結束的時候,會調用close_if_unusable_or_obsolete
方法,判斷當連接超時或者處在不可恢復狀態時則關閉連接
【所以CONN_MAX_AGE的值不能大於數據庫服務器的wait_timeout的值,否則服務器把連接關閉了,django還傻乎乎的以為連接可用,造成下次訪問的時候出現gone away的錯誤】。
總結
1. django的數據庫連接是保存到線程變量的 數據庫連接是全局的,但只存在於當前線程中,如果線程關閉,數據庫連接也不存在了。
2. 可以通過CONN_MAX_AGE參數配置數據庫連接的存活時間 即使設置了CONN_MAX_AGE參數,也是在線程依然存活的情況下,數據庫連接能夠存活的時間。
需要注意的兩點是:
-
CONN_MAX_AGE
應該小於數據庫本身的最大連接時間wait_timeout
,否則應用程序可能會獲取到連接超時的數據庫連接,這時會出現MySQL server has gone away
的報錯。 -
如果部署方式采用多線程,最大線程數不能大於最大數據庫連接數。另外,開發模式下(runserver),由於每條請求都是創建一個新的
Thread
,就不要使用CONN_MAX_AGE
參數了,這樣在老的請求線程中保存的數據庫連接根本不能復用。
參考閱讀:
通過CONN_MAX_AGE優化Django的數據庫連接
Django Doc
==================================================
https://my.oschina.net/gongju/blog/203142
http://www.cnblogs.com/Alexander-Lee/archive/2011/11/12/django_long_connection.html
http://jingpin.jikexueyuan.com/article/56130.html
https://gxnotes.com/article/76436.html
https://zhuanlan.zhihu.com/p/26986895
前幾篇文章中提到的8小時后連接斷開,估計就是CONN_MAX_AGE控制的吧?
我使用django與apache和mod_wsgi和PostgreSQL(所有在同一個主機),我需要處理很多簡單的動態頁面請求(每秒數百)。我面臨的瓶頸是django沒有持久的數據庫連接和重新連接每個請求(這需要接近5ms)。在做一個基准測試時,我得到了持久的連接,我可以處理近500 r /s,而沒有我只有50 r /s。
任何人有任何建議?如何修改django來使用持久連接?或加速從python到DB的連接
提前致謝。
最佳解決方案
Django 1.6已添加persistent connections support (link to doc for django 1.9):
Persistent connections avoid the overhead of re-establishing a connection to the database in each request. They’re controlled by the CONN_MAX_AGE parameter which defines the maximum lifetime of a connection. It can be set independently for each database.
DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'chipdb', # 你的數據庫名稱 'USER': 'chip', # 你的數據庫用戶名 'PASSWORD': 'mypassword', # 你的數據庫密碼 'HOST': 'chip.mysql.rds.aliyuncs.com', # 你的數據庫主機,留空默認為localhost 'PORT': '3306', # 你的數據庫端口 'CONN_MAX_AGE': 60, # 空閑超時關閉數據庫連接, 0 表示使用完馬上關閉,None 表示不關閉 } ... }
None 和設置一個超時時間還是有區別的:
經測試發現:
即使你設置了'CONN_MAX_AGE': 60
因為在/usr/local/python2.7.13/lib/python2.7/site-packages/django/db/__init__.py中
signals.request_started.connect(close_old_connections)
signals.request_finished.connect(close_old_connections)
,所以每次請求時,還是會close_old_connections,現象就是:
確實會持久,但依然一次請求一個連接
正確的做法是設置為None
次佳解決方案
嘗試PgBouncer – PostgreSQL的輕量級連接池。特征:
-
旋轉連接時的幾級暴行:
-
會話池
-
事務池
-
語句池
-
-
低內存要求(默認為2k每個連接)。
第三種解決方案
在Django中繼,編輯django/db/__init__.py
並注釋掉該行:
signals.request_finished.connect(close_connection)
這個信號處理程序使它在每次請求后斷開與數據庫的連接。我不知道這樣做的所有side-effects是什么,但是在每個請求之后開始一個新的連接是沒有意義的;如您所注意到的那樣,它會破壞性能。
我現在使用這個,但是我沒有完成一整套測試,看看是否有任何破壞。
我不知道為什么大家認為這需要一個新的后台或一個特殊的連接池或其他復雜的解決方案。這似乎很簡單,雖然我不懷疑有一些晦澀的陷阱,使他們首先做到這一點 – 應該更明智地處理;正如你所注意到的,每個請求的5ms開銷對於high-performance服務來說是相當多的。 (需要我150ms – 我還沒有想出為什么還有)
編輯:另一個必要的更改是在django /middleware /transaction.py;刪除兩個transaction.is_dirty()測試,並始終調用 commit()或 rollback()。否則,如果只從數據庫中讀取,那么它將不會提交事務,這將使鎖定打開,應該被關閉。
第四種方案
我創建了一個小型Django patch,通過sqlalchemy池實現MySQL和PostgreSQL的連接池。
這對http://grandcapital.net/很長時間的生產是完美的。
補丁是在谷歌搜索這個話題之后寫的。
第五種方案
免責聲明:我還沒有嘗試過。
我相信你需要實現一個自定義的數據庫后端。網絡上有幾個示例顯示了如何使用連接池實現數據庫后端。
使用連接池可能是一個很好的解決方案,因為網絡連接在連接返回到池時保持打開狀態。
兩個帖子都使用MySQL – 也許你可以使用類似的技術與Postgresql。
編輯:
-
有人為執行連接池的psycopg2后端發布了a patch。我建議在自己的項目中創建一個現有的后端的副本,然后修補它。
參考文獻
============================================================
這是2014年是寫的一篇文章,今天翻開來看依然有效,只是代碼會有些不同。這篇老文章,但是代碼根據Django 2.0的版本進行了更新。
有同學問要怎么來閱讀Django源代碼,這篇文章就是個一個例子,從問題入手,步入到源碼深處。
不怎么優雅的分割 ----------------------
上周對我們用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的文檔上只是簡單得介紹了原理和使用方式,對於好奇的同學來說,這個顯然是不夠的。於是我也好奇的看了下代碼,把相關的片段貼到這里。
1. 首先是一次請求開始和結束時對連接的處理
請求開始
# django.core.handlers.wsgi.py
class WSGIHandler(base.BaseHandler):
initLock = Lock()
request_class = WSGIRequest
def __call__(self, environ, start_response):
# ..... 省略若干代碼
# 觸發request_started這個Signal
signals.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,
}
)
# 請求結束
# 代碼位置:django/http/response.py
class HttpResponseBase:
"""
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
self.closed = True
signals.request_finished.send(sender=self._handler_class)
這里只是觸發,那么在哪對這些signal進行處理呢?
# 文件:django.db.__init__.py from django.db.utils import ConnectionHandler connections = 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代碼:
# 文件:django/db/utils.py class ConnectionHandler(object): def __init__(self, databases=None): """ databases is an optional dictionary of database definitions (structured like settings.DATABASES). """ # databases來自settings對數據庫的配置 self._databases = databases self._connections = local() @cached_property def databases(self): if self._databases is None: self._databases = settings.DATABASES if self._databases == {}: self._databases = { DEFAULT_DB_ALIAS: { 'ENGINE': 'django.db.backends.dummy', }, } if self._databases[DEFAULT_DB_ALIAS] == {}: 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._databases def __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']) # 關鍵在這了,這個就是conn conn = backend.DatabaseWrapper(db, alias) # 放到 local里 setattr(self._connections, alias, conn) return conn
這個代碼的關鍵就是生成對於backend的conn,並且放到local中。backend.DatabaseWrapper繼承了db.backends.base.base.BaseDatabaseWrapper類的 close_if_unusable_or_obsolete() 的方法,來直接看下這個方法。
# 文件:django/db/backends/base/base.py class BaseDatabaseWrapper: """ Represents a database connection. """ def connect(self): """Connects to the database. Assumes that the connection is closed.""" # ....省略其他代碼 # 連接數據庫時讀取配置中的CONN_MAX_AGE max_age = self.settings_dict['CONN_MAX_AGE'] self.close_at = None if max_age is None else time.time() + max_age # ....省略其他代碼 def 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 = False else: self.close() return if self.close_at is not None and time.time() >= self.close_at: self.close() return
參考
https://docs.djangoproject.com/en/dev/ref/databases/#persistent-database-connections
----EOF-----