Лучший способ управлять длительным PHP скрипт?

У меня есть PHP script, который занимает много времени (5-30 минут). На всякий случай это важно, script использует завиток для очистки данных с другого сервера. Именно по этой причине он занимает так много времени; он должен дождаться загрузки каждой страницы до ее обработки и перехода к следующему.

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

Мне нужно знать, как можно завершить HTTP-запрос до завершения работы script. Кроме того, это лучший способ сделать это PHP скрипт?

Ответ 1

Конечно, это можно сделать с помощью 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');, но причины, по которым здесь немного времени для включения.

Ответ 2

Быстрый и грязный способ заключается в использовании функции ignore_user_abort в php. В основном это говорит: "Не важно, что делает пользователь, запустите этот script, пока он не будет завершен. Это несколько опасно, если это публичный сайт (потому что возможно, что вы закончите с 20 ++ версиями script, запущенных одновременно, если он инициирован 20 раз).

"Чистый" способ (по крайней мере, IMHO) заключается в установке флага (например, в db), когда вы хотите инициировать процесс и запускать cronjob каждый час (или так), чтобы проверить, установлен ли этот флаг. Если он установлен, запускается длительный script, если он НЕ установлен, ничего не происходит.

Ответ 3

Вы можете использовать exec или system, чтобы начать фоновое задание, а затем выполните эту работу.

Кроме того, есть более эффективные подходы к очистке сети, которую вы используете. Вы можете использовать поточный подход (несколько потоков, выполняющих одну страницу за раз), или один с помощью eventloop (один поток делает несколько страниц за раз). Мой личный подход с использованием Perl будет использовать AnyEvent:: HTTP.

ETA: symcbean объяснил, как правильно отделить фоновый процесс здесь.

Ответ 4

Нет, PHP не лучшее решение.

Я не уверен в Ruby или Perl, но с Python вы можете переписать свой скребок страницы на многопоточность, и он, вероятно, будет работать как минимум на 20 раз быстрее. Написание многопоточных приложений может быть чем-то сложным, но самое первое приложение Python, которое я написал, было скремблером страницы muttti. И вы можете просто вызвать Python script из вашей PHP-страницы, используя одну из функций выполнения оболочки.

Ответ 5

Да, вы можете сделать это на PHP. Но в дополнение к PHP было бы разумно использовать диспетчер очереди. Здесь стратегия:

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

  • Отправьте каждую маленькую задачу в очередь.

  • Запустите ваши рабочие места в очереди.

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

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

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

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

Ответ 6

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

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

Symcbean имеет несколько хороших советов о том, как управлять фоновыми процессами по его ссылке.

Короче говоря, напишите CLI PHP script для обработки длинных битов. Убедитесь, что он каким-то образом сообщает статус. Сделайте php-страницу для обработки обновлений состояния, используя AJAX или традиционные методы. Ваш запуск script запустит процесс, запущенный в его собственном сеансе, и вернет подтверждение, что процесс идет.

Удачи.

Ответ 7

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

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

Ответ 8

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

Ответ 9

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

<?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.';
}

Ответ 10

Я хотел бы предложить решение, немного отличающееся от symcbean, главным образом потому, что у меня есть дополнительное требование, чтобы длительный процесс нужно запускать как другой пользователь, а не как пользователь apache/www-data.

Первое решение с использованием cron для опроса таблицы фоновых задач:

  • Веб-страница PHP вставляется в таблицу фоновых задач, укажите "SUBMITTED"
  • cron запускается один раз каждые 3 минуты, используя другого пользователя, запуская PHP CLI script, который проверяет таблицу фоновых задач для строк "SUBMITTED"
  • PHP CLI обновит столбец состояния в строке в "ОБРАБОТКА" и начнет обработку, после завершения он будет обновлен до "COMPLETED"

Второе решение с использованием средства инициализации Linux:

  • Веб-страница PHP обновляет управляющий файл с параметрами, заданными пользователем, а также предоставляет идентификатор задачи
  • shell script (как пользователь, не являющийся пользователем), запущенный inotifywait, будет ждать записи файла управления
  • после того, как файл управления записан, событие close_write будет поднято, оболочка script продолжит
  • shell script выполняет PHP CLI для выполнения долгого процесса
  • PHP CLI записывает вывод в файл журнала, идентифицированный идентификатором задачи, или, как альтернатива, обновляет прогресс в таблице состояний
  • Веб-страница PHP могла опросить файл журнала (на основе идентификатора задачи), чтобы показать ход выполнения долгого процесса, или он также может запрашивать таблицу состояния

В моем посте можно найти дополнительную информацию: http://inventorsparadox.blogspot.co.id/2016/01/long-running-process-in-linux-using-php.html

Ответ 11

Я сделал аналогичные вещи с Perl, double fork() и отсоединением от родительского процесса. Вся работа по настройке HTTP должна выполняться в разветвленном процессе.

Ответ 12

Используйте прокси для делегирования запроса.

Ответ 13

то, что я ВСЕГДА использую, является одним из этих вариантов (поскольку разные варианты 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 AND перенаправил вывод.

Ответ 14

если у вас длинный script, затем разделите работу страницы с помощью параметра ввода для каждой задачи. (тогда каждая страница действует как поток) если страница имеет 1 lac 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-адреса в виде цикла, выполните следующий ответ: fooobar.com/questions/49521/...

Ответ 15

Не лучший подход, как многие здесь заявили, но это может помочь:

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

Ответ 16

Я не могу комментировать, но заметил, что решение Francisco всегда будет возвращать только первый URL, так как return $ url; немедленно выйдет из функции.