Лучший способ управлять долгосрочным PHP-скриптом?
У меня есть скрипт PHP, который занимает много времени (5-30 минут), чтобы завершить. На случай, если это имеет значение, скрипт использует curl для очистки данных с другого сервера. Это причина, по которой это занимает так много времени; он должен ждать загрузки каждой страницы, прежде чем обрабатывать ее и переходить к следующей.
Я хочу иметь возможность инициировать сценарий и позволить ему быть, пока это не будет сделано, который установит флаг в таблице базы данных.
Что мне нужно знать, так это как можно завершить http-запрос до завершения работы скрипта. Кроме того, является ли PHP-скрипт лучшим способом сделать это?
15 ответов
Конечно, это может быть сделано с помощью PHP, однако вы НЕ должны делать это как фоновую задачу - новый процесс должен быть отделен от группы процессов, в которой он инициирован.
Поскольку люди продолжают давать один и тот же неправильный ответ на этот FAQ, я написал более полный ответ здесь:
http://symcbean.blogspot.com/2010/02/php-and-long-running-processes.html
Из комментариев:
Короткая версия
shell_exec('echo /usr/bin/php -q longThing.php | at now');
но причины, по которым здесь много времени для включения
Быстрый и грязный способ будет использовать ignore_user_abort
функция в php. Это в основном говорит: не волнует, что делает пользователь, запускайте этот скрипт, пока он не закончится. Это несколько опасно, если это общедоступный сайт (поскольку возможно, что в итоге вы запустили одновременно 20+ версий скрипта, если он был запущен 20 раз).
"Чистый" способ (по крайней мере, IMHO) - установить флаг (например, в БД), когда вы хотите запустить процесс и запускать cronjob каждый час (или около того), чтобы проверить, установлен ли этот флаг. Если он установлен, запускается длительный скрипт, если он НЕ установлен, ничего не происходит.
Вы можете использовать exec или system, чтобы запустить фоновую работу, а затем выполнить работу в ней.
Кроме того, есть лучшие подходы к очистке сети, чем тот, который вы используете. Вы можете использовать многопоточный подход (несколько потоков делают по одной странице за раз) или один, использующий Eventloop (один поток делает несколько страниц одновременно). Мой личный подход с использованием Perl будет использовать AnyEvent:: HTTP.
ETA: symcbean объяснил, как правильно отделить фоновый процесс здесь.
Да, вы можете сделать это на PHP. Но помимо PHP было бы разумно использовать Queue Manager. Вот стратегия:
Разбейте свою большую задачу на меньшие задачи. В вашем случае каждая задача может загружать одну страницу.
Отправьте каждое небольшое задание в очередь.
Запустите своих рабочих очереди где-нибудь.
Использование этой стратегии имеет следующие преимущества:
Для долго выполняющихся задач у него есть возможность восстановиться в случае фатальной проблемы в середине выполнения - не нужно начинать с самого начала.
Если ваши задачи не нужно запускать последовательно, вы можете запустить несколько рабочих для одновременного запуска задач.
У вас есть различные варианты (это только несколько):
- RabbitMQ ( https://www.rabbitmq.com/tutorials/tutorial-one-php.html)
- ZeroMQ ( http://zeromq.org/bindings:php)
- Если вы используете инфраструктуру Laravel, очереди встроены ( https://laravel.com/docs/5.4/queues) с драйверами для AWS SES, Redis, Beanstalkd
Нет, PHP не лучшее решение.
Я не уверен насчет Ruby или Perl, но с Python вы можете переписать свой скребок страниц, чтобы он был многопоточным, и он, вероятно, работал бы как минимум в 20 раз быстрее. Написание многопоточных приложений может быть довольно сложной задачей, но самое первое написанное мною приложение на Python было многопоточным скребком страниц. И вы можете просто вызвать скрипт Python из вашей страницы PHP, используя одну из функций выполнения оболочки.
PHP может или не может быть лучшим инструментом, но вы знаете, как его использовать, и остальная часть вашего приложения написана с его использованием. Эти два качества в сочетании с тем фактом, что PHP "достаточно хорош", дают веские основания использовать его вместо Perl, Ruby или Python.
Если ваша цель - выучить другой язык, выберите один и используйте его. Любой язык, который вы упомянули, сделает работу, без проблем. Мне нравится Perl, но то, что тебе нравится, может быть другим.
У Symcbean есть несколько полезных советов о том, как управлять фоновыми процессами.
Короче говоря, напишите PHP-скрипт CLI для обработки длинных битов. Убедитесь, что он сообщает о состоянии каким-либо образом. Создайте страницу php для обработки обновлений статуса, используя AJAX или традиционные методы. Ваш стартовый скрипт запустит процесс, запущенный в своем собственном сеансе, и вернет подтверждение того, что процесс идет.
Удачи.
Вы можете отправить его как запрос XHR (Ajax). У клиентов обычно нет тайм-аута для XHR, в отличие от обычных HTTP-запросов.
Я хотел бы предложить решение, которое немного отличается от Symcbean, главным образом потому, что у меня есть дополнительное требование, чтобы длительный процесс запускался как другой пользователь, а не как пользователь apache / www-data.
Первое решение с использованием cron для опроса таблицы фоновых задач:
- Веб-страница PHP вставляется в таблицу фоновых задач, состояние "ОТПРАВЛЕНО"
- cron запускается один раз каждые 3 минуты, используя другого пользователя, и запускает скрипт PHP CLI, который проверяет таблицу фоновых задач на наличие строк "SUBMITTED"
- PHP CLI обновит столбец состояния в строке до "ОБРАБОТКА" и начнет обработку, после завершения он будет обновлен до "ЗАВЕРШЕНО"
Второе решение с использованием средства Linux inotify:
- Веб-страница PHP обновляет управляющий файл с параметрами, установленными пользователем, а также дает идентификатор задачи
- Сценарий оболочки (как пользователь без www), выполняющий inotifywait, будет ожидать записи управляющего файла
- после записи управляющего файла будет вызвано событие close_write и сценарий оболочки продолжит работу
- сценарий оболочки выполняет PHP CLI, чтобы сделать длительный процесс
- PHP CLI записывает вывод в файл журнала, идентифицируемый по идентификатору задачи, или, в качестве альтернативы, обновляет прогресс в таблице состояния
- Веб-страница PHP может опрашивать файл журнала (на основе идентификатора задачи), чтобы показать прогресс долгого процесса, или она также может запросить таблицу состояния
Некоторая дополнительная информация может быть найдена в моем посте: http://inventorsparadox.blogspot.co.id/2016/01/long-running-process-in-linux-using-php.html
Я согласен с ответами, которые говорят, что это должно выполняться в фоновом режиме. Но также важно, чтобы вы сообщали о состоянии, чтобы пользователь знал, что работа выполняется.
Получив PHP-запрос на запуск процесса, вы можете сохранить в базе данных представление задачи с уникальным идентификатором. Затем запустите процесс очистки экрана, передав ему уникальный идентификатор. Сообщите в приложение для iPhone, что задача была запущена и что она должна проверить указанный URL-адрес, содержащий новый идентификатор задачи, чтобы получить последний статус. Приложение iPhone теперь может опрашивать (или даже "долго опрашивать") этот URL. В то же время фоновый процесс обновляет представление задачи в базе данных, поскольку он работает с процентом завершения, текущим шагом или любыми другими индикаторами состояния, которые вы хотите. И когда он закончится, он установит флаг завершения.
Я понимаю, что это довольно старый вопрос, но хотел бы дать ему шанс. Этот сценарий пытается обработать как начальный стартовый вызов, чтобы быстро завершить его и разделить тяжелую нагрузку на более мелкие куски. Я не проверял это решение.
<?php
/**
* crawler.php located at http://mysite.com/crawler.php
*/
// Make sure this script will keep on runing after we close the connection with
// it.
ignore_user_abort(TRUE);
function get_remote_sources_to_crawl() {
// Do a database or a log file query here.
$query_result = array (
1 => 'http://exemple.com',
2 => 'http://exemple1.com',
3 => 'http://exemple2.com',
4 => 'http://exemple3.com',
// ... and so on.
);
// Returns the first one on the list.
foreach ($query_result as $id => $url) {
return $url;
}
return FALSE;
}
function update_remote_sources_to_crawl($id) {
// Update my database or log file list so the $id record wont show up
// on my next call to get_remote_sources_to_crawl()
}
$crawling_source = get_remote_sources_to_crawl();
if ($crawling_source) {
// Run your scraping code on $crawling_source here.
if ($your_scraping_has_finished) {
// Update you database or log file.
update_remote_sources_to_crawl($id);
$ctx = stream_context_create(array(
'http' => array(
// I am not quite sure but I reckon the timeout set here actually
// starts rolling after the connection to the remote server is made
// limiting only how long the downloading of the remote content should take.
// So as we are only interested to trigger this script again, 5 seconds
// should be plenty of time.
'timeout' => 5,
)
));
// Open a new connection to this script and close it after 5 seconds in.
file_get_contents('http://' . $_SERVER['HTTP_HOST'] . '/crawler.php', FALSE, $ctx);
print 'The cronjob kick off has been initiated.';
}
}
else {
print 'Yay! The whole thing is done.';
}
ВСЕГДА я использую один из этих вариантов (потому что разные версии Linux имеют разные правила обработки вывода / некоторые программы выводят по-разному):
Вариант I @exec ('./ myscript.php \ 1> / dev / null \ 2> / dev / null &');
Вариант II @exec ('php -f myscript.php \ 1> / dev / null \ 2> / dev / null &');
Вариант III @exec ('nohup myscript.php \ 1> / dev / null \ 2> / dev / null &');
Возможно, вам не нужно устанавливать "nohup". Но, например, когда я автоматизировал видеосвязи FFMPEG, интерфейс вывода как-то не обрабатывался на 100% путем перенаправления потоков вывода 1 и 2, поэтому я использовал nohup И перенаправил вывод.
Не лучший подход, как многие здесь заявили, но это может помочь:
ignore_user_abort(1); // run script in background even if user closes browser
set_time_limit(1800); // run it for 30 minutes
// Long running script here
Если у вас длинный сценарий, то делите работу страницы с помощью входного параметра для каждой задачи.(тогда каждая страница действует как нить), то есть, если страница имеет длинный цикл обработки lac product_keywords, то вместо цикла создайте логику для одного ключевого слова и передайте это ключевое слово из magic или cornjobpage.php(в следующем примере)
и для фонового работника я думаю, что вы должны попробовать эту технику, это поможет вызвать столько страниц, сколько вам нужно, все страницы будут работать одновременно независимо, не дожидаясь ответа каждой страницы как асинхронного.
cornjobpage.php // главная страница
<?php
post_async("http://localhost/projectname/testpage.php", "Keywordname=testValue");
//post_async("http://localhost/projectname/testpage.php", "Keywordname=testValue2");
//post_async("http://localhost/projectname/otherpage.php", "Keywordname=anyValue");
//call as many as pages you like all pages will run at once independently without waiting for each page response as asynchronous.
?>
<?php
/*
* Executes a PHP page asynchronously so the current page does not have to wait for it to finish running.
*
*/
function post_async($url,$params)
{
$post_string = $params;
$parts=parse_url($url);
$fp = fsockopen($parts['host'],
isset($parts['port'])?$parts['port']:80,
$errno, $errstr, 30);
$out = "GET ".$parts['path']."?$post_string"." HTTP/1.1\r\n";//you can use POST instead of GET if you like
$out.= "Host: ".$parts['host']."\r\n";
$out.= "Content-Type: application/x-www-form-urlencoded\r\n";
$out.= "Content-Length: ".strlen($post_string)."\r\n";
$out.= "Connection: Close\r\n\r\n";
fwrite($fp, $out);
fclose($fp);
}
?>
testpage.php
<?
echo $_REQUEST["Keywordname"];//case1 Output > testValue
?>
PS: если вы хотите отправить параметры URL как цикл, следуйте этому ответу: /questions/8462430/kak-zapustit-php-kod-asinhronno/8462441#8462441
Если желаемый результат вашего скрипта - это некоторая обработка, а не веб-страница, то я считаю, что желаемое решение - запустить ваш скрипт из оболочки, просто как
php my_script.php
Я делал подобные вещи с Perl, double fork() и отсоединением от родительского процесса. Вся работа по извлечению http должна выполняться в раздвоенном процессе.