Лучший способ управления длительным PHP скриптом?
У меня есть php-скрипт, который занимает много времени (5-30 минут) для завершения. На всякий случай, если это имеет значение, скрипт использует curl для очистки данных с другого сервера. Именно по этой причине он занимает так много времени; он должен ждать загрузки каждой страницы, прежде чем обрабатывать ее и переходить к следующему.
Я хочу иметь возможность инициировать скрипт и пусть это будет до тех пор, пока это не будет сделано, что установит флаг в таблице базы данных.
Что мне нужно знать, так это как завершить http запрос до завершения работы скрипта. Кроме того, является ли PHP-скрипт лучшим способом сделать это?
14 ответов:
конечно, это можно сделать с 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) - установить флаг (например, в БД), когда вы хотите инициировать процесс и запустить расписанию каждый час (или около того), чтобы проверить, если флаг установлен. Если он установлен, запускается долго работающий скрипт, если он не установлен, ничего не происходит.
вы могли бы использовать exec или система чтобы начать фоновое задание, а затем выполнить работу в этом.
кроме того, есть лучшие подходы к выскабливанию сети, которую вы используете. Вы можете использовать потоковый подход (несколько потоков, выполняющих одну страницу за раз) или один с помощью eventloop (один поток, выполняющий несколько страниц одновременно). Мой личный подход с использованием Perl будет использовать AnyEvent:: HTTP.
ETA: symcbean объяснил, как правильно отсоединить фоновый процесс здесь.
нет, PHP-это не лучшее решение.
Я не уверен в Ruby или Perl, но с Python вы можете переписать свой скребок страницы, чтобы быть многопоточным, и он, вероятно, будет работать как минимум в 20 раз быстрее. Написание многопоточных приложений может быть несколько сложной задачей, но самое первое приложение Python, которое я написал, было mutlti-threaded page scraper. И вы можете просто вызвать скрипт Python из своей страницы PHP, используя одну из функций выполнения оболочки.
PHP может быть или не быть лучшим инструментом, но вы знаете, как его использовать, и остальная часть вашего приложения написана с его помощью. Эти два качества в сочетании с тем, что PHP "достаточно хорош", делают его довольно сильным аргументом для использования вместо Perl, Ruby или Python.
Если ваша цель состоит в том, чтобы выучить другой язык, а затем выбрать один и использовать его. Любой язык, который вы упомянули, сделает эту работу, без проблем. Мне нравится Perl, но то, что вам нравится может быть отличающийся.
У Symcbean есть несколько хороших советов о том, как управлять фоновыми процессами по его ссылке.
короче говоря, напишите скрипт PHP CLI для обработки длинных битов. Убедитесь, что он сообщает статус в некотором роде. Сделайте страницу php для обработки обновлений состояния, используя AJAX или традиционные методы. Ваш стартовый скрипт запустит процесс, запущенный в своем собственном сеансе, и вернет подтверждение того, что процесс идет.
удачи.
Да, вы можете сделать это в PHP. Но в дополнение к PHP было бы разумно использовать администратор очередей. Вот стратегия:
разбить большую задачу на более мелкие задачи. В вашем случае каждая задача может загружать одну страницу.
отправить каждую небольшую задачу в очередь.
запустите ваших работников очереди где-нибудь.
использование этой стратегии имеет следующее преимущества:
для длительных задач он имеет возможность восстановления в случае возникновения фатальной проблемы в середине выполнения - нет необходимости начинать с самого начала.
Если ваши задачи не должны выполняться последовательно, вы можете запустить несколько рабочих для одновременного выполнения задач.
У вас есть множество вариантов (это всего лишь несколько):
- 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 запроса на запуск процесса, вы можете сохранить в базе данных представление задачи с уникальным идентификатором. Затем запустите процесс очистки экрана, передав ему уникальный идентификатор. Доложить приложение для iPhone, что задача была запущена и что он должен проверить указанный URL-адрес, содержащий новый идентификатор задачи, чтобы получить последний статус. Приложение iPhone теперь может опрашивать (или даже "длинный опрос") этот URL. В то же время фоновый процесс обновит представление базы данных задачи, поскольку она работает с процентом завершения, текущим шагом или любыми другими индикаторами состояния, которые вы хотите. И когда он закончит, он установит завершенный флаг.
вы можете отправить его как запрос XHR (Ajax). Клиенты обычно не имеют таймаута для XHRs, в отличие от обычных HTTP-запросов.
Я понимаю, что это довольно старый вопрос, но хотел бы дать ему выстрел. Этот скрипт пытается решить как начальный вызов kick off, чтобы быстро закончить, так и сократить тяжелую нагрузку на более мелкие куски. Я не тестировал это решение.
<?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.'; }
Я хотел бы предложить решение, которое немного отличается от symcbean, главным образом потому, что у меня есть дополнительное требование, что длительный процесс должен запускаться как другой пользователь, а не как пользователь apache / www-data.
первое решение с помощью cron для опроса таблицы фоновых задач:
- веб-страница PHP вставляется в фоновую таблицу задач, состояние "отправлено"
- cron запускается один раз каждые 3 минуты, используя другого пользователя, запускающего скрипт php CLI, который проверяет фоновую таблицу задач на наличие "отправленных" строк
- PHP CLI обновит столбец состояния в строке в "обработка" и начнет обработку, после завершения он будет обновлен до "завершено"
второе решение с использованием Linux inotify facility:
- веб-страница 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
Я делал подобные вещи с Perl, double fork () и отсоединением от родительского процесса. Вся работа по извлечению http должна выполняться в раздвоенном процессе.
то, что я всегда использую, является одним из этих вариантов (потому что разные варианты Linux имеют разные правила обработки вывода/некоторые программы выводятся по-разному):
Вариант I @выполнение.'(/мой_сценарий.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 и перенаправил выход.
Если у вас длинный скрипт, то разделите работу страницы с помощью входного параметра для каждой задачи.(тогда каждая страница действует как поток) т. е. если страница имеет 1 lac product_keywords длинный цикл процесса, то вместо цикла сделайте логику для одного ключевого слова и передайте это ключевое слово из magic или cornjobpage.PHP(в следующем примере)
и фоновый поток worker, я думаю, вы должны попробовать эту технику, это поможет вызвать столько страниц, как все страницы будут работать сразу самостоятельно без ожидание ответа каждой страницы в асинхронном режиме.
cornjobpage.php / / mainpage
<?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 как цикл, то следуйте этому ответу:https://stackoverflow.com/a/41225209/6295712