Вопрос

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

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

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

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

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

Это было полезно?

Решение

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

Свернуть самостоятельно не так уж сложно, вот еще несколько вариантов:

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

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

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

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

  • Изменение размера изображения - и при передаче слегка загруженной очереди 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();) сначала он делал паузу на пару секунд.

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

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

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

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

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

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

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

?>

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

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

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

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

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

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

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

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

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

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

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

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

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

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top