Запустить задачу PHP асинхронно - программирование

Запустить задачу PHP асинхронно

Я работаю над несколько большим веб-приложением, а бэкэнд - в основном на PHP. В коде есть несколько мест, где мне нужно выполнить некоторую задачу, но я не хочу, чтобы пользователь дождался результата. Например, при создании новой учетной записи мне нужно отправить им приветственное письмо. Но когда они нажимают кнопку "Финальная регистрация", я не хочу, чтобы они дождались отправки сообщения электронной почты, я просто хочу запустить процесс и сразу же отправить сообщение пользователю.

До сих пор, в некоторых местах, я использовал то, что было похоже на взлом с exec(). В основном делайте такие вещи, как:

exec("doTask.php $arg1 $arg2 $arg3 >/dev/null 2>&1 &");

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

Я заново изобретаю колесо? Есть ли лучшее решение, чем exec() взломать или очередь MySQL?

4b9b3361

Ответ 1

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

Роллинг не слишком сложный, вот несколько других вариантов:

  • GearMan - этот ответ был написан в 2009 году, и с тех пор GearMan выглядит популярным вариантом, см. комментарии ниже.
  • ActiveMQ, если вы хотите, чтобы полная очередь сообщений с открытым исходным кодом.
  • ZeroMQ - это довольно классная библиотека сокетов, которая позволяет легко писать распределенный код, не беспокоясь о сокетке само программирование. Вы можете использовать его для очередности сообщений на одном хосте - вы просто попросите своего webapp нажать что-нибудь в очередь, что непрерывное консольное приложение будет потреблять при следующей подходящей возможности.
  • beanstalkd - нашел это только при написании этого ответа, но выглядит интересно
  • dropr - это проект очереди сообщений на основе PHP, но не поддерживается с сентября 2010 года
  • php-enqueue - это недавно (2017) поддерживаемая оболочка вокруг множества систем очередей
  • Наконец, сообщение в блоге об использовании memcached для очереди сообщений

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

Ответ 2

Если вы просто хотите выполнить один или несколько HTTP-запросов, не дожидаясь ответа, есть и простое решение PHP.

В вызывающем script:

$socketcon = fsockopen($host, 80, $errno, $errstr, 10);
if($socketcon) {   
   $socketdata = "GET $remote_house/script.php?parameters=... HTTP 1.1\r\nHost: $host\r\nConnection: Close\r\n\r\n";      
   fwrite($socketcon, $socketdata); 
   fclose($socketcon);
}
// repeat this with different parameters as often as you like

В вызываемом script.php вы можете вызвать эти функции PHP в первых строках:

ignore_user_abort(true);
set_time_limit(0);

Это приводит к тому, что script продолжает работать без ограничения времени, когда соединение HTTP закрывается.

Ответ 3

Другим способом для процессов вилки является завиток. Вы можете настроить свои внутренние задачи как веб-сервис. Например:

Затем в ваших пользовательских сценариях вызовы выполняются:

$service->addTask('t1', $data); // post data to URL via curl

Ваша служба может отслеживать очередь задач с помощью mysql или всего, что вам нравится, так это: все это завершено в службе, а ваш script - просто потребляет URL-адреса. Это освобождает вас от необходимости переместить службу на другой компьютер/сервер (т.е. Легко масштабируется).

Добавление http-авторизации или пользовательской схемы авторизации (например, веб-сервисов Amazon) позволяет вам открывать ваши задачи, которые будут потребляться другими людьми/службами (если хотите), и вы можете принять их дальше и добавить службу мониторинга сверху вниз отслеживать состояние очереди и задачи.

Требуется небольшая работа по настройке, но есть много преимуществ.

Ответ 4

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

Несколько вещей, которые я сделал с этим:

  • Изменение размера изображения - и с легко загруженной очередью, передаваемой на CLI на основе PHP script, изменение размера (2 МБ +) изображений работало просто отлично, но попытка изменить размер одинаковых изображений в экземпляре mod_php регулярно запускалась в память -пространственные проблемы (я ограничил процесс PHP до 32 МБ, а изменение размера заняло больше)
  • проверки ближайшего будущего - beanstalkd имеет задержек, доступных для него (сделать это задание доступным только после X секунд) - поэтому я могу запустить 5 или 10 проверок для события, немного позже во времени

Я написал систему на основе Zend-Framework, чтобы декодировать "хороший" URL-адрес, например, чтобы изменить размер изображения, которое он назвал бы QueueTask('/image/resize/filename/example.jpg'). Сначала URL был декодирован в массив (модуль, контроллер, действие, параметры), а затем преобразован в JSON для вставки в очередь.

Долгое время cli script затем взял задание из очереди, запустил его (через Zend_Router_Simple) и, если необходимо, поместил информацию в memcached для веб-сайта PHP, чтобы поднять его, когда это необходимо.

Одна морщина, которую я также вложил, заключалась в том, что cli- script выполнял только 50 циклов перед перезапуском, но если бы он захотел перезапустить, как планировалось, он немедленно выполнил бы это (запускается через bash - script). Если возникла проблема, и я сделал exit(0) (значение по умолчанию для exit; или die();), он сначала приостанавливается на пару секунд.

Ответ 5

Если речь идет о предоставлении дорогостоящих задач, в случае поддержки php-fpm, почему бы не использовать fastcgi_finish_request()?

Эта функция удаляет все данные ответа клиенту и завершает запрос. Это позволяет выполнять трудоемкие задачи, не оставляя открытое соединение с клиентом.

Вы не используете асинхронность таким образом:

  • Сначала сделайте весь свой основной код.
  • Выполнить fastcgi_finish_request().
  • Сделайте все тяжелое дело.

Еще раз нужно php-fpm.

Ответ 6

Вот простой класс, который я кодировал для своего веб-приложения. Это позволяет использовать PHP-скрипты и другие скрипты. Работает в UNIX и Windows.

class BackgroundProcess {
    static function open($exec, $cwd = null) {
        if (!is_string($cwd)) {
            $cwd = @getcwd();
        }

        @chdir($cwd);

        if (strtoupper(substr(PHP_OS, 0, 3)) == 'WIN') {
            $WshShell = new COM("WScript.Shell");
            $WshShell->CurrentDirectory = str_replace('/', '\\', $cwd);
            $WshShell->Run($exec, 0, false);
        } else {
            exec($exec . " > /dev/null 2>&1 &");
        }
    }

    static function fork($phpScript, $phpExec = null) {
        $cwd = dirname($phpScript);

        @putenv("PHP_FORCECLI=true");

        if (!is_string($phpExec) || !file_exists($phpExec)) {
            if (strtoupper(substr(PHP_OS, 0, 3)) == 'WIN') {
                $phpExec = str_replace('/', '\\', dirname(ini_get('extension_dir'))) . '\php.exe';

                if (@file_exists($phpExec)) {
                    BackgroundProcess::open(escapeshellarg($phpExec) . " " . escapeshellarg($phpScript), $cwd);
                }
            } else {
                $phpExec = exec("which php-cli");

                if ($phpExec[0] != '/') {
                    $phpExec = exec("which php");
                }

                if ($phpExec[0] == '/') {
                    BackgroundProcess::open(escapeshellarg($phpExec) . " " . escapeshellarg($phpScript), $cwd);
                }
            }
        } else {
            if (strtoupper(substr(PHP_OS, 0, 3)) == 'WIN') {
                $phpExec = str_replace('/', '\\', $phpExec);
            }

            BackgroundProcess::open(escapeshellarg($phpExec) . " " . escapeshellarg($phpScript), $cwd);
        }
    }
}

Ответ 7

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

На самом деле я добавил к этому дополнительный уровень и получаю и сохраняю идентификатор процесса. Это позволяет мне перенаправить на другую страницу и заставить пользователя сидеть на этой странице, используя AJAX, чтобы проверить, завершен ли процесс (идентификатор процесса больше не существует). Это полезно для случаев, когда длина script приведет к таймауту браузера, но пользователю нужно дождаться завершения этого script до следующего шага. (В моем случае он обрабатывал большие ZIP файлы с CSV файлами, которые добавляют до 30 000 записей в базу данных, после чего пользователь должен подтвердить некоторую информацию.)

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

Ответ 8

PHP HAS многопоточность, его просто не активировано по умолчанию, есть расширение, называемое pthreads, которое делает именно это. Вам понадобится php, скомпилированный с ZTS. (Thread Safe) Ссылки:

Examples

Еще один учебник

pthreads Расширение PECL

Ответ 9

Это отличная идея использовать cURL, как было предложено rojoca.

Вот пример. Вы можете контролировать text.txt, пока script работает в фоновом режиме:

<?php

function doCurl($begin)
{
    echo "Do curl<br />\n";
    $url = 'http://'.$_SERVER['SERVER_NAME'].$_SERVER['REQUEST_URI'];
    $url = preg_replace('/\?.*/', '', $url);
    $url .= '?begin='.$begin;
    echo 'URL: '.$url.'<br>';
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $url);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    $result = curl_exec($ch);
    echo 'Result: '.$result.'<br>';
    curl_close($ch);
}


if (empty($_GET['begin'])) {
    doCurl(1);
}
else {
    while (ob_get_level())
        ob_end_clean();
    header('Connection: close');
    ignore_user_abort();
    ob_start();
    echo 'Connection Closed';
    $size = ob_get_length();
    header("Content-Length: $size");
    ob_end_flush();
    flush();

    $begin = $_GET['begin'];
    $fp = fopen("text.txt", "w");
    fprintf($fp, "begin: %d\n", $begin);
    for ($i = 0; $i < 15; $i++) {
        sleep(1);
        fprintf($fp, "i: %d\n", $i);
    }
    fclose($fp);
    if ($begin < 10)
        doCurl($begin + 1);
}

?>

Ответ 10

К сожалению, PHP не имеет каких-либо встроенных возможностей потоковой передачи. Поэтому я думаю, что в этом случае у вас нет выбора, кроме как использовать какой-то пользовательский код, чтобы делать то, что вы хотите сделать.

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

Ответ 11

Если вы установите HTTP-заголовок Content-Length в ответе "Спасибо за регистрацию", браузер должен закрыть соединение после получения указанного количества байтов. Это приводит к запуску процесса на стороне сервера (при условии, что параметр ignore_user_abort установлен), поэтому он может завершить работу, не дожидаясь конечного пользователя.

Конечно, вам нужно будет рассчитать размер вашего содержимого ответа перед рендерингом заголовков, но это довольно легко для коротких ответов (записать вывод в строку, вызвать strlen(), заголовок вызова(), строку отображения).

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

Ответ 12

Если вам не нужен полностью раздутый ActiveMQ, я рекомендую рассмотреть RabbitMQ. RabbitMQ - это легкий обмен сообщениями, который использует стандарт AMQP.

Я рекомендую также изучить php-amqplib - популярную клиентскую библиотеку AMQP для доступа к брокерам сообщений на основе AMQP.

Ответ 13

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

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/...

Ответ 14

PHP - это однопоточный язык, поэтому нет официального способа запуска асинхронного процесса с ним, кроме использования exec или popen. Существует сообщение в блоге об этом здесь. Ваша идея для очереди в MySQL также является хорошей идеей.

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