Запускать задачу PHP асинхронно - PullRequest
135 голосов
/ 13 мая 2009

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

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

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

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

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

Ответы [ 15 ]

77 голосов
/ 13 мая 2009

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

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

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

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

20 голосов
/ 14 апреля 2011

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

17 голосов
/ 14 мая 2009

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

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

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

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

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

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

7 голосов
/ 21 января 2016

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

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

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

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

Еще раз требуется php-fpm.

7 голосов
/ 18 мая 2009

Я использовал 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();), он сначала приостановился бы на пару секунд.

5 голосов
/ 14 мая 2009

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

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

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

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

3 голосов
/ 25 февраля 2015

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

Примеры

Другой учебник

pthreads PECL Extension

2 голосов
/ 29 мая 2013

Отличная идея использовать 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);
}

?>
1 голос
/ 24 февраля 2015

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...