为什么在 运行 Telegram 机器人几个小时后我得到 'MySQL server has gone away'?

Why do I get 'MySQL server has gone away' after running a Telegram bot for some hours?

我正在构建一个使用 mysqlclient(版本 2.0.3)作为数据库后端的 Django(版本 3.0.5)应用程序。此外,我写了一个 Django 命令,运行 是一个使用 python-telegram-bot API 编写的机器人,所以这个机器人的任务是无限期地 运行,因为它必须随时响应命令。

问题是大约 24 小时。在 运行 启动机器人后(不一定一直处于空闲状态),在 运行 启动任何命令后我得到一个 django.db.utils.OperationalError: (2006, 'MySQL server has gone away') 异常。

我绝对确定 MySQL 服务器一直处于 运行ning 状态,并且在我收到此异常时仍处于 运行ning 状态。 MySQL 服务器版本为 5.7.35.

我的假设是某些 MySQL 线程会老化并关闭,因此在重新使用它们后它们将不会被更新。

有没有人碰到过这种情况,知道怎么解决的?

Traceback (most recent call last):
  File "/opt/django/gip/venv/lib/python3.6/site-packages/telegram/ext/dispatcher.py", line 555, in process_update
    handler.handle_update(update, self, check, context)
  File "/opt/django/gip/venv/lib/python3.6/site-packages/telegram/ext/handler.py", line 198, in handle_update
    return self.callback(update, context)
  File "/opt/django/gip/gip/hospital/gipcrbot.py", line 114, in ayuda
    perfil = get_permiso_efectivo(update.message.from_user.id)
  File "/opt/django/gip/gip/hospital/telegram/funciones.py", line 33, in get_permiso_efectivo
    u = Telegram.objects.get(idtelegram=userid)
  File "/opt/django/gip/venv/lib/python3.6/site-packages/django/db/models/manager.py", line 82, in manager_method
    return getattr(self.get_queryset(), name)(*args, **kwargs)
  File "/opt/django/gip/venv/lib/python3.6/site-packages/django/db/models/query.py", line 411, in get
    num = len(clone)
  File "/opt/django/gip/venv/lib/python3.6/site-packages/django/db/models/query.py", line 258, in __len__
    self._fetch_all()
  File "/opt/django/gip/venv/lib/python3.6/site-packages/django/db/models/query.py", line 1261, in _fetch_all
    self._result_cache = list(self._iterable_class(self))
  File "/opt/django/gip/venv/lib/python3.6/site-packages/django/db/models/query.py", line 57, in __iter__
    results = compiler.execute_sql(chunked_fetch=self.chunked_fetch, chunk_size=self.chunk_size)
  File "/opt/django/gip/venv/lib/python3.6/site-packages/django/db/models/sql/compiler.py", line 1151, in execute_sql
    cursor.execute(sql, params)
  File "/opt/django/gip/venv/lib/python3.6/site-packages/django/db/backends/utils.py", line 100, in execute
    return super().execute(sql, params)
  File "/opt/django/gip/venv/lib/python3.6/site-packages/django/db/backends/utils.py", line 68, in execute
    return self._execute_with_wrappers(sql, params, many=False, executor=self._execute)
  File "/opt/django/gip/venv/lib/python3.6/site-packages/django/db/backends/utils.py", line 77, in _execute_with_wrappers
    return executor(sql, params, many, context)
  File "/opt/django/gip/venv/lib/python3.6/site-packages/django/db/backends/utils.py", line 86, in _execute
    return self.cursor.execute(sql, params)
  File "/opt/django/gip/venv/lib/python3.6/site-packages/django/db/utils.py", line 90, in __exit__
    raise dj_exc_value.with_traceback(traceback) from exc_value
  File "/opt/django/gip/venv/lib/python3.6/site-packages/django/db/backends/utils.py", line 86, in _execute
    return self.cursor.execute(sql, params)
  File "/opt/django/gip/venv/lib/python3.6/site-packages/django/db/backends/mysql/base.py", line 74, in execute
    return self.cursor.execute(query, args)
  File "/opt/django/gip/venv/lib/python3.6/site-packages/MySQLdb/cursors.py", line 206, in execute
    res = self._query(query)
  File "/opt/django/gip/venv/lib/python3.6/site-packages/MySQLdb/cursors.py", line 319, in _query
    db.query(q)
  File "/opt/django/gip/venv/lib/python3.6/site-packages/MySQLdb/connections.py", line 259, in query
    _mysql.connection.query(self, query)
django.db.utils.OperationalError: (2006, 'MySQL server has gone away')

我试过的东西

我已经尝试更改 Django settings.py 文件,因此我为 CONN_MAX_AGE 设置了一个显式值,并且我还为 MySQL 客户端 wait_timeout 参数设置了一个值, CONN_MAX_AGE 低于 wait_timeout.

settings.py:

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'OPTIONS': {
            'read_default_file': '/opt/django/gip/gip/gip/my.cnf',
        },
        'CONN_MAX_AGE': 3600,
    }
}

my.cnf:

[client]
...
wait_timeout = 28800

不幸的是,行为完全相同:我在大约 24 小时后收到异常。在 运行 启动机器人之后。


CONN_MAX_AGE 设置为 None 也没有任何区别。


我按照@r-marolahy 的建议安装了 mysql-server-has-gone-away python 包,但也没有什么不同。在 运行 将其关闭将近 24 小时后,“消失”消息再次显示。


我也试过关闭旧连接的方法:

from django.db import close_old_connections

try:
    #do your long running operation here
except django.db.utils.OperationalError:
    close_old_connections()
    #do your long running operation here

仍然得到相同的结果。

这通常是因为服务器端 wait_timeout。服务器在 wait_timeout 秒不活动后关闭连接。 您应该增加超时:

SET SESSION wait_timeout = ...

或处理此类错误并在发生时重新连接并重试。

另一种选择是定期使用查询(如 select 1)ping 服务器 (wait_timeout - 1)

由于服务器超时,MySQL 关闭连接时django 发生此错误。要启用持久连接,请将 CONN_MAX_AGE 设置为秒的正整数或将其设置为 None 以实现无限持久连接 (source)。

更新1: 如果上面建议的解决方案不起作用,您可能想尝试 mysql-server-has-gone-away 包。我还没有尝试过,但在这种情况下可能会有所帮助。

Update2: 另一种尝试是尝试使用 try/except 语句来捕获此 OperationalError 并重置与 close_old_connections 的连接。

from django.db import close_old_connections

try:
    #do your long running operation here
except django.db.utils.OperationalError:
    close_old_connections()
    #do your long running operation here

update3: 描述 here

The Django ORM is a synchronous piece of code, and so if you want to access it from asynchronous code you need to do special handling to make sure its connections are closed properly.

然而,Django ORM 使用 asgiref.sync.sync_to_async 适配器接缝,该适配器仅在 MySQL 关闭连接之前有效。在这种情况下,使用 channels.db.database_sync_to_async(这是 SyncToAsync 版本,在它退出时清理旧的数据库连接)可能会解决这个问题。

您可以像下面这样使用它 (source):

from channels.db import database_sync_to_async

async def connect(self):
    self.username = await database_sync_to_async(self.get_name)()

def get_name(self):
    return User.objects.all()[0].name

或将其用作装饰器:

@database_sync_to_async
def get_name(self):
    return User.objects.all()[0].name

确保首先遵循安装说明 here

连接会因多种原因而中断,而不仅仅是超时。 所以,简单地计划一下吧。

计划 A(最稳健的解决方案):

每当 运行 查询时,检查错误并使用代码重新连接并重新运行查询(或事务)。

B计划(交易有风险):

开启自动重新连接。如果您使用过多语句交易,这就是 不好的 。事务中间的自动重新连接可能会导致数据集损坏(通过违反“事务”的语义)。这是因为事务的第一部分是 ROLLBACK'd(由于断开连接)和剩下的是 COMMITted.

计划 C(直截了当):

收到消息时连接;做你的工作;然后断开连接。也就是说,大部分时间都断开连接。如果您有很多客户端连接(但很少做任何事情),则此方法是必要的。

方案D(不值得考虑):

增加各种超时。当其他问题未解决(网络问题)时,为什么还要费心解决一个问题(低超时)。

我的偏好? C 很容易,而且几乎总是足够的。 A 需要更多的代码,但是是“最好的”。 C和A都可能更好

发生这种情况的原因是因为 close_old_connection 函数。

所以,您可以尝试做的是在与 db 交互之前添加关闭旧连接的调用:

示例代码:

from django.db import close_old_connections
close_old_connections()
# do some db actions, it will reconnect db

如果不能解决您的问题,请告诉我。

我也有这个问题。 (2006, 'MySQL server has gone away') 可能由于各种原因而发生,其中:

  • 请求太长

解决方案是微调 MySQL/MariaDB 以允许更大的请求:

/etc/mysql/mariadb.conf.d/50-server.cnf

[mysqld]
...
max_allowed_packet=128M
innodb_log_file_size = 128M # Fix kopano-server: SQL [00000088] info: MySQL server has gone away. Reconnecting, see https://jira.kopano.io/browse/KC-1053
  • 几个小时没有与客户互动

您可以使用此处发布的任何其他解决方案。 解决方案可以是将超时设置为非常大的值(在我的设置中为 80 小时)

/etc/mysql/mariadb.conf.d/50-server.cnf

[mysqld]
...
wait_timeout = 288000 # Increase timeout to 80h before Mysql server will also go away

我最终在机器人中每隔 X 小时(在本例中为 6 小时)安排一次数据库查询。 python-telegram-bot 有一个名为 JobQueue 的 class,它有一个名为 run_repeating 的方法。这将 运行 每 n 秒执行一次任务。所以我声明:

def check_db(context):
    # Do the code for running "SELECT 1" in the DB
    return

updater.job_queue.run_repeating(check_db, interval=21600, first=21600)

这次更改后,我再也没有遇到同样的问题。


此外,在我的情况下,不时调用大部分未记录的 close_if_unusable_or_obsolete() Django 方法也有效。

from django.db import connection

connection.close_if_unusable_or_obsolete()