Почему я получаю "MySQL server has gone away" после запуска бота Telegram в течение нескольких часов?

Я создаю приложение Django (версия 3.0.5), которое использует mysqlclient (версия 2.0.3) в качестве бэкенда БД. Кроме того, я написал команду Django, которая запускает бота, написанного с использованием API python-telegram-bot.

Проблема заключается в том, что примерно через 24 часа после запуска бота (не обязательно все время простаивающего), я получаю исключение django.db.utils.OperationalError: (2006, 'MySQL server has gone away') после выполнения любой команды.

Я абсолютно уверен, что сервер MySQL работал все время и все еще работает в то время, когда я получаю это исключение.

Я предполагаю, что некоторые потоки MySQL устаревают и закрываются, поэтому после повторного использования они не возобновляются.

Кто-нибудь сталкивался с такой ситуацией и знает, как ее решить?

Обычно это происходит из-за того, что на стороне сервера wait_timeout. Сервер закрывает соединение после wait_timeout секунд бездействия. Вам следует либо увеличить таймаут:

SET SESSION wait_timeout = ...

Или справиться с подобной ошибкой, переподключиться и повторить попытку, когда это произойдет.

Другой вариант - пинговать сервер запросом (например, select 1) через регулярные промежутки времени (wait_timeout - 1)

Эта ошибка возникала в django, когда MySQL закрывал соединение из-за тайм-аута сервера. Чтобы включить постоянные соединения, установите CONN_MAX_AGE в целое положительное число секунд или установите None для неограниченных постоянных соединений (source).

Update1:. Если предложенное выше решение не сработало, вы можете попробовать пакет 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: как описано здесь

Django ORM - это синхронная часть кода, и поэтому, если вы хотите получить к нему доступ из асинхронного кода, вам нужно сделать специальную обработку, чтобы убедиться, что его соединения закрываются должным образом.

Однако, похоже, что 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

Сначала обязательно следуйте инструкции по установке здесь.

Соединение будет разорвано по любой из множества причин, а не только по таймауту. Поэтому просто планируйте его разрыв.

План 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, который имеет метод 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)

После этого изменения у меня больше не было такой проблемы.

Вернуться на верх