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