PHP 작업을 비동기 적으로 실행


144

다소 큰 웹 응용 프로그램에서 작업하고 있으며 백엔드는 대부분 PHP입니다. 코드에 몇 가지 작업을 완료 해야하는 곳이 있지만 사용자가 결과를 기다리도록하고 싶지 않습니다. 예를 들어, 새 계정을 만들 때 환영 이메일을 보내야합니다. 그러나 '등록 완료'버튼을 누르면 이메일이 실제로 전송 될 때까지 기다리지 않고 프로세스를 시작하고 사용자에게 메시지를 즉시 반환하고 싶습니다.

지금까지 어떤 곳에서는 exec ()와 같은 느낌의 것을 사용했습니다. 기본적으로 다음과 같은 작업을 수행합니다.

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

어느 것이 효과가있는 것처럼 보이지만 더 좋은 방법이 있는지 궁금합니다. MySQL 테이블에 작업을 대기열에 넣는 시스템과 해당 테이블을 1 초에 한 번 쿼리하고 찾은 새로운 작업을 실행하는 별도의 장기 실행 PHP 스크립트를 작성하려고합니다. 또한 필요한 경우 나중에 여러 작업자 시스템간에 작업을 분할 할 수 있다는 이점이 있습니다.

바퀴를 다시 발명하고 있습니까? exec () 해킹 또는 MySQL 대기열보다 더 나은 솔루션이 있습니까?

답변:


80

큐 접근 방식을 사용했으며 서버로드가 유휴 상태가 될 때까지 해당 처리를 연기 할 수있어 "긴급하지 않은 작업"을 쉽게 분할 할 수 있으면로드를 매우 효과적으로 관리 할 수 ​​있습니다.

직접 롤링하는 것은 그리 까다 롭지 않습니다. 다음은 확인할 다른 옵션입니다.

  • GearMan- 이 답변은 2009 년에 작성되었으며 그 이후로 GearMan은 인기있는 옵션으로 보입니다. 아래 설명을 참조하십시오.
  • 전체 오픈 소스 메시지 큐를 원하는 경우 ActiveMQ
  • ZeroMQ- 소켓 프로그래밍 자체에 대해 너무 걱정할 필요없이 분산 코드를 쉽게 작성할 수있는 매우 멋진 소켓 라이브러리입니다. 단일 호스트에서 메시지 대기열에 사용할 수 있습니다. 웹앱이 다음 번에 적절한 기회에 지속적으로 실행되는 콘솔 앱이 소비하는 대기열로 무언가를 밀어 넣습니다.
  • beanstalkd- 이 답변을 작성하는 동안 만 찾았지만 흥미로워 보입니다.
  • dropr 는 PHP 기반의 메시지 큐 프로젝트이지만 2010 년 9 월 이후 적극적으로 관리되지 않았습니다
  • php-enqueue 는 다양한 대기열 시스템을 중심으로 최근 (2017) 유지 관리 래퍼입니다.
  • 마지막으로 메시지 대기열에 memcached를 사용하는 방법에 대한 블로그 게시물

또 다른 방법은 ignore_user_abort 를 사용 하는 것입니다. 페이지를 사용자에게 보낸 후에는 조기 종료에 대한 두려움없이 최종 처리를 수행 할 수 있습니다. 원근법.


모든 팁에 감사드립니다. ignore_user_abort에 대한 구체적인 내용은 실제로 도움이되지 않습니다. 제 전체 목표는 사용자에게 불필요한 지연을 피하는 것입니다.
— davr

2
"감사합니다"응답에서 Content-Length HTTP 헤더를 설정하면 지정된 바이트 수가 수신 된 후 브라우저가 연결을 닫아야합니다. 그러면 최종 사용자를 기다리지 않고 서버 측 프로세스가 실행됩니다 (ignore_user_abort가 설정되었다고 가정). 물론 헤더를 렌더링하기 전에 응답 내용의 크기를 계산해야하지만 짧은 응답에서는 매우 쉽습니다.
— Peter

1
Gearman ( gearman.org )은 크로스 플랫폼 인 훌륭한 오픈 소스 메시지 큐입니다. C, PHP, Perl 또는 다른 언어로 작업자를 작성할 수 있습니다. MySQL 용 Gearman UDF 플러그인이 있으며 PHP의 Net_Gearman 또는 gearman pear 클라이언트를 사용할 수도 있습니다.
— 저스틴 스완 하트

Gearman은 모든 맞춤형 작업 큐잉 시스템에 대해 오늘 (2015 년)에 추천 할 것입니다.
— 피터

다른 옵션은 요청을 처리하고 그 사이의 작업으로 빠른 응답을 반환하도록 노드 js 서버를 설정하는 것입니다. 노드 js 스크립트 내부의 많은 것들이 http 요청과 같이 비동기 적으로 실행됩니다.
— Zordon

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 연결이 닫힐 때 스크립트가 시간 제한없이 계속 실행됩니다.


PHP가 안전 모드에서 실행되면 set_time_limit는 효과가 없습니다
— Baptiste Pernet

17

프로세스를 분기하는 또 다른 방법은 curl을 사용하는 것입니다. 내부 작업을 웹 서비스로 설정할 수 있습니다. 예를 들면 다음과 같습니다.

그런 다음 사용자 액세스 스크립트에서 서비스를 호출하십시오.

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

귀하의 서비스는 mysql 또는 원하는 점이 무엇이든간에 작업 대기열을 추적 할 수 있습니다 : 서비스 내에 모두 포함되어 있으며 스크립트는 URL을 소비하고 있습니다. 이렇게하면 필요한 경우 서비스를 다른 머신 / 서버로 옮길 수 있습니다 (즉, 쉽게 확장 가능).

http 권한 또는 사용자 지정 권한 부여 체계 (예 : Amazon의 웹 서비스)를 추가하면 다른 사람 / 서비스 (원하는 경우)가 소비 할 작업을 열 수 있으며 계속 추적하여 모니터링 서비스를 추가 할 수 있습니다. 대기열 및 작업 상태.

약간의 설정 작업이 필요하지만 많은 이점이 있습니다.


1
웹 서버에 과부하가
— 걸리기 때문에이

7

한 프로젝트에 Beanstalkd 를 사용 하고 다시 계획했습니다. 비동기 프로세스를 실행하는 훌륭한 방법이라는 것을 알았습니다.

내가 한 두 가지 작업은 다음과 같습니다.

  • 이미지 크기 조정-약간의 대기열이 CLI 기반 PHP 스크립트로 전달되어 큰 (2mb +) 이미지 크기 조정은 정상적으로 작동했지만 mod_php 인스턴스 내에서 동일한 이미지의 크기를 조정하려고 시도하면 정기적으로 메모리 공간 문제가 발생했습니다 (I PHP 프로세스를 32MB로 제한했으며 크기 조정은 그 이상이었습니다.
  • 가까운 미래 점검-Beanstalkd에서 지연을 사용할 수 있습니다 (X 초 후에 만이 작업을 실행 가능하게 함). 이벤트에 대해 5 ~ 10 회 점검을 실행할 수 있습니다.

'nice'url을 해독하기 위해 Zend-Framework 기반 시스템을 작성했습니다. 예를 들어 이미지 크기를 조정하려면 QueueTask('/image/resize/filename/example.jpg'). URL은 먼저 배열 (모듈, 컨트롤러, 작업, 매개 변수)로 디코딩 된 다음 대기열 자체에 주입하기 위해 JSON으로 변환되었습니다.

오랫동안 실행되는 cli 스크립트는 대기열에서 작업을 가져 와서 (Zend_Router_Simple을 통해) 실행 한 다음, 필요한 경우 웹 사이트 PHP가 정보를 수집 할 때 memcached에 정보를 입력하여 완료되었습니다.

필자가 넣은 한 가지 주름은 cli-script가 다시 시작하기 전에 50 루프 동안 만 실행되었지만 계획대로 다시 시작하려는 경우 즉시 시작합니다 (bash-script를 통해 실행 됨). 문제가 있고 내가 한 경우 exit(0)( exit;또는 의 기본값 die();) 먼저 몇 초 동안 일시 중지됩니다.


나는 콩 줄기 모양을 좋아합니다. 일단 지속성을 추가하면 완벽 할 것이라고 생각합니다.
— davr

그것은 이미 코드베이스에 있고 안정화되고 있습니다. 또한 '명명 된 직업'을 기대하고 있기 때문에 거기에 물건을 던질 수는 있지만 이미 거기에 있다면 추가되지 않을 것입니다. 정기적 인 행사에 좋습니다.
— Alister Bulman

@AlisterBulman은 "오래 실행되는 cli 스크립트가 대기열에서 작업을 선택했습니다"에 대한 자세한 정보 나 예제를 제공 할 수 있습니다. 내 응용 프로그램에 대한 cli 스크립트를 작성하려고합니다.
— Sasi varna kumar

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는 단일 스레드이므로 할 수있는 일이별로 없습니다.

실제로 이것에 하나의 추가 레벨을 추가했으며 프로세스 ID를 가져오고 저장합니다. 이를 통해 다른 페이지로 리디렉션하고 사용자가 해당 페이지에 앉아 AJAX를 사용하여 프로세스가 완료되었는지 확인합니다 (프로세스 ID가 더 이상 존재하지 않음). 이는 스크립트 길이로 인해 브라우저가 시간 초과 될 수 있지만 다음 단계 전에 해당 스크립트가 완료 될 때까지 기다려야하는 경우에 유용합니다. (제 경우에는 사용자가 정보를 확인 해야하는 데이터베이스에 최대 30 000 개의 레코드를 추가하는 CSV와 같은 파일로 큰 ZIP 파일을 처리하고있었습니다.)

보고서 생성에도 비슷한 프로세스를 사용했습니다. 느린 SMTP에 실제 문제가없는 한 전자 메일과 같은 작업에 "백그라운드 처리"를 사용할지 잘 모르겠습니다. 대신 테이블을 대기열로 사용한 다음 1 분마다 실행되는 프로세스를 통해 대기열 내에서 전자 메일을 보낼 수 있습니다. 이메일을 두 번 또는 다른 유사한 문제로 보내는 것에주의해야합니다. 다른 작업에도 비슷한 큐잉 프로세스를 고려할 것입니다.


1
첫 번째 문장에서 어떤 방법을 언급하고 있습니까?
— Simon East


2

rojoca에서 제안한대로 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);
}

?>

2
소스 코드에 주석을 달면 정말 도움이 될 것입니다. 나는 거기에서 무슨 일이 일어나고 있는지, 어떤 부분이 모범이며 어떤 부분이 내 자신의 목적으로 재사용 가능한지 전혀 모른다.
— Thomas Tempelmann

1

불행히도 PHP에는 어떤 종류의 네이티브 스레딩 기능이 없습니다. 따라서이 경우에는 원하는 것을 수행하기 위해 일종의 사용자 정의 코드를 사용하는 것 외에는 선택의 여지가 없다고 생각합니다.

인터넷에서 PHP 스레딩 항목을 검색하면 일부 사람들이 PHP에서 스레드를 시뮬레이션하는 방법을 생각해 냈습니다.


1

"감사합니다"응답에서 Content-Length HTTP 헤더를 설정하면 지정된 바이트 수를받은 후 브라우저가 연결을 닫아야합니다. 그러면 서버 측 프로세스가 실행중인 상태로 유지됩니다 (ignore_user_abort가 설정되어 있다고 가정). 따라서 최종 사용자를 기다리지 않고 작업을 완료 할 수 있습니다.

물론 헤더를 렌더링하기 전에 응답 내용의 크기를 계산해야하지만 짧은 응답 (문자열에 출력 출력, strlen () 호출, header () 호출, 렌더링 문자열)에는 매우 쉽습니다.

이 접근 방식은 "프론트 엔드"대기열을 관리 하지 않아도 되는 이점이 있으며, HTTP 하위 프로세스 경주가 서로 밟 히지 않도록 백엔드에서 약간의 작업을 수행해야 할 수도 있습니다. , 어쨌든.


작동하지 않는 것 같습니다. header('Content-Length: 3'); echo '1234'; sleep(5);브라우저를 사용하면 3 자만 사용 하더라도 응답을 표시하기 전에 5 초 동안 기다립니다. 내가 무엇을 놓치고 있습니까?
— 토마스 TEMPELMANN

@ThomasTempelmann-출력을 실제로 즉시 렌더링하려면 flush ()를 호출해야합니다. 그렇지 않으면 스크립트가 종료되거나 버퍼를 플러시하기에 충분한 데이터가 STDOUT에 전송 될 때까지 출력이 버퍼링됩니다.
— 피터

나는 이미 여러 가지 방법으로 플러시를 시도했습니다. 도움이 없습니다. 그리고 데이터는에서 알 수 있듯이 압축되지 않은 상태로 전송 된 것으로 보입니다 phpinfo(). 내가 상상할 수있는 유일한 다른 것은 최소 버퍼 크기 (예 : 256 정도의 바이트)에 도달해야한다는 것입니다.
— Thomas Tempelmann

@ThomasTempelmann-gzip에 대한 귀하의 질문이나 답변에 아무것도 보이지 않습니다 (복잡한 계층을 추가하기 전에 가장 간단한 시나리오를 먼저 수행하는 것이 좋습니다). 서버가 실제로 데이터를 보내는시기를 설정하기 위해 브라우저 플러그인의 패킷 스니퍼 (예 : 피들러, 탬퍼 데이터 등)를 사용할 수 있습니다. 그런 다음 웹 서버가 플러시에 관계없이 종료 될 때까지 실제로 모든 스크립트 출력을 보유하고 있음을 발견하면 웹 서버 구성을 수정해야합니다 (이 경우 PHP 스크립트가 할 수있는 것은 없습니다).
— 피터

가상 웹 서비스를 사용하므로 구성을 거의 제어 할 수 없습니다. 범인이 될 수있는 것에 대한 다른 제안을 찾고 싶었지만 귀하의 답변이 보편적으로 적용 가능한 것은 아닌 것 같습니다. 너무 많은 일이 잘못 될 수 있습니다. 귀하의 솔루션은 여기에 제공된 다른 모든 anwer보다 확실히 구현하기가 훨씬 쉽습니다. 그것은 나에게 효과적이지 않습니다.
— Thomas Tempelmann

1

완전한 ActiveMQ를 원하지 않으면 RabbitMQ 를 고려하는 것이 좋습니다 . RabbitMQ는 AMQP 표준 을 사용하는 경량 메시징입니다 .

AMQP 기반 메시지 브로커에 액세스하기 위해 널리 사용되는 AMQP 클라이언트 라이브러리 인 php-amqplib 도 확인하는 것이 좋습니다 .


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
    ?>

추신 : URL 매개 변수를 루프로 보내려면 다음 대답을 따르십시오 : https : //.com/a/41225209/6295712


0

다음을 사용하여 서버에서 새 프로세스 생성 exec()curl을 하거나 다른 서버에서 직접 또는 해도 전혀 확장되지 않습니다. exec를 사용하려면 기본적으로 다른 비 대면 서버에서 처리 할 수있는 장기 실행 프로세스로 서버를 채우고 있습니다. 어떤 종류의로드 밸런싱으로 빌드하지 않는 한 curl을 사용하면 다른 서버와 연결됩니다.

몇 가지 상황에서 Gearman을 사용했으며 이러한 종류의 사용 사례에 더 적합합니다. 단일 작업 대기열 서버를 사용하여 기본적으로 서버에서 수행해야하는 모든 작업의 ​​대기열을 처리하고 작업자 서버를 스핀 업할 수 있습니다. 각 서버는 필요한만큼 작업자 프로세스 인스턴스를 실행할 수 있으며 그 수를 확장 할 수 있습니다. 작업자 서버를 필요에 따라 배치하고 필요하지 않은 경우 스핀 다운합니다. 또한 필요할 때 작업자 프로세스를 완전히 종료하고 작업자가 다시 온라인 상태가 될 때까지 작업을 대기시킵니다.


-4

PHP는 단일 스레드 언어이므로 execor를 사용하는 것 이외의 방법으로 비동기 프로세스를 시작할 수있는 공식적인 방법은 없습니다 popen. 여기에 대한 블로그 게시물이 있습니다 . MySQL의 대기열에 대한 아이디어도 좋습니다.

여기서 귀하의 특정 요구 사항은 사용자에게 이메일을 보내는 것입니다. 이메일을 보내는 것이 매우 간단하고 빠른 작업이기 때문에 왜 비동기식으로 그렇게하려고하는지 궁금합니다. 나는 당신이 많은 양의 이메일을 보내고 있고 ISP가 스팸에 대한 의심으로 당신을 차단하고 있다고 가정합니다.


다른 작업은 설명하기가 더 복잡하기 때문에 이메일은 단지 예일 뿐이며 실제로 문제의 핵심은 아닙니다. 우리가 이메일을 보내던 방식으로, 이메일 명령은 원격 서버가 메일을 수락 할 때까지 반환되지 않습니다. 일부 메일 서버는 메일을 수락하기 전에 (아마 스팸봇과 싸울 수 있기 때문에) 10-20 초 지연과 같은 긴 지연을 추가하도록 구성되었으며 이러한 지연은 사용자에게 전달됩니다. 이제 로컬 메일 서버를 사용하여 보낼 메일을 대기열에 넣으 므로이 특정 메일은 적용되지 않지만 비슷한 성격의 다른 작업이 있습니다.
— davr

예를 들어, ssl 및 포트 465를 사용하여 Google Apps Smtp를 통해 이메일을 보내는 데 평소보다 시간이 오래 걸립니다.
— Gixty
당사 사이트를 사용함과 동시에 당사의 쿠키 정책과 개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.