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.