Виконайте завдання PHP асинхронно


144

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

До сих пір я десь використовував те, що відчуваєш, як хак з exec (). В основному такі дії, як:

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

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

Чи я знову вигадую колесо? Чи є краще рішення, ніж хакер exec () або чергу MySQL?

Відповіді:


80

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

Ознайомлення з власним не надто складне, ось кілька інших варіантів, щоб перевірити:

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

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


Дякую за всі поради. Конкретний варіант ignore_user_abort не дуже допомагає в моєму випадку, вся моя мета - уникнути зайвих затримок для користувача.
davr

2
Якщо ви встановите заголовок HTTP довжини вмісту у відповіді "Дякую за реєстрацію", браузер повинен закрити з'єднання після отримання зазначеної кількості байтів. Це залишає процес запуску серверного сервера (припускаючи, що встановлено ignore_user_abort), не змушуючи кінцевого користувача чекати. Звичайно, вам потрібно буде обчислити розмір вмісту вашої відповіді, перш ніж надавати заголовки, але це досить просто для коротких відповідей.
Петро

1
Gearman ( gearman.org ) - чудова черга з відкритим кодом, яка є крос-платформою. Ви можете писати працівників на C, PHP, Perl або майже будь-якою іншою мовою. Існують плагіни Gearman UDF для MySQL, а також ви можете використовувати Net_Gearman від PHP або клієнта грушевого груша.
Джастін Сванхарт

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 закрите.


set_time_limit не впливає, якщо php працює у безпечному режимі
Baptiste Pernet

17

Ще один спосіб розщедрити процеси - за допомогою curl. Ви можете налаштувати свої внутрішні завдання як веб-сервіс. Наприклад:

Потім у своїх сценаріях, з якими користувач отримує доступ, здійснюють дзвінки в службу:

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

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

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

Це вимагає трохи налаштування роботи, але є багато переваг.


1
Мені не подобається такий підхід, оскільки він перевантажує веб-сервер
Овед Явін

7

Я використовував Beanstalkd для одного проекту і планував ще раз. Я вважав, що це відмінний спосіб запустити асинхронні процеси.

Кілька речей, які я зробив з цим, є:

  • Зміна розміру зображення - і з легким завантаженням черги, що переходить до сценарію PHP на основі CLI, розмір великих (2 Мб +) зображень спрацьовував чудово, але спроби змінити розміри тих самих зображень у модній екземплярі mod_php регулярно стикалися з проблемами в просторі пам'яті (я обмежили процес PHP 32MB, і зміна розміру зайняла більше, ніж це)
  • найближчі чеки - beanstalkd має доступні затримки (зробити це завдання доступним для запуску лише через X секунд) - тому я можу зняти 5 чи 10 перевірок на подію, трохи пізніше

Я написав систему на основі Zend-Framework, щоб розшифрувати "приємний" URL-адрес, так, наприклад, змінити розмір зображення, яке воно викликало б QueueTask('/image/resize/filename/example.jpg'). Спочатку URL розшифровується до масиву (модуль, контролер, дія, параметри), а потім перетворюється в JSON для ін'єкції в саму чергу.

Потім тривалий сценарій кліпу підхопив завдання з черги, запустив його (через Zend_Router_Simple) і, якщо потрібно, поклав інформацію в запам’ятовуваний веб-сайт PHP, щоб забрати, як потрібно, коли це було зроблено.

Однією зморшкою, яку я також вклала, було те, що cli-script працював лише на 50 циклів перед перезавантаженням, але якщо він захотів перезапустити так, як планувалося, це зробить це негайно (запускається через bash-скрипт). Якщо виникла проблема, і я зробив exit(0)(значення за замовчуванням для exit;або die();), спочатку призупиниться на пару секунд.


Мені подобається зовнішній вигляд beanstalkd, коли вони додають наполегливості, я думаю, що це буде ідеально.
davr

Це вже в кодовій базі і стабілізується. Я також з нетерпінням чекаю "названих робочих місць", тому я можу кинути речі туди, але знайте, що вони не будуть додані, якщо там вже є. Добре для регулярних заходів.
Алістер Бульман

@AlisterBulman ви можете надати більше інформації або приклади для "Довго запущений сценарій кліпу, а потім підбирав завдання з черги". Я намагаюся створити такий сценарій для моєї програми.
Сасі варна кумар

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
На який спосіб ви звертаєтесь у своєму першому реченні?
Саймон Схід


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);
}

?>

2
Це дійсно допоможе, якби коментовано вихідний код. Я поняття не маю, що там відбувається і які частини є прикладом і які частини можна повторно використовувати для моїх власних цілей.
Томас Темпельман

1

На жаль, PHP не має жодних власних можливостей для нарізки різьби. Тому я думаю, що в цьому випадку у вас немає іншого вибору, як використовувати якийсь спеціальний код, щоб робити те, що ви хочете робити.

Якщо ви шукаєте по мережі Інтернет для PHP-різьблення, деякі люди придумали способи імітувати теми на PHP.


1

Якщо ви встановите HTTP-заголовок довжини вмісту у відповіді "Дякую за реєстрацію", браузер повинен закрити з'єднання після отримання зазначеної кількості байтів. Це залишає процес запуску на сервері (припускаючи, що встановлено ignore_user_abort), щоб він міг закінчити роботу, не змушуючи кінцевого користувача чекати.

Звичайно, вам потрібно буде обчислити розмір вмісту вашої відповіді, перш ніж надавати заголовки, але це досить просто для коротких відповідей (записувати вихід у рядок, виклик strlen (), заголовок виклику (), візуалізація рядка).

Цей підхід має перевагу в тому, що ви не змушуєте керувати чергою "переднього кінця", і хоча вам може знадобитися виконати деяку роботу на задньому кінці, щоб запобігти переходу дочірніх процесів HTTP один на одного, це вже потрібно зробити , все одно.


Це, здається, не працює. Коли я використовую header('Content-Length: 3'); echo '1234'; sleep(5);тоді, навіть якщо браузер займає лише 3 символи, він все ще чекає 5 секунд, перш ніж показати відповідь. Що я пропускаю?
Томас Темпельман

@ThomasTempelmann - Вам, ймовірно, потрібно зателефонувати flush (), щоб змусити результат вивести насправді негайно, інакше вихід буде буферизований доти, поки ваш сценарій не вийде або не буде надіслано достатньо даних для STDOUT, щоб промити буфер.
Петро

Я вже спробував багато способів промити, знайдений тут на SO. Ніхто не допоможе. І, здається, дані також надсилаються не gzipped, як можна зрозуміти phpinfo(). Єдине інше, що я можу собі уявити, це те, що мені потрібно досягти мінімального розміру буфера спочатку, наприклад 256 байт.
Томас Темпельман

@ThomasTempelmann - я не бачу нічого у вашому запитанні чи моїй відповіді про gzip (зазвичай, має сенс спочатку найпростіший сценарій працювати перед тим, як додати шари складності). Для того, щоб встановити, коли сервер фактично надсилає дані, ви можете використовувати сніффер пакетів плагіна браузера (наприклад, fiddler, tamperdata тощо). Потім, якщо ви виявите, що веб-сервер дійсно тримає весь вихід сценарію до виходу незалежно від флеш-версії, вам потрібно змінити конфігурацію веб-сервера (нічого, що ваш сценарій PHP може зробити в цьому випадку).
Петро

Я використовую віртуальний веб-сервіс, тому я мало контролюю його конфігурацію. Я сподівався знайти інші пропозиції щодо того, що може бути винуватцем, але, здається, ваша відповідь просто не настільки загальноприйнятна, як здається. Занадто багато речей може піти не так, очевидно. Ваше рішення, безумовно, реалізується набагато простіше, ніж всі інші коси, наведені тут. Шкода, що це не працює для мене.
Томас Темпельман

1

Якщо ви не хочете повністю роздутого ActiveMQ, рекомендую розглянути RabbitMQ . RabbitMQ - це полегшене обмін повідомленнями, яке використовує стандарт AMQP .

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


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 також хороша ідея.

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


Електронний лист був лише прикладом, оскільки інші завдання складніші для пояснення, і це насправді не питання. Як ми звикли відправляти електронну пошту, команда електронної пошти не поверталася, поки віддалений сервер не прийняв пошту. Ми виявили, що деякі поштові сервери були налаштовані на додавання великих затримок (наприклад, 10-20 секунд затримки) до прийняття пошти (можливо, для боротьби зі спам-ботами), і ці затримки потім передаватимуться нашим користувачам. Зараз ми використовуємо локальний сервер для встановлення черги на пошту, яку потрібно надіслати, тому цей конкретний не застосовується, але у нас є інші завдання подібного характеру.
davr

Наприклад: надсилання електронних листів через Google Apps Smtp за допомогою ssl та порту 465 займає більше часу, ніж зазвичай.
Gixty
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.