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

144

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

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

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

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

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

Davr
источник

Ответы:

80

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

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

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

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

Пол Диксон
источник
Спасибо за все советы. Конкретная информация о ignore_user_abort не очень помогает в моем случае, моя цель - избежать ненужных задержек для пользователя.
Давр
2
Если вы задали HTTP-заголовок Content-Length в ответе «Спасибо за регистрацию», то браузер должен закрыть соединение после получения указанного количества байтов. Это оставляет процесс на стороне сервера запущенным (при условии, что установлен ignore_user_abort), не заставляя конечного пользователя ждать. Конечно, вам нужно будет вычислить размер содержимого вашего ответа перед рендерингом заголовков, но это довольно просто для коротких ответов.
Питер
1
Gearman ( gearman.org ) - отличная кроссплатформенная очередь сообщений с открытым исходным кодом. Вы можете писать рабочие на C, PHP, Perl или на любом другом языке. Существуют плагины Gearman UDF для MySQL, и вы также можете использовать Net_Gearman из PHP или клиент Gearman Pear.
Джастин Суонхарт
Gearman - это то, что я бы порекомендовал сегодня (в 2015 году) для любой пользовательской системы очередей на работу.
Питер
Другой вариант - настроить js-сервер узла для обработки запроса и возврата быстрого ответа с промежуточной задачей. Многие вещи внутри js-скрипта узла выполняются асинхронно, например, http-запрос.
Зордон
22

Если вы просто хотите выполнить один или несколько 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);
}
// repeat this with different parameters as often as you like

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

ignore_user_abort(true);
set_time_limit(0);

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

Markus
источник
set_time_limit не действует, если php работает в безопасном режиме
Baptiste Pernet
17

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

Затем в ваших пользовательских скриптах заходите в сервис:

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

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

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

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

rojoca
источник
1
Мне не нравится этот подход, потому что он перегружает веб-сервер
Oved Yavine
7

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

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

  • Изменение размера изображения - и с помощью слегка загруженной очереди, передаваемой PHP-скрипту на основе CLI, изменение размера больших (2 МБ +) изображений работало очень хорошо, но попытка изменить размер тех же изображений в экземпляре mod_php регулярно приводила к проблемам с объемом памяти (я ограничил процесс PHP до 32 МБ, а изменение размера заняло больше, чем это)
  • проверки на ближайшее будущее - в 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();), он сначала приостановился бы на пару секунд.

Алистер Булман
источник
Мне нравится внешний вид beanstalkd, как только они добавляют настойчивость, я думаю, что это будет прекрасно.
Давр
Это уже в кодовой базе и стабилизируется. Я также с нетерпением жду «именных рабочих мест», так что я могу добавлять туда вещи, но знаю, что они не будут добавлены, если они уже есть. Хорошо для регулярных мероприятий.
Алистер Булман
@AlisterBulman, не могли бы вы дать больше информации или примеров для "Долгосрочного сценария cli, который затем забрал задание из очереди". Я пытаюсь создать такой скрипт для моего приложения.
Саси Варна Кумар
7

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

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

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

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

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

Денис Горобченко
источник
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);
        }
    }
}
Эндрю Мур
источник
4

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

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

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

Дэррил Хейн
источник
1
На какой метод вы ссылаетесь в первом предложении?
Саймон Ист
3

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

Примеры

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

Pthreads PECL Extension

Омар С.
источник
2

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

Вот пример. Вы можете отслеживать 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);
}

?>
Kjeld
источник
2
Это действительно поможет, если исходный код будет прокомментирован. Я понятия не имею, что там происходит, и какие части являются примерами, и какие части можно повторно использовать для моих собственных целей.
Томас Темпельманн
1

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

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

Петр Д
источник
1

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

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

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

Питер
источник
Это не похоже на работу. Когда я использую header('Content-Length: 3'); echo '1234'; sleep(5);то, хотя браузер берет только 3 символа, он все еще ждет в течение 5 секунд, прежде чем показать ответ. Чего мне не хватает?
Томас Темпельманн
@ThomasTempelmann - вам, вероятно, нужно вызвать flush (), чтобы заставить вывод отображаться немедленно, иначе выход будет буферизован до тех пор, пока ваш скрипт не выйдет или пока в STDOUT не будет отправлено достаточно данных для очистки буфера.
Питер
Я уже перепробовал много способов очистки, найденных здесь на SO. Никто не поможет. И, судя по всему, данные тоже отправляются без gzip phpinfo(). Единственное, что я могу себе представить, это то, что мне нужно сначала достичь минимального размера буфера, например, 256 или около того байтов.
Томас Темпельманн
@ThomasTempelmann - я не вижу ничего в вашем вопросе или моем ответе о gzip (обычно имеет смысл сначала заставить простейший сценарий работать, прежде чем добавлять слои сложности). Чтобы установить, когда сервер действительно отправляет данные, вы можете использовать анализатор пакетов плагина браузера (например, fiddler, tamperdata и т. Д.). Затем, если вы обнаружите, что веб-сервер действительно удерживает весь вывод скрипта до завершения независимо от сброса, вам необходимо изменить конфигурацию вашего веб-сервера (в этом случае ваш PHP-скрипт ничего не может сделать).
Питер
Я использую виртуальный веб-сервис, поэтому я мало контролирую его конфигурацию. Я надеялся найти другие предложения о том, что может быть виновником, но кажется, что ваш ответ просто не так универсально применим, как кажется. Слишком много вещей может пойти не так, как надо. Ваше решение, безусловно, гораздо проще реализовать, чем все остальные ответы, приведенные здесь. Жаль, что это не работает для меня.
Томас Темпельманн
1

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

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

phpPhil
источник
0

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

cornjobpage.php // главная страница

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

Хасан Саид
источник
0

Создание новых процессов на сервере с использованием exec()или непосредственно на другом сервере с использованием curl вовсе не так хорошо масштабируется, если мы стремимся к exec, вы в основном заполняете свой сервер долгосрочными процессами, которые могут обрабатываться другими серверами, не подключенными к сети, и использование curl связывает другой сервер, если вы не встроите балансировку нагрузки.

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

Крис Рутерфурд
источник
-4

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

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

Марк W
источник
Письмо было только примером, так как другие задачи более сложны для объяснения, и это не совсем вопрос. Как мы использовали для отправки электронной почты, команда электронной почты не возвращалась, пока удаленный сервер не принял почту. Мы обнаружили, что некоторые почтовые серверы были настроены на добавление длительных задержек (например, 10-20 секунд) перед приемом почты (возможно, для борьбы со спам-ботами), и эти задержки затем передавались бы нашим пользователям. Теперь мы используем локальный почтовый сервер для постановки в очередь отправляемых писем, поэтому этот конкретный не применим, но у нас есть другие задачи аналогичного характера.
Давр
Например: отправка электронной почты через Google Apps Smtp с помощью ssl и порта 465 занимает больше времени, чем обычно.
Gixty