Проблемы «долгих» скриптов PHP

Иногда возникает необходимость писать скрипты, работа которых занимает продолжительное время. Например, скрипты создания/развертывания бэкапов, установки демо-версии какого-то приложения, агрегирования больших объемов данных, импорта/экспорта данных и т.п. Для того, чтобы такие скрипты не прекращали свою работу в неожиданный момент, нужно знать и помнить о некоторых вещах.

Внешний таймаут


В первую очередь нужно установить подходящее значение параметра max_execution_time в конфиге PHP.

Если скрипт запускается веб-сервером (т.е. в ответ на HTTP-запрос от пользователя), то следует также правильно настроить параметры таймаута в конфиге веб-сервера. Для apache это параметры TimeOut и FastCgiServer… -idle-timeout ... (если PHP работает через FastCGI), для nginx send_timeout и fastcgi_read_timeout (если PHP работает через FastCGI).

Веб-сервер может также проксировать запросы на другой веб-сервер, который и запустит PHP скрипт (не редкий пример, nginx — фронтенд, apache — бэкэнд). В этом случае на проксирующем веб-сервере необходимо также настраивать таймаут проксирования. Для apache ProxyTimeout, для nginx proxy_read_timeout.

Прерывание пользователем


Если скрипт запускается в ответ на HTTP-запрос, то пользователь может остановить выполнение запроса в своем браузере, в этом случае прекратит свою работу и PHP скрипт. Если же требуется, чтобы скрипт продолжил свою работу даже после остановки запроса, установите в TRUE параметр ignore_user_abort в конфиге PHP.

Потеря открытых соединений


Если в скрипте открывается соединение с каким-либо сервисом/службой (с БД, с почтовым сервером, с FTP-сервером, ...), и во время выполнения скрипта некоторое время соединение не используется, то оно может быть закрыто этим сервисом. Например, если во время работы скрипта некоторое время не выполнять запросы к MySQL, то MySQL закроет соединение через время, заданное в параметре wait_timeout. Как следствие, при попытке выполнить очередной запрос возникнет ошибка.

В таких случаях следует в первую очередь попробовать увеличить таймаут соединения. Например, для MySQL можно выполнить запрос (спасибо Snowly)
SET SESSION wait_timeout = 9999

Если же такой возможности нет или этот вариант по каким то причинам не подходит, то можно проверять активность соединения, в тех местах кода, где возможны простои его использования, и переподключаться при необходимости. Например в модуле MySQLi есть полезная функция mysqli::ping для проверки активности соединения, а также параметр конфигурации mysqli.reconnect для автоматического переподключения, при разрыве соединения. При отсутствии подобных функций для других видов соединений, можно попробовать написать ее самому. В ней нужно тривиальным образом обратиться к сервису и в случае ошибки (отловить при помощи try… catch ...) переподключиться. Например
class FtpConnection
{
	private $ftp;

	public function connect()
	{
		$this->ftp = ftp_connect('ftp.server');
		...
	}

	public function reconnect()
	{
		try
		{
			if (!ftp_pwd($this->ftp))
				$this->connect();
		}
		catch($e)
		{
			$this->connect();
		}
	}

	...
}

или
class MssqlConnection
{
	private $db;

	public function connect()
	{
		$this->db = mssql_connect('mssql.server');
		...
	}
	
	public function reconnect()
	{
		try
		{
			if (!mssql_query('SELECT 1 FROM dual', $this->db))
				$this->connect();
		}
		catch($e)
		{
			$this->connect();
		}
	}

	...
}


Параллельный запуск


Нередко долгие скрипты запускаются по расписанию (по cron), и ожидается, что в один момент времени будет работать только одна копия скрипта. Но может случиться так, что очередной запуск скрипта произойдет раньше, чем закончит работу предыдущий, и как правило это нежелательно (дважды импортируются одни и те же данные, затрутся данные используемые первым скриптом, ...).

В таких случаях можно использовать блокировку используемых ресурсов, но эта задача всегда решается индивидуально. Либо можно просто проверять, не запущена ли другая копия этого скрипта, и либо подождать завершения его работы, либо завершить текущий запуск. Для этого можно просматривать список запущенных процессов, либо использовать блокировку запуска самого скрипта, что то вроде:

if (lockStart('script.php'))
{
    // основной код скрипта
    ...
    lockStop('script.php');
}


Нагрузка на веб-сервер


В случаях, когда долгие скрипты запускаются через веб-сервер, соединение клиента с этим самым веб-сервером остается открытым до тех пор, пока не отработает скрипт. Это не есть хорошо, т.к. задача веб-сервера как можно быстрее обработать запрос и отдать результат. Если же соединение остается висеть, то один из воркеров (процессов) веб-сервера на долгое время будет занят. А если одновременно будет запущено достаточно много таких скриптов, то они могут занять все (ну или почти все) свободные воркеры (для apache см. MaxClients), и веб-сервер просто не сможет обрабатывать другие запросы.

Поэтому следует при обработке запроса пользователя, запускать скрипт в фоновом режиме через php-cli, чтобы не нагружать веб-сервер, а пользователю отвечать что его запрос обрабатывается. При необходимости можно периодически проверять состояние обработки при помощи AJAX запросов.

Вот, пожалуй, и все что я могу рассказать по этой теме. Надеюсь, для кого-то будет полезным.
Share post
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 34

    +16
    Проблема долгих скриптов PHP в том что их пишут «программисты» которые посмотрели 3 видеоурока и приступили к написанию своей CMS.
    **смайлик**

    Долгие операции нужно выносить за пределы, и выполнять их, например, в кроне.
    • UFO just landed and posted this here
        0
        Но они выполняются прозрачно для пользователя.
          +1
          поясните, пожалста, что вы имеете ввиду?
          в PHP параметр mysqli.reconnect по умолчанию отключен, а следовательно при потере соединения с MySQL оно не будет «прозрачно» восстановлено
          по поводу блокировки тоже не понятно
          0
          Таймаут соединения можно решить например через постоянные подключения.
            0
            Покажите где такое есть в PDO?
              0
              <?php
              $dbh = new PDO('mysql:host=localhost;dbname=test', $user, $pass, array(
                  PDO::ATTR_PERSISTENT => true
              ));
              ?>
              


              php.net/manual/en/pdo.connections.php
                +1
                Читайте внимательно
                Постоянные соединения не закрываются при завершении работы скрипта, они кэшируются и используются повторно, когда другой скрипт запрашивает соединение с теми же учетными данными

                К сожалению этот параметр не решает проблему сброса соединения со стороны MySQL сервера.
                  –1
                  Мое дело малое, показать как сделать в PDO персистентное соединение. А по поводу проблемы с таймаутами — не вижу проблемы как таковой. По умолчанию у mysql этот таймаут равен 8-ми часам если память не изменяет. С другой стороны я не вижу смысла какому-либо скрипту держать соединение с базой дольше чем минуту. Если у вас возникают проблемы с таймаутами — то скорее всего у вас просто что-то не так с реализацией задачи. Если нужно что-то долго обсчитывать в базе — открываем соединение — достаем данные — закрываем. Оно нам не понадобится пока не обсчитаем все. Можно сделать очередь задачи и все такое прочее.
                    0
                    Я вам А, вы мне В. Разговор был про таймаут, вы мне рассказывается про персистентное соединение, при чем тут это? Даже если все фоне то кого волнует что соединение будет персистентное? От того что оно закешируется для фоновой задачи вы где-то выиграете?
              +1
              Персистентные соединения вообще ничего связанного с таймаутами, отличное от обычных, неперсистентных соединений, не имеет.

              Персистентные соединения это лишь пул, который, в случае с кроновскими и шелл-скриптами вообще некому будет создать и поддерживать.
            0
            Я вел речь о запланированно долгих скриптах, а не о плохооптимизированных
            Долгие операции нужно выносить за пределы, и выполнять их, например, в кроне.
            вообще-то, в конце я написал о том, что долгие операции следует выполнять в фоновом режиме (в кроне это делать или запускать в качестве фонового процесса — зависит от задачи и особого значения не имеет)
              +2
              3 Видео Урока Евгения Попова… используя готовые скрипты на PHP с сайта woweb.ru :)
              +2
              В mysql может быть полезно установить таймауты больше, если предполагается что обработка займет продолжительное время.
              SET SESSION wait_timeout (ну и interactive_timeout тоже) = 9999
                0
                да, но не всегда есть возможность менять параметры MySQL
                  +5
                  To indicate explicitly that a variable is a session variable, precede its name by SESSION, @@session., or @@. Setting a session variable requires no special privilege, but a client can change only its own session variables, not those of any other client.

                  dev.mysql.com/doc/refman/5.0/en/using-system-variables.html
                    0
                    был неправ, спасибо за информацию
                    согласен, в первую очередь стоит попробовать увеличить таймаут соединения
                    внес поправку в статью
              • UFO just landed and posted this here
                  0
                  добавил
                    0
                    Тогда уж добавляйте, интересный для меня, пример к
                    При необходимости можно периодически проверять состояние обработки при помощи AJAX запросов.


                    И опишите «нюанс» с mysql_reconnect (этим я уже не пользуюсь, как и mysqli_*) когда он переподключается с параметрами «по умолчанию», а не с теми параметрами, с которыми он подключался.
                  0
                  Не хватает примера запуска скрипта через php-cli из кода.
                    0
                    для nginx send_timeout и fastcgi_read_timeout (если PHP работает через FastCGI)
                    Потрудитесь объяснить причем тут вообще send_timeout.

                    Если скрипт запускается в ответ на HTTP-запрос, то пользователь может остановить выполнение запроса в своем браузере, в этом случае прекратит свою работу и PHP скрипт.
                    Это, мягко говоря, не всегда так.
                      0
                      А не лучше сделать выполнения клиентской части на клиентской машине? В наше время уже давно используется Ajax…
                        0
                        К чему был ваш комментарий?
                        0
                        Потрудитесь объяснить причем тут вообще send_timeout.

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

                        На сколько я понимаю, если скрипт отправит что-то клиенту, после чего долгое время (больше чем указано в send_timeout) будет «молчать», то соединение закроется. Поправьте если я ошибаюсь

                        Это, мягко говоря, не всегда так.

                        Ну как я и написал за это в PHP отвечает параметр ignore_user_abort. Если вам есть что добавить, то я готов это добавить в статью.
                          +2
                          На сколько я понимаю, если скрипт отправит что-то клиенту, после чего долгое время (больше чем указано в send_timeout) будет «молчать», то соединение закроется. Поправьте если я ошибаюсь
                          Скрипт ничего клиенту не отправляет, этим в данном случае nginx занимается.

                          Фразу «клиент ничего не примет» следует понимать буквально: клиент не принял отправленных ему данных в течение заданного времени, nginx сделал write() в сокетный буфер, но последний так и остался в том же состоянии, нового события на запись не наступило. Ну а если все записанные данные в сокет успешно отправляются, то таймер выключается.

                          Ну как я и написал за это в PHP отвечает параметр ignore_user_abort. Если вам есть что добавить, то я готов это добавить в статью.
                          Я в том сообщении процитировал конкретное ваше заявление. Вы полагаете, что у сервера всегда есть возможность узнать о том, что клиент нажал кнопку stop в браузере и закрыл соединение (если вообще закрыл). Нет, часто это невозможно сделать, пока не начнешь писать в сокет, а писать нечего, пока ваш скрипт не вернул ответ. Более того, если вы ещё и кэш включите, то даже в случае когда закрытие соединения было четко детектировано, с точки зрения php ничего не произойдет, поскольку нам нужно заполнить ячейку кэша. Даже если кэш не используются, то могут быть включены опции: fastcgi_ignore_client_abort и proxy_ignore_client_abort. PHP не общается с клиентом напрямую, а полагается только на то, как поведет себя сервер приложений, который взаимодействует с веб-сервером, но даже у последнего не всегда есть достаточно информации о том, что проихошло на той стороне.

                          — Что же касается статьи в целом, то это отличный пример того, как не надо делать обработку долгих задач. Проблеск этого понимания промелькнул только в конце статьи, да и то был испорчен витиеватым и неубедительным объяснением, почему обработку следует производить в фоне. На первый абзац из «Нагрузка на веб-сервер», можно только ответить: для этого люди и используют nginx!

                          Обработку следует производить не просто в фоне, а последовательно, с использованием очереди заданий и менеджера, который эту очередь разгребает. Потому, что ваш сервер в принципе может быть не в состоянии обрабатывать параллельно то количество задач, которое на него поступило в данный момент. В целом он вряд ли способен обрабатывать их эффективно, если количество CPU-bound потоков привышает число имеющихся в распоряжении вычислительных ядер.

                          Ну и да, в статье полностью перемешаны в винегрет CPU-bound и I/O-bound, т.е. задачи, требующие разных подходов для их решения. Да и решать такие задачи на php можно наверное только если ничего больше не знать, что уже само по себе должно быть моветон.
                            +2
                            Два чая, этому господину…

                            Всё ждал, кто же наконец предложит использовать очереди?
                              0
                              Спасибо за критику, статья и вправду получилась немного сумбурной (мало опыта в написании статей). Но все же я не ставил своей целью описывать разные типы долгих задач и методы их решения. Я лишь хотел написать о факторах, которые могут помешать работе таких скриптов.

                              По поводу того, что не надо решать такие задачи на PHP, не соглашусь. Если у вас уже есть веб-приложение, написанное на PHP, со своими классами и библиотеками, и в нем вам нужно добавить, например, функцию импорта, в которой будут использоваться эти классы и библиотеки, то я не вижу смысла реализовывать ее на другом языке.
                                0
                                На чем реализовывать вопрос десятый. Дело в том как реализовывать. Долгие скрипты реализовывать как веб-страницы не тру.
                          +2
                          if (lockStart('script.php'))
                          {
                              // основной код скрипта
                              ...
                              lockStop('script.php');
                          }
                          

                          Старайтесь избегать подобных подходов. Любая ошибка или непредусмотренная ситуация в скрипте и вы попали, как говорится. Вообще ПХП мало предназначен для подобного и лучше как можно больше вещей доверять профессионалам — ОС.
                            0
                            lockStart($key, $params, $lock_set_timeout, $expire_sec);
                            lockStop($key, $params, $timeout);
                            prolong($key, $params, $time_sec);
                            Ну и какой-нибудь kill($key,...)
                            
                            +1
                            с mysql все не однозначно, если вы налетели на mysql has gone away в транзакции то реконектиться нельзя, т.к часть данных уже уже потеряли. те влоб использовать подход с реконектом незя

                              +2
                              У php-fpm есть замечательная штука fastcgi_finish_request()

                              А вообще, если у Вас очень долгий скрипт после HTTP запроса от пользователя, то это потенциальная угроза DoS атаки.
                                +1
                                Если скрипт запускается в ответ на HTTP-запрос, то пользователь может остановить выполнение запроса в своем браузере, в этом случае прекратит свою работу и PHP скрипт.

                                Это не совсем так. Не помню, как оно у апача, но в связке nginx + php-fpm от nginx команда уходит на php-fpm, и продолжает выполняться вне зависимости от того, стопнул юзер запрос или нет.

                                Only users with full accounts can post comments. Log in, please.