Код MySQL вызывает сбой PHP-скрипта в popen / exec

У меня есть следующий код PHP 5.6.19 на сервере Ubuntu 14.04 . Этот код просто подключается к MySQL 5.6.28 данных MySQL 5.6.28 , ждет минуту, запускает другой процесс сам, а затем выходит.

Примечание: это полный скрипт, и цель состоит в том, чтобы продемонстрировать проблему – она ​​не делает ничего полезного.

 class DatabaseConnector { const DB_HOST = 'localhost'; const DB_NAME = 'database1'; const DB_USERNAME = 'root'; const DB_PASSWORD = 'password'; public static $db; public static function Init() { if (DatabaseConnector::$db === null) { DatabaseConnector::$db = new PDO('mysql:host=' . DatabaseConnector::DB_HOST . ';dbname=' . DatabaseConnector::DB_NAME . ';charset=utf8', DatabaseConnector::DB_USERNAME, DatabaseConnector::DB_PASSWORD); } } } $startTime = time(); // ***** Script works fine if this line is removed. DatabaseConnector::Init(); while (true) { // Sleep for 100 ms. usleep(100000); if (time() - $startTime > 60) { $filePath = __FILE__; $cmd = "nohup php $filePath > /tmp/1.log 2>&1 &"; // ***** Script sometimes exits here without opening the process and without errors. $p = popen($cmd, 'r'); pclose($p); exit; } } 

Я запускаю первый процесс скрипта, используя nohup php myscript.php > /tmp/1.log 2>&1 & .

Этот цикл цикла должен продолжаться вечно, но … на основе нескольких тестов в течение дня (но не сразу) процесс на сервере «исчезает» без причины. Я обнаружил, что код MySQL вызывает popen кода доступа (сценарий выходит без ошибок или вывода).

Что здесь происходит?


Заметки

  • Сервер работает 24/7.
  • Память не является проблемой.
  • База данных подключается правильно.
  • Путь к файлу не содержит пробелов.
  • Та же проблема существует при использовании shell_exec или exec вместо popenpclose ).

Я также знаю, что popen – это строка, которая терпит неудачу, потому что я сделал дальнейшую отладку (не показан выше), выполнив вход в файл в определенных точках скрипта.

Является ли родительский процесс окончательным выходом после форкирования? Я думал, что pclose будет ждать выхода ребенка, прежде чем вернуться.

Если это не выход, я бы предположил, что, поскольку соединение mySQL никогда не закрывается, вы в конечном итоге нажимаете ограничение на соединение (или какой-либо другой предел), когда вы создаете дерево дочерних процессов.

Редактировать 1

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

Похоже, что повторное создание дочерних процессов порождает все больше FD, до тех пор, пока в конце концов он не сможет больше:

 $ lsof | grep type=STREAM | wc -l 240 $ lsof | grep type=STREAM | wc -l 242 ... $ lsof | grep type=STREAM | wc -l 425 $ lsof | grep type=STREAM | wc -l 428 ... 

И это потому, что ребенок наследует родительские FD (в этом случае для подключения mySQL), когда он вилки.

Если вы закроете соединение mySQL перед тем, как popen (в вашем случае):

 DatabaseConnector::$db = null; 

Проблема, надеюсь, исчезнет.

У меня была аналогичная ситуация с использованием pcntl_fork() и подключения MySQL. Причина здесь, вероятно, такая же.

Справочная информация

popen() создает дочерний процесс. Вызов pclose() закрывает канал связи, и дочерний процесс продолжает работать до тех пор, пока он не выйдет. Это когда вещи начинают выходить из-под контроля.

Когда дочерний процесс завершается, родительский процесс получает сигнал SIGCHLD . Родительский процесс здесь – это интерпретатор PHP, который запускает код, который вы опубликовали. Детский процесс запускается с использованием popen() (неважно, какая команда выполняется).

Здесь есть небольшая вещь, которую вы, вероятно, не знаете или вы нашли в документации, и проигнорировали ее, потому что это не имеет большого смысла, когда одна программа на PHP. Он упоминается в документации sleep() :

Если вызов был прерван сигналом, sleep() возвращает ненулевое значение.

Функция sleep() PHP – это просто оболочка системного вызова sleep() Linux (и функция usleep() PHP – это оболочка системного вызова usleep() Linux).

То, что не указано в документации PHP, четко указано в документации по системным вызовам:

sleep() заставляет вызывающий поток спать до тех пор, пока не истекут секунды секунд или не поступит сигнал, который не будет проигнорирован.

Вернитесь к своему коду.

В вашем коде есть два места, где интерпретатор PHP вызывает системную функцию usleep() Linux. Один из них хорошо виден: ваш PHP-код вызывает его. Другой скрыт (см. Ниже).

Что происходит (видимая часть)

Начиная со второй итерации, если дочерний процесс (созданный с использованием popen() на предыдущей итерации) заканчивается, когда родительская программа находится внутри вызова usleep(100000) , процесс интерпретатора PHP получает сигнал SIGCHLD и его выполнение возобновляется до время от времени. Функция usleep() возвращается раньше, чем ожидалось. Поскольку тайм-аут короток, этот эффект не наблюдается невооруженным глазом. Положите 10 секунд вместо 0,1 секунды, и вы это заметите.

Однако, кроме разбитого таймаута, это не влияет на выполнение вашего кода фатальным образом.

Почему он падает (невидимая часть)

Второе место, где входящий сигнал вредит выполнению ваших программ, скрыт глубоко внутри кода интерпретатора PHP. По некоторым причинам протокола клиентская библиотека MySQL использует функции sleep() и / или usleep() в нескольких местах. Если интерпретатор оказывается внутри одного из этих вызовов, когда SIGCHLD прибывает, код клиентской библиотеки MySQL возобновляется неожиданно и много раз заканчивается ошибочным статусом «Сервер MySQL ушел (ошибка 2006)».

Возможно, что ваш код игнорирует (или проглатывает) состояние ошибки MySQL (потому что он не ожидает, что это произойдет в этом месте). Мой не сделал, и я провел несколько дней расследования, чтобы узнать факты, изложенные выше.

Решение

Решение проблемы легко (после того, как вы узнаете все внутренние детали, представленные выше). Это намечено в приведенной выше документации: «сигнал поступает, который не игнорируется» .

Сигналы могут маскироваться (игнорироваться), когда их прибытие нежелательно. Расширение PHP PCNTL предоставляет функцию pcntl_sigprocmask() . Он завершает системный вызов sigprocmask() Linux, который устанавливает, какие сигналы могут быть получены программой с этого момента (на самом деле, какие сигналы блокируются).

Есть две стратегии, которые вы можете реализовать, в зависимости от того, что вам нужно.

Если вашей программе необходимо обмениваться данными с базой данных и получать уведомление о завершении обработки дочернего процесса, вы должны перенести все вызовы базы данных в пару вызовов pcntl_sigprocmask() чтобы блокировать, затем разблокировать сигнал SIGCHLD .

Если вам все равно, когда дочерние процессы завершаются, вы просто вызываете:

 pcntl_sigprocmask(SIG_BLOCK, array(SIGCHLD)); 

прежде чем вы начнете создавать любой дочерний процесс (до момента while() ). Это заставляет процесс игнорировать завершение дочерних процессов и позволяет запускать запросы к базе данных без нежелательного прерывания.

Предупреждение

Обработка по умолчанию сигнала SIGCHLD – это вызов wait() , чтобы позволить очистке системы после завершения дочернего процесса. Что происходит, если сигнал не обрабатывается (поскольку его доставка заблокирована) объясняется в документации wait() :

Ребенок, который заканчивается, но не ждал, становится «зомби». Ядро хранит минимальный набор информации о процессе зомби (PID, статус завершения, информацию об использовании ресурсов), чтобы позволить родительскому лицу позже выполнить ожидание получения информации о ребенке. До тех пор, пока зомби не будет удален из системы через ожидание, он будет использовать слот в таблице процессов ядра, и если эта таблица заполнится, невозможно будет создать дополнительные процессы. Если родительский процесс завершается, то его «зомбические» дети (если есть) принимаются init(1) , что автоматически выполняет ожидание для удаления зомби.

На простом английском языке, если вы заблокируете прием сигнала SIGCHLD , вам нужно вызвать pcntl_wait() , чтобы pcntl_wait() дочерние процессы зомби.

Можете добавить:

 pcntl_wait($status, WNOHANG); 

где-то внутри цикла while (как раз перед тем, как он заканчивается, например).

сценарий выходит без какой-либо ошибки или вывода

Не удивительно, когда в коде отсутствует проверка ошибок. Однако, если это действительно «сбой», тогда:

  • если причина ловушки среды выполнения PHP, то она будет пытаться зарегистрировать ошибку. Пробовали ли вы попытаться создать сценарий ошибок, чтобы изменить, что реортонирование / ведение журнала работает так, как вы ожидаете?

  • если ошибка не застряла в среде выполнения PHP, ОС должна сбрасывать основной файл – вы проверили конфигурацию ОС ? Посмотрел основной файл? Проанализировали его ?

$ cmd = "nohup php $ filePath> /tmp/1.log 2> & 1 &";

Вероятно, это не так, как вы думаете. Когда вы запускаете процесс в фоновом режиме с большинством версий nohup, он по-прежнему сохраняет связь с родительским процессом; родитель не может быть извлечен до тех пор, пока дочерний процесс не выйдет – и ребенок всегда порождает другого ребенка, прежде чем он это сделает.

Это недействительный способ сохранить код в фоновом режиме / в качестве демона. Какой правильный подход зависит от того, чего вы пытаетесь достичь. Есть ли конкретная причина для попытки возобновить процесс каждые 60 секунд?

(Вы никогда явно не закрываете соединение с базой данных – это меньше проблема, поскольку PHP должен делать это при вызове exit ).

Вы можете прочитать это и это

Я предлагаю, чтобы процесс не вышел после pclose. В этом случае каждый процесс имеет собственное соединение с db. По прошествии некоторого времени достигается ограничение доступа к MySQL, а новое соединение терпит неудачу. Чтобы понять, что происходит, добавьте несколько журналов до и после строк DatabaseConnector::Init(); и pclose($p);