Почему я получаю "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)
После этого изменения у меня больше не было такой проблемы.