Лучший способ управления длительным 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 было бы разумно использовать администратор очередей. Вот стратегия:

  1. разбить большую задачу на более мелкие задачи. В вашем случае каждая задача может загружать одну страницу.

  2. отправить каждую небольшую задачу в очередь.

  3. запустите своих работников очереди где-нибудь.

использование этой стратегии имеет следующее преимущества:

  1. для длительных задач он имеет возможность восстановить в случае фатальной проблемы происходит в середине запуска - нет необходимости начинать с самого начала.

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

У вас есть множество вариантов (это всего лишь несколько):

  1. в RabbitMQ (https://www.rabbitmq.com/tutorials/tutorial-one-php.html)
  2. ZeroMQ (http://zeromq.org/bindings:php)
  3. Если вы используете Laravel framework, очереди являются встроенными (https://laravel.com/docs/5.4/queues), с драйверами для AWS SES, Redis, Beanstalkd

Я согласен с ответами, которые говорят, что это должно быть запущено в фоновом процессе. Но также важно, чтобы вы сообщали о состоянии, чтобы пользователь знал, что работа выполняется.

при получении запроса PHP для запуска процесса вы можете сохранить в базе данных представление задачи с уникальным идентификатором. Затем запустите процесс очистки экрана, передав ему уникальный идентификатор. Сообщите приложению iPhone, что задача была запущена и что он должен проверить указанный URL-адрес, содержащий новый идентификатор задачи, чтобы получить последний статус. Приложение iPhone теперь может опросить (или даже "длинный опрос") этот URL. В то же время фоновый процесс обновит представление базы данных задачи по мере ее работы с процентом завершения, текущим шагом или любыми другими индикаторами состояния, которые вы хотите. И когда он закончит, он установит завершенный флаг.


вы можете отправить его как запрос XHR (Ajax). Клиенты обычно не имеют тайм-аута для XHRs, в отличие от обычных HTTP-запросов.


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

<?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 веб-страница обновляет файл управления с параметрами, заданными пользователем, а также дает id задачи
  • сценарий оболочки (как пользователь без www), работающий inotifywait, будет ждать элемента управления файл для записи
  • после записи контрольного файла будет вызвано событие close_write, а сценарий оболочки продолжит
  • Shell script выполняет 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 лак product_keywords процесс длительный цикл, то вместо петли сделать логику для одного сайта и сайта от магии или cornjobpage.PHP(в следующем примере)

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

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