最近我们的 Django 项目供 Java Sofa 应用进行 tr 调用时, 经常会出现一个异常: django.db.utils.OperationalError: (2006, 'MySQL server has gone away')
. 本文记录了 分析, 本地重现与解决此问题 的全过程.
原因分析:
Django 在 1.6 引入长链接 (Persistent connections) 的概念, 可以在一个 HTTP 请求中一直用同一个连接对数据库进行读写操作.
但我们的应用对数据库的操作 太不频繁 了, 两次操作数据库的间隔大于 MySQL 配置的超时时间(默认为 8 个小时), 导致下一次操作数据库时的 connection 过期失效.
Our databases have a 300-second (5-minute) timeout on inactive connections. That means, if you open a connection to the database, and then you don't do anything with it for 5 minutes, then the server will disconnect, and the next time you try to execute a query, it will fail.
重现问题:
设置 mysql wait_timeout
为 10s
在 macOS 上的 mysql 配置文件路径: /usr/local/etc/my.cnf
1 | # Default Homebrew MySQL server config |
重启 mysql:
1 | ➜ ~ brew services restart mysql |
检查 wait_timeout
的值是否已被更新.
1 | mysql> show variables like '%wait_timeout%'; |
重现 exception:
1 | XXX.objects.exists() |
有意思的一个点是, sleep 10s 之后, 第一次操作数据库, 会出现 (2013, 'Lost connection to MySQL server during query’)
异常. 之后再操作数据库, 才会抛出 (2006, 'MySQL server has gone away’)
异常.
解决问题:
第一个最暴力的方法就是增加 mysql 的 wait_timeout
让 mysql 不要太快放弃连接. 感觉不太靠谱, 因为不能杜绝这种 Exception 的发生.
第二个办法就是手动把 connection 直接关闭:
1 | Alarm.objects.exists() |
发现不会出现 (2006, 'MySQL server has gone away’)
异常了, 但总感觉还是不够优雅.
最终决定在客户端 (Django), 设置超时时间(CONN_MAX_AGE: 5
) 比 mysql 服务端 (wait_timeout = 10
) 小:
1 | DATABASES = { |
但很奇怪没有生效??? 看了源代码, 发现只有在 request_started
(HTTP request) 和request_finished
的时候, 在 close_if_unusable_or_obsolete
才用到 CONN_MAX_AGE
并去验证时间关闭 connection.
具体代码见: python3.6/site-packages/django/db/__init__.py#64
1 | # Register an event to reset transaction state and close connections past |
而我的代码是处理一个任务而不是 HTTP 请求, 所以不会触发这个 signal. 于是我写了一个装饰器, 在任务的开始和结束的时候, 关闭所有数据库连接.
1 | from django.db import connections |
ps. CONN_MAX_AGE 默认其实为 0, 意味着默认在 http 请求和结束时会关闭所有数据库连接.
其他:
django.db 中 connection 和 connections 的区别???
connection
对应的是默认数据库的连接, 用代码表示就是connections[DEFAULT_DB_ALIAS]
connections
对应的是 setting.DATABASES 中所有数据库的 connection