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


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

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

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

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

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

15 126

15 ответов:

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

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

  • GearMan - этот ответ был написан в 2009 году, и с тех пор GearMan выглядит популярным вариантом, см. комментарии под.
  • ActiveMQ если вы хотите полную очередь сообщений с открытым исходным кодом.
  • ZeroMQ - это довольно крутая библиотека сокетов, которая позволяет легко писать распределенный код, не беспокоясь слишком много о самом программировании сокетов. Вы можете использовать его для очереди сообщений на одном хосте - вам просто нужно, чтобы ваше веб-приложение отправило что-то в очередь, которую непрерывно работающее консольное приложение будет использовать на следующем подходящем возможность
  • 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 до 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();) он сначала приостановится на пару секунд.

Если это просто вопрос обеспечения дорогостоящих задач, в случае поддержки 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 stuff, некоторые люди придумали способы имитации потоков на PHP.

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

конечно, вам нужно будет рассчитать размер вашего содержимого ответа перед рендерингом заголовков, но это довольно легко для коротких ответов (запись вывода в строку, вызов strlen(), заголовок вызова (), 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 также является хорошей идеей.

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