Web

Асинхронный запуск задачи PHP

Я работаю над достаточно большим веб-приложением, и бэкэнд написан в основном на PHP. В коде есть несколько мест, где мне нужно выполнить какую-то задачу, но я не хочу заставлять пользователя ждать результата. Например, при создании нового аккаунта мне нужно отправить ему приветственное письмо. Но когда он нажимает кнопку «Завершить регистрацию», я не хочу заставлять его ждать, пока письмо будет действительно отправлено, я просто хочу запустить процесс и сразу же вернуть пользователю сообщение. До сих пор в некоторых местах я использовал то, что кажется хаком, с помощью exec(). В основном я делал такие вещи, как:

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

 Это, похоже, работает, но мне интересно, есть ли лучший способ. Я рассматриваю возможность написания системы, которая ставит задачи в очередь в таблице MySQL, и отдельного долго работающего PHP-скрипта, который раз в секунду запрашивает эту таблицу и выполняет все новые задачи, которые он находит. Это также позволит мне разделить задачи между несколькими рабочими машинами в будущем, если мне это понадобится. Не изобретаю ли я заново колесо? Есть ли лучшее решение, чем хак exec() или очередь MySQL?

 

Ответ 1

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

  1. GearMan этот решение было написано в 2009 году, и с тех пор GearMan выглядит популярным вариантом.

  2. ActiveMQ если вам нужна полноценная очередь сообщений с открытым исходным кодом.

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

  4. beanstalkd нашел его только во время написания этого ответа, но выглядит интересно.

  5. dropr проект очереди сообщений на базе PHP, но не поддерживается с сентября 2010 года.

  6. php-enqueue недавно (2017) поддерживаемая обертка для различных систем очередей.

  7. Наконец, статья в блоге об использовании memcached для очередей сообщений.

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

 

Ответ 2

Когда вы просто хотите выполнить один или несколько HTTP-запросов, не дожидаясь ответа, также существует простое PHP-решение. В вызывающем скрипте:

$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);

}

// повторяйте это с разными параметрами так часто, как вам необходимо

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

ignore_user_abort(true);

set_time_limit(0);

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

 

Ответ 3

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

  • http://domain/tasks/t1

  • http://domain/tasks/t2

 

Затем в скриптах с пользовательским доступом сделайте вызовы к службе:

$service->addTask('t1', $data); // публикуем данные в URL через curl

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

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

  • http://domain/queue?task=t1

  • http://domain/queue?task=t2

  • http://domain/queue/t1/100931

 

Это требует некоторой подготовки, но дает много преимуществ.

 

Ответ 4

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

  1. Изменение размера изображений при легкой загрузке очереди, передаваемой PHP-скрипту на основе CLI, изменение размера больших (2 mb+) изображений работало просто отлично, но при попытке изменить размер тех же изображений в экземпляре mod_php регулярно возникали проблемы с пространством памяти (я ограничил PHP-процесс 32 MB, а изменение размера занимало больше).

  2. Проверки в ближайшем будущем beanstalkd имеет возможность задержки (сделать это задание доступным для выполнения только через X секунд), — так что я могу запустить 5 или 10 проверок для события немного позже по времени.

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

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

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

 

Ответ 5

Вот простой класс, который я создал для своего веб-приложения. Он позволяет форкать 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);

        }

    }

}

 

Ответ 6

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

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

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

 

Ответ 7

Вот пример, использующий curl. Вы можете отслеживать text.txt, пока скрипт работает в фоновом режиме:

<?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);

}

?>

 

 Ответ 8

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

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");

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

            ?>

            <?php

            /*

             * Выполняет PHP-страницу асинхронно, так что текущей странице не нужно ждать окончания ее выполнения.

             *  

             */

            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";//вы можете использовать POST вместо GET, если хотите

                $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

    ?>

 

Схожие статьи

Какой выбрать CDN для сайта. Оптимизируем скорость загрузки
Web

Какой выбрать CDN для сайта. Оптимизируем скорость загрузки

Web

Почему происходит «Неопределенная переменная/индекс/смещение» в PHP

Все, что нужно знать об атаке на WordPress — меры предосторожности
Web

Все, что нужно знать об атаке на WordPress — меры предосторожности

Самый быстрый DNS-сервер России: сравниваем и выбираем лучший из них
Web

Самый быстрый DNS-сервер России: сравниваем и выбираем лучший из них