PHP Görevini Eşzamansız Olarak Çalıştırma


144

Biraz büyük bir web uygulaması üzerinde çalışıyorum ve arka uç çoğunlukla PHP'de. Kodda bazı görevleri tamamlamam gereken birkaç yer var, ancak kullanıcıyı sonuç için bekletmek istemiyorum. Örneğin, yeni bir hesap oluştururken onlara hoş geldiniz e-postası göndermem gerekiyor. Ancak 'Kaydı Bitir' düğmesine bastıklarında, e-posta gönderilinceye kadar beklemelerini istemiyorum, sadece işlemi başlatmak ve hemen kullanıcıya bir mesaj göndermek istiyorum.

Şimdiye kadar, bazı yerlerde exec () ile hack gibi hissettiren şeyleri kullanıyorum. Temelde aşağıdaki gibi şeyler yapıyor:

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

Bu işe yarıyor gibi görünüyor, ama daha iyi bir yol olup olmadığını merak ediyorum. Bir MySQL tablosunda görevleri sıralayan bir sistem ve saniyede bir kez bu tabloları sorgulayan ve bulduğu yeni görevleri yürüten ayrı bir uzun süreli PHP komut dosyası yazmayı düşünüyorum. Bu da, eğer gerekirse, gelecekte birkaç işçi makinesi arasında görevleri bölmeme izin verme avantajına da sahip olacaktı.

Tekerleği yeniden icat ediyor muyum? Exec () kesmek veya MySQL kuyruğundan daha iyi bir çözüm var mı?

Yanıtlar:


80

Kuyruk yaklaşımını kullandım ve sunucu yükünüz boşta olana kadar işlemeyi erteleyebileceğiniz ve "acil olmayan" görevleri kolayca ayırabiliyorsanız yükünüzü oldukça etkili bir şekilde yönetmenize izin verdiği için iyi çalışıyor.

Kendinizi yuvarlamak çok zor değil, kontrol etmek için birkaç seçenek daha var:

  • GearMan - bu cevap 2009'da yazılmıştır ve o zamandan beri GearMan popüler bir seçenek gibi görünüyor, aşağıdaki yorumlara bakın.
  • Tam gelişmiş bir açık kaynak mesaj kuyruğu istiyorsanız ActiveMQ .
  • ZeroMQ - soket programlamasının kendisi hakkında çok fazla endişelenmenize gerek kalmadan dağıtılmış kod yazmayı kolaylaştıran oldukça havalı bir soket kütüphanesi. Tek bir ana bilgisayarda ileti sıralaması için kullanabilirsiniz - web uygulamanızın sürekli çalışan bir konsol uygulamasının bir sonraki uygun fırsatta tüketeceği bir kuyruğa bir şey itmesini sağlarsınız
  • beanstalkd - sadece bu cevabı yazarken buldum, ama ilginç görünüyor
  • dropr PHP tabanlı bir mesaj kuyruğu projesidir, ancak Eylül 2010'dan beri aktif olarak sürdürülmemiştir.
  • php-enqueue son zamanlarda (2017) çeşitli kuyruk sistemleri etrafında korunan bir sarıcıdır
  • Son olarak, mesaj kuyruğu için memcached kullanımı hakkında bir blog yazısı

Belki de daha basit başka bir yaklaşım ignore_user_abort kullanmaktır - sayfayı kullanıcıya gönderdikten sonra, son işleminizi erken sonlandırma korkusu olmadan yapabilirsiniz, ancak bu, kullanıcıdan sayfa yükünü uzatmış gibi görünme etkisine sahiptir. perspektif.


Tüm ipuçları için teşekkürler. İgnore_user_abort ile ilgili özel olan benim durumumda gerçekten yardımcı olmuyor, tüm hedefim kullanıcı için gereksiz gecikmelerden kaçınmak.
davr

2
İçerik Uzunluğu HTTP üstbilgisini "Kaydettiğiniz için Teşekkür Ederiz" yanıtınızda ayarlarsanız, belirtilen sayıda bayt alındıktan sonra tarayıcının bağlantıyı kapatması gerekir. Bu, son kullanıcıyı bekletmeden sunucu tarafı işlemini çalışır durumda bırakır (ignore_user_abort öğesinin ayarlandığı varsayılarak). Tabii ki, başlıkları oluşturmadan önce yanıt içeriğinizin boyutunu hesaplamanız gerekecektir, ancak bu kısa yanıtlar için oldukça kolaydır.
Peter

1
Gearman ( gearman.org ) çapraz platform olan harika bir açık kaynak mesaj kuyruğu. İşçileri C, PHP, Perl veya hemen hemen başka herhangi bir dilde yazabilirsiniz. MySQL için Gearman UDF eklentileri vardır ve PHP'den veya dişli armut istemcisinden Net_Gearman'ı da kullanabilirsiniz.
Justin Swanhart

Gearman, herhangi bir özel iş kuyruğu sistemi üzerinde bugün (2015'te) tavsiye edeceğim şey olurdu.
Peter

Başka bir seçenek, bir isteği işlemek ve aradaki bir görevle hızlı bir yanıt döndürmek için bir düğüm js sunucusu kurmaktır. Düğüm js betiğinin içindeki birçok şey, http isteği gibi eşzamansız olarak yürütülür.
Zordon

22

Sadece bir veya birkaç HTTP isteğini yanıtı beklemek zorunda kalmadan yürütmek istediğinizde, basit bir PHP çözümü de vardır.

Çağıran komut dosyasında:

$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

Aranan script.php dosyasında, ilk satırlarda şu PHP işlevlerini çağırabilirsiniz:

ignore_user_abort(true);
set_time_limit(0);

Bu, HTTP bağlantısı kapatıldığında komut dosyasının zaman sınırı olmadan çalışmaya devam etmesine neden olur.


php güvenli modda çalıştırırsa set_time_limit'in bir etkisi yoktur
Baptiste

17

Çatal işlemlerinin diğer bir yolu kıvrılmadır. Dahili görevlerinizi bir web hizmeti olarak ayarlayabilirsiniz. Örneğin:

Ardından, kullanıcı tarafından erişilen komut dosyalarınızda hizmeti arayın:

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

Hizmetiniz mysql ile görev sırasını veya istediğiniz herhangi bir şeyi takip edebilir: hepsi hizmetin içine sarılır ve komut dosyanız sadece URL tüketir. Bu, gerekirse hizmeti başka bir makineye / sunucuya taşımanızı sağlar (yani kolayca ölçeklenebilir).

Http yetkisi veya özel bir yetkilendirme şeması (Amazon'un web hizmetleri gibi) eklemek, görevlerinizi diğer kişiler / hizmetler tarafından tüketilmek üzere açmanızı sağlar (isterseniz) ve daha ileri gidebilir ve takip etmek için en üste bir izleme hizmeti ekleyebilirsiniz. kuyruk ve görev durumu.

Biraz kurulum işi gerektirir, ancak birçok faydası vardır.


1
Bu yaklaşımı sevmiyorum çünkü web sunucusunu aşırı
yüklüyor

7

Beanstalkd'ı bir proje için kullandım ve tekrar planladım. Eşzamansız süreçleri çalıştırmak için mükemmel bir yol buldum.

Bununla yaptığım birkaç şey:

  • Görüntü yeniden boyutlandırma - ve hafif yüklü bir kuyruk CLI tabanlı bir PHP komut dosyasına aktarılırken, büyük (2mb +) görüntüleri yeniden boyutlandırmak gayet iyi çalıştı, ancak bir mod_php örneğinde aynı görüntüleri yeniden boyutlandırmaya çalışmak bellek alanı sorunlarına düzenli olarak çalışıyordu (I PHP sürecini 32 MB ile sınırladı ve yeniden boyutlandırma bundan daha fazlasını aldı)
  • yakın gelecekteki kontroller - beanstalkd'in gecikmeleri var (bu işi sadece X saniye sonra çalıştırılabilir hale getirin) - böylece bir olay için 5 veya 10 kontrolü erteleyebilirim, biraz sonra

'Güzel' bir URL'nin kodunu çözmek için Zend-Framework tabanlı bir sistem yazdım, örneğin, çağırdığı bir görüntüyü yeniden boyutlandırmak için QueueTask('/image/resize/filename/example.jpg'). URL önce bir diziye (modül, denetleyici, eylem, parametreler) kodu çözüldü ve ardından kuyruğa enjeksiyon için JSON'a dönüştürüldü.

Uzun süren bir cli komut dosyası daha sonra işi kuyruktan aldı, çalıştırdı (Zend_Router_Simple aracılığıyla) ve gerekirse PHP web sitesinin tamamlandığında gerektiği gibi alması için bilgileri memcached'a koydu.

Ayrıca koyduğum bir kırışıklık, cli-betiğin yeniden başlatmadan önce sadece 50 döngü boyunca çalıştığıydı, ancak planlandığı gibi yeniden başlatmak isterse, hemen yapardı (bir bash betiği ile çalıştırılıyor). Bir sorun varsa ve yaptım exit(0)(için varsayılan değerexit; veyadie(); ) ilk önce birkaç saniye duraklar.


Ben beanstalkd görünümünü seviyorum, kalıcılık ekledikten sonra mükemmel olacağını düşünüyorum.
davr

Bu zaten kod tabanında ve stabilize ediliyor. Ben de 'adlandırılmış işleri' dört gözle bekliyorum, o yüzden oraya bir şeyler fırlatabilirim, ama orada bir tane varsa eklenmeyeceğini biliyorum. Düzenli etkinlikler için iyi.
Alister Bulman

@AlisterBulman "Uzun süren bir cli betiği daha sonra işi kuyruktan aldı" için daha fazla bilgi veya örnek verebilir misiniz? Benim uygulama için böyle bir cli komut dosyası oluşturmaya çalışıyorum.
Sasi varna kumar

7

Sadece pahalı görevler sağlama sorusu varsa, php-fpm destekleniyorsa, neden fastcgi_finish_request()işlevi kullanmıyorsunuz ?

Bu işlev, istemciye verilen tüm yanıt verilerini temizler ve isteği sonlandırır. Bu, istemciye bağlantı açık bırakılmadan zaman alan görevlerin gerçekleştirilmesine izin verir.

Asenkronikliği gerçekten bu şekilde kullanmazsınız:

  1. Önce tüm ana kodunuzu yapın.
  2. gerçekleştirmek fastcgi_finish_request() .
  3. Tüm ağır şeyleri yapın.

Bir kez daha php-fpm gereklidir.


5

İşte web uygulamam için kodladığım basit bir sınıf. PHP komut dosyalarının ve diğer komut dosyalarının çatallanmasına izin verir. UNIX ve Windows üzerinde çalışır.

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

Bu, birkaç yıldır kullandığım yöntemle aynı ve daha iyi bir şey görmedim veya bulamadım. İnsanların söylediği gibi, PHP tek iş parçacıklı, bu yüzden yapabileceğiniz başka bir şey yok.

Aslında buna fazladan bir seviye ekledim ve bu işlem kimliğini alıp saklıyor. Bu, işlemin tamamlanıp tamamlanmadığını kontrol etmek için AJAX kullanarak başka bir sayfaya yönlendirmemi ve kullanıcının o sayfada oturmasını sağlamamı sağlıyor (işlem kimliği artık mevcut değil). Bu, komut dosyasının uzunluğunun tarayıcının zaman aşımına neden olacağı durumlarda yararlıdır, ancak kullanıcının bir sonraki adımdan önce komut dosyasının tamamlanmasını beklemesi gerekir. (Benim durumumda, veritabanına 30 000 kayıt ekleyen CSV benzeri dosyalar içeren büyük ZIP dosyaları işleniyordu ve daha sonra kullanıcının bazı bilgileri onaylaması gerekiyor.)

Rapor oluşturmak için de benzer bir süreç kullandım. Yavaş bir SMTP ile ilgili gerçek bir sorun olmadığı sürece, e-posta gibi bir şey için "arka plan işleme" kullanacağımdan emin değilim. Bunun yerine bir tabloyu kuyruk olarak kullanabilir ve daha sonra kuyruktaki e-postaları göndermek için her dakika çalışan bir işleme sahip olabilirim. İki kez e-posta veya benzeri sorunlar göndermek için savaşmanız gerekir. Diğer görevler için de benzer bir kuyruklama süreci düşünürdüm.


1
İlk cümlede hangi yönteme başvuruyorsunuz?
Simon East


2

Rojoca tarafından önerildiği gibi cURL kullanmak harika bir fikirdir.

İşte bir örnek. Komut dosyası arka planda çalışırken text.txt dosyasını izleyebilirsiniz:

<?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
Kaynak kodun yorumlanması gerçekten yararlı olacaktır. Orada neler olduğu ve hangi parçaların örnek olduğu ve hangi parçaların kendi amacım için tekrar kullanılabileceği hakkında hiçbir fikrim yok.
Thomas Tempelmann

1

Ne yazık ki PHP'nin herhangi bir yerel iş parçacığı oluşturma özelliği yoktur. Bu durumda, ne yapmak istediğinizi yapmak için özel bir kod kullanmaktan başka seçeneğiniz olmadığını düşünüyorum.

PHP iş parçacığı şeyler için net etrafında arama yaparsanız, bazı insanlar PHP iş parçacığı simüle etmek için yollar bulduk.


1

İçerik Uzunluğu HTTP üstbilgisini "Kaydettiğiniz için Teşekkür Ederim" yanıtınızda ayarlarsanız, belirtilen sayıda bayt alındıktan sonra tarayıcının bağlantıyı kapatması gerekir. Bu, sunucu tarafı işlemini çalışır durumda bırakır (ignore_user_abort öğesinin ayarlandığı varsayılarak), böylece son kullanıcıyı bekletmeden çalışmayı bitirebilir.

Tabii ki, üstbilgileri oluşturmadan önce yanıt içeriğinizin boyutunu hesaplamanız gerekir, ancak bu kısa yanıtlar için oldukça kolaydır (bir dizeye çıktı yazma, çağrı strlen (), çağrı başlığı (), render dizesi).

Bu yaklaşım avantajına sahip değildir , bir "ön uç" kuyruk yönetmek için zorluyor ve bir şeyi zaten yapmak için gerekli olduğunu, birbirlerine bozkır HTTP çocuk süreçlerini yarış önlemek için arka plan üzerinde bazı işler yapmak gerekebilir rağmen , yine de.


Bu işe yaramıyor gibi görünüyor. header('Content-Length: 3'); echo '1234'; sleep(5);Tarayıcıyı yalnızca 3 karakter almasına rağmen kullandığımda , yanıtı göstermeden önce yine de 5 saniye bekler. Neyi kaçırıyorum?
Thomas Tempelmann

@ThomasTempelmann - Çıktının hemen gerçekte oluşturulmasını sağlamak için muhtemelen flush () öğesini çağırmanız gerekir, aksi takdirde komut dosyası çıkana veya tamponu temizlemek için STDOUT'a yeterli veri gönderilinceye kadar çıktı arabelleğe alınır.
Peter

Ben zaten burada SO bulundu, yıkamak için birçok yol denedim. Hiçbiri yardım etmiyor. Ve verilerin de söyleyebileceği gibi, sıkıştırılmamış olarak gönderildiği görülüyor phpinfo(). Hayal edebileceğim tek şey, önce minimum bir tampon boyutuna, örneğin 256 veya bayt'a ulaşmam gerektiğidir.
Thomas Tempelmann

@ThomasTempelmann - Sorunuzda hiçbir şey görmüyorum veya gzip ile ilgili cevabımı görmüyorum (karmaşıklık katmanları eklemeden önce ilk önce en basit senaryoyu çalıştırmak mantıklıdır). Sunucunun gerçekte veri gönderdiğini belirlemek için tarayıcı eklentisinin bir paket dinleyicisini (kemancı, tamperdata vb.) Kullanabilirsiniz. Daha sonra, web sunucusunun yıkamadan bağımsız olarak çıkışa kadar tüm komut dosyası çıktılarını gerçekten tuttuğunu görürseniz, web sunucusu yapılandırmanızı değiştirmeniz gerekir (bu durumda PHP komut dosyanızın yapabileceği hiçbir şey yoktur).
Peter

Sanal bir web hizmeti kullanıyorum, bu yüzden yapılandırması üzerinde çok az kontrole sahibim. Suçlu olabilecek şeyler hakkında başka öneriler bulmayı umuyordum, ancak cevabınızın göründüğü kadar evrensel olarak uygulanabilir olmadığı anlaşılıyor. Açıkçası çok fazla şey ters gidebilir. Çözümünüzü uygulamak, burada verilen diğer tüm yanıtlardan çok daha kolaydır. Çok kötü benim için işe yaramıyor.
Thomas Tempelmann


0

Ben bu tekniği denemek gerektiğini düşünüyorum gibi tüm sayfalar asynchronous olarak her sayfa yanıtı beklemeden bağımsız olarak tek seferde çalışacak gibi istediğiniz kadar çağrı yardımcı olacaktır.

cornjobpage.php // ana sayfa

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

Not: URL parametrelerini döngü olarak göndermek istiyorsanız şu yanıtı izleyin: https://stackoverflow.com/a/41225209/6295712


0

Yeni süreçleri sunucuda exec()veya doğrudan curl kullanarak başka bir sunucuda oluşturmak hiç de iyi ölçeklenmez, exec için gidersek, sunucunuzu web'e yönelik olmayan diğer sunucular tarafından işlenebilen uzun çalışan süreçlerle doldurursunuz, ve curl kullanmak, bir tür yük dengelemesi oluşturmadıkça başka bir sunucuyu bağlar.

Gearman'ı birkaç durumda kullandım ve bu tür bir kullanım durumu için daha iyi buluyorum. Temelde sunucu tarafından yapılması gereken tüm işlerin kuyruğunu işlemek ve her biri gerektiği kadar çalışan işleminin örneklerini çalıştırabilecek işçi sunucularını döndürmek ve sayısını artırmak için tek bir iş kuyruğu sunucusu kullanabilirim işçi sunucuları gerektiği gibi ve gerektiğinde onları aşağı döndürün. Ayrıca, gerektiğinde işçi süreçlerini tamamen kapatmama izin verin ve işçiler tekrar çevrimiçi olana kadar işleri sıraya koyarım.


-4

PHP tek iş parçacıklı bir dildir, bu nedenle execveya ile eşzamansız bir işlem başlatmak için resmi bir yol yoktur popen. Burada bununla ilgili bir blog yazısı var . MySQL'de bir kuyruk fikriniz de iyi bir fikirdir.

Buradaki özel gereksiniminiz kullanıcıya bir e-posta göndermek içindir. Bir e-posta göndermenin gerçekleştirilmesi oldukça önemsiz ve hızlı bir görev olduğu için bunu neden zaman uyumsuz olarak yapmaya çalıştığınızı merak ediyorum. Tonlarca e-posta gönderiyorsanız ve ISS'niz sizi spam spam şüphesiyle engelliyorsa, bu kuyruk için bir neden olabilir, ancak bunun dışında bunu bu şekilde yapmak için herhangi bir neden düşünemiyorum.


E-posta sadece bir örnektir, çünkü diğer görevleri açıklamak daha karmaşıktır ve aslında sorunun konusu değildir. E-posta gönderme yöntemimiz, uzak sunucu postayı kabul edene kadar e-posta komutu döndürmez. Bazı posta sunucularının, posta kabul etmeden önce (muhtemelen spambotlarla savaşmak için) uzun gecikmeler (10-20 saniye gecikmeler gibi) ekleyecek şekilde yapılandırıldığını ve bu gecikmelerin kullanıcılarımıza iletileceğini tespit ettik. Şimdi, gönderilecek postaları sıraya koymak için yerel bir posta sunucusu kullanıyoruz, bu yüzden bu özel uygulama geçerli değil, ancak benzer nitelikteki başka görevlerimiz var.
davr

Örneğin: ssl ve 465 numaralı bağlantı noktasıyla Google Apps Smtp aracılığıyla e-posta göndermek normalden daha uzun sürer.
Gixty
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.