Выполнить задачу PHP асинхронно

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

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

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

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

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

15 ответов


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

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

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

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


когда вы просто хотите выполнить один или несколько 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-соединение закрыто.


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

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

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

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

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

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


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

пара вещей, которые я сделал с ним:

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


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


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

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

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

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


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

примеры

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

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


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

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

?>

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

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


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

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

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


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

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


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

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


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

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


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

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