Странные ошибки MySQL - колхозный метод обхода в yii2

Feb 08, 2023 23:22

Столкнулся на одном из проектов со странным поведением связки PHP+MySQL.
Проект на yii2, работающий под PHP8. На отдельном виртуальном сервере работает MySQL (есть и другие взаимодействующие сервисы, но сейчас речь не о них). И понадобилось по этому проекту обсчитать несколько очень громоздких отчётов по одинаковой методике. Для этого в контроллере командной строки (исключаем фактор max_execution_time) был написан код, в котором в числе прочего имеется метод проверки данных примерно такого вида именаизменены совпаденияслучайны:

private function checkUser(int $id): bool
{
$user = \app\models\User::findOne($id);
// other code
}

То есть, всё вполне себе просто: простейшим запросом ищем запись в таблице по первичному ключу и дальше уже проверяем условия.
Этот метод мог вызываться в ходе обсчёта несколько сотен тысяч раз (строк в таблице много, обсчёт мог длиться и несколько часов); при этом других запросов к MySQL от скрипта в этот момент просто не было, только эти подряд. Вроде бы ничего намекающего на проблемы. Но: на некоторых обсчётах в ходе выполнения запроса неожиданно выпадало исключение (конфиденциальная инфа, конечно, заменена):

PHP Fatal error: Uncaught PDOException: SQLSTATE[HY000]: General error: 2006 MySQL server has gone away in /home/project/deploy/releases/XXX/vendor/yiisoft/yii2/db/Command.php:1302
Stack trace:
#0 /home/project/deploy/releases/XXX/vendor/yiisoft/yii2/db/Command.php(1302): PDOStatement->execute()
#1 /home/project/deploy/releases/XXX/vendor/yiisoft/yii2/db/Command.php(1168): yii\db\Command->internalExecute()
#2 /home/project/deploy/releases/XXX/vendor/yiisoft/yii2/db/Command.php(424): yii\db\Command->queryInternal()
#3 /home/project/deploy/releases/XXX/vendor/yiisoft/yii2/db/Query.php(287): yii\db\Command->queryOne()
#4 /home/project/deploy/releases/XXX/vendor/yiisoft/yii2/db/ActiveQuery.php(304): yii\db\Query->one()
#5 /home/project/deploy/releases/XXX/vendor/yiisoft/yii2/db/BaseActiveRecord.php(110): yii\db\ActiveQuery->one()
#6 /home/project/deploy/releases/XXX/yii2/commands/ReportController.php(233): yii\db\BaseActiveRecord::findOne()
#7 /home/project/deploy/releases/XXX/yii2/commands/ReportController.php(420): app\commands\ReportController->checkUser()
#8 [internal function]: app\commands\ReportController->actionReportData()
#9 /home/project/deploy/releases/XXX/vendor/yiisoft/yii2/base/InlineAction.php(57): call_user_func_array()
#10 /home/project/deploy/releases/XXX/vendor/yiisoft/yii2/base/Controller.php(178): yii\base\InlineAction->runWithParams()
#11 /home/project/deploy/releases/XXX/vendor/yiisoft/yii2/console/Controller.php(180): yii\base\Controller->runAction()
#12 /home/project/deploy/releases/XXX/vendor/yiisoft/yii2/base/Module.php(552): yii\console\Controller->runAction()
#13 /home/project/deploy/releases/XXX/vendor/yiisoft/yii2/console/Application.php(180): yii\base\Module->runAction()
#14 /home/project/deploy/releases/XXX/vendor/yiisoft/yii2/console/Application.php(147): yii\console\Application->runAction()
#15 /home/project/deploy/releases/XXX/vendor/yiisoft/yii2/base/Application.php(384): yii\console\Application->handleRequest()
#16 /home/project/deploy/releases/XXX/yii2/yii(20): yii\base\Application->run()
#17 {main}
Next yii\db\Exception: SQLSTATE[HY000]: General error: 2006 MySQL server has gone away
The SQL being executed was: SELECT * FROM `user` WHERE `id`=YYYYY in /home/project/deploy/releases/XXX/vendor/yiisoft/yii2/db/Schema.php:676
Stack trace:
#0 /home/project/deploy/releases/XXX/vendor/yiisoft/yii2/db/Command.php(1307): yii\db\Schema->convertException()
#1 /home/project/deploy/releases/XXX/vendor/yiisoft/yii2/db/Command.php(1168): yii\db\Command->internalExecute()
#2 /home/project/deploy/releases/XXX/vendor/yiisoft/yii2/db/Command.php(424): yii\db\Command->queryInternal()
#3 /home/project/deploy/releases/XXX/vendor/yiisoft/yii2/db/Query.php(287): yii\db\Command->queryOne()
#4 /home/project/deploy/releases/XXX/vendor/yiisoft/yii2/db/ActiveQuery.php(304): yii\db\Query->one()
#5 /home/project/deploy/releases/XXX/vendor/yiisoft/yii2/db/BaseActiveRecord.php(110): yii\db\ActiveQuery->one()
#6 /home/project/deploy/releases/XXX/yii2/commands/ReportController.php(233): yii\db\BaseActiveRecord::findOne()
#7 /home/project/deploy/releases/XXX/yii2/commands/ReportController.php(420): app\commands\ReportController->checkUser()
#8 [internal function]: app\commands\ReportController->actionReportData()
#9 /home/project/deploy/releases/XXX/vendor/yiisoft/yii2/base/InlineAction.php(57): call_user_func_array()
#10 /home/project/deploy/releases/XXX/vendor/yiisoft/yii2/base/Controller.php(178): yii\base\InlineAction->runWithParams()
#11 /home/project/deploy/releases/XXX/vendor/yiisoft/yii2/console/Controller.php(180): yii\base\Controller->runAction()
#12 /home/project/deploy/releases/XXX/vendor/yiisoft/yii2/base/Module.php(552): yii\console\Controller->runAction()
#13 /home/project/deploy/releases/XXX/vendor/yiisoft/yii2/console/Application.php(180): yii\base\Module->runAction()
#14 /home/project/deploy/releases/XXX/vendor/yiisoft/yii2/console/Application.php(147): yii\console\Application->runAction()
#15 /home/project/deploy/releases/XXX/vendor/yiisoft/yii2/base/Application.php(384): yii\console\Application->handleRequest()
#16 /home/project/deploy/releases/XXX/yii2/yii(20): yii\base\Application->run()
#17 {main}
Additional Information:
Array
(
[0] => HY000
[1] => 2006
[2] => MySQL server has gone away
)

thrown in /home/project/deploy/releases/XXX/vendor/yiisoft/yii2/db/Schema.php on line 676

При этом значения id попадались произвольно, но в любом случае после довольно приличного количества вызовов метода. И только (!) на таких запросах.
Естественно, возник вопрос, чтозанафиг? Почему на простейшем запросе может происходить такой завал? Такое я в своё время видел, когда длина текста SQL-запроса превышала заданную в конфигах, но здесь такого нет даже близко. Скрипты, делающие сотни тысяч простых запросов к базе, я уже писал не раз и не два (в том числе для этого проекта), но такого никогда ещё не видел.
Пока что в направлении выяснения причины репа чешется, но понимания ситуации пока нет. На всякий случай, если вдруг кто с подобным столкнётся, даю сработавший способ обхода ситуации. Скажу и честно сразу: не уверен, что он приличный с точки зрения yii и вообще не говнокод, но задачу выполнить и всё обсчитать позволил; если очень надо, то при осторожном применении годен. Конструктивная критика и подсказка приличных методов принимается в комментах. В общем, вызов запроса был немного модифицирован:

private function checkUser($id): bool
{
do {
$success = true;
try {
$user = \app\models\User::findOne($id);
} catch(\yii\db\Exception $e) {
$this->stdout('Exception for user id ' . $id . "\n");
$success = false;
$db = \app\models\User::getDb();
$db->close();
$db->open();
}
} while ($success === false);
// other code
}

Перевод на русский таков. Если в ходе выполнения запроса падает ошибка, PDO завершал скрипт с помощью исключения. Это исключение мы начали ловить, при его выпадании ругаемся, но делаем переподключение к серверу БД и повторяем запрос в цикле, пока он не выполнится успешно. Проблема будет, только если сервер базы упадёт совсем, но это будет, по крайней мере, видно по циклическим сообщениям в консоли.
На всякий случай сразу отвечу на вопрос, зачем при переподключении делать сначала close? Соединение с сервером БД ведь уже разорвано. Тоже сначала так думал и написал только open. Но, видимо, PDO или yii не считал соединение разорванным, и open вёл себя согласно документации к yii: если соединение уже установлено, то ничего не делать. Пришлось словить вывод сообщения в бесконечном цикле и доработать.

колхоз, программизмы

Previous post Next post
Up