Jalankan Tugas PHP secara Asinkron

144

Saya bekerja pada aplikasi web yang agak besar, dan backend kebanyakan dalam PHP. Ada beberapa tempat dalam kode di mana saya perlu menyelesaikan beberapa tugas, tetapi saya tidak ingin membuat pengguna menunggu hasilnya. Misalnya, saat membuat akun baru, saya harus mengirim mereka email selamat datang. Tetapi ketika mereka menekan tombol 'Selesai Pendaftaran', saya tidak ingin membuat mereka menunggu sampai email benar-benar dikirim, saya hanya ingin memulai prosesnya, dan segera mengembalikan pesan kepada pengguna.

Sampai sekarang, di beberapa tempat saya telah menggunakan sesuatu yang terasa seperti hack dengan exec (). Pada dasarnya melakukan hal-hal seperti:

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

Yang tampaknya berhasil, tapi saya bertanya-tanya apakah ada cara yang lebih baik. Saya sedang mempertimbangkan untuk menulis sebuah sistem yang mengantri tugas-tugas dalam tabel MySQL, dan skrip PHP yang sudah berjalan lama yang menanyakan tabel itu satu kali per detik, dan menjalankan tugas-tugas baru yang ditemukannya. Ini juga akan memiliki keuntungan membiarkan saya membagi tugas di antara beberapa mesin pekerja di masa depan jika saya perlu.

Apakah saya menciptakan kembali roda? Apakah ada solusi yang lebih baik daripada hack exec () atau antrian MySQL?

davr
sumber

Jawaban:

80

Saya telah menggunakan pendekatan antrian, dan itu berfungsi dengan baik karena Anda dapat menunda pemrosesan itu hingga beban server Anda menganggur, memungkinkan Anda mengelola beban dengan cukup efektif jika Anda dapat mempartisi "tugas yang tidak mendesak" dengan mudah.

Memutar sendiri tidak terlalu rumit, berikut beberapa opsi lain untuk memeriksa:

  • GearMan - jawaban ini ditulis pada tahun 2009, dan sejak itu GearMan terlihat sebagai pilihan yang populer, lihat komentar di bawah.
  • ActiveMQ jika Anda ingin antrian pesan sumber terbuka penuh meledak.
  • ZeroMQ - ini adalah library socket yang cukup keren yang membuatnya mudah untuk menulis kode terdistribusi tanpa harus terlalu khawatir tentang pemrograman socket itu sendiri. Anda dapat menggunakannya untuk antrian pesan pada satu host - Anda hanya perlu mendorong aplikasi web Anda ke antrian yang akan dikonsumsi aplikasi konsol yang berjalan terus-menerus pada kesempatan yang sesuai berikutnya
  • beanstalkd - hanya menemukan ini saat menulis jawaban ini, tetapi terlihat menarik
  • dropr adalah proyek antrian pesan berbasis PHP, tetapi belum dipelihara secara aktif sejak Sep 2010
  • php-enqueue adalah pembungkus yang dikelola baru-baru ini (2017) di sekitar berbagai sistem antrian
  • Akhirnya, posting blog tentang menggunakan memcached untuk antrian pesan

Pendekatan lain, mungkin lebih sederhana, adalah dengan menggunakan ign_user_abort - setelah Anda mengirim halaman ke pengguna, Anda dapat melakukan pemrosesan akhir tanpa takut penghentian prematur, meskipun ini memiliki efek muncul untuk memperpanjang pemuatan halaman dari pengguna. perspektif.

Paul Dixon
sumber
Terima kasih untuk semua tipsnya. Yang spesifik tentang ign_user_abort tidak benar-benar membantu dalam kasus saya, seluruh tujuan saya adalah untuk menghindari penundaan yang tidak perlu bagi pengguna.
davr
2
Jika Anda mengatur header HTTP Panjang Konten di respons "Terima Kasih Untuk Mendaftar", maka browser akan menutup koneksi setelah jumlah byte yang ditentukan diterima. Ini membiarkan proses sisi server berjalan (dengan anggapan ign_user_abort disetel) tanpa membuat pengguna akhir menunggu. Tentu saja Anda perlu menghitung ukuran konten respons Anda sebelum merender tajuk, tetapi cukup mudah untuk tanggapan singkat.
Peter
1
Gearman ( gearman.org ) adalah antrian pesan open source yang bagus yang bersifat lintas platform. Anda dapat menulis pekerja dalam bahasa C, PHP, Perl atau bahasa lainnya. Ada plugin UDF Gearman untuk MySQL dan Anda juga dapat menggunakan Net_Gearman dari PHP atau klien pir gearman.
Justin Swanhart
Gearman akan menjadi apa yang saya rekomendasikan hari ini (tahun 2015) atas sistem antrian pekerjaan khusus.
Peter
Pilihan lain adalah mengatur server node js untuk menangani permintaan dan mengembalikan respons cepat dengan tugas di antaranya. Banyak hal di dalam skrip simpul js dieksekusi secara tidak sinkron seperti permintaan http.
Zordon
22

Ketika Anda hanya ingin mengeksekusi satu atau beberapa permintaan HTTP tanpa harus menunggu respons, ada solusi PHP sederhana juga.

Dalam skrip panggilan:

$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

Pada script.php yang disebut, Anda dapat menjalankan fungsi-fungsi PHP ini di baris pertama:

ignore_user_abort(true);
set_time_limit(0);

Ini menyebabkan skrip untuk terus berjalan tanpa batas waktu ketika koneksi HTTP ditutup.

Markus
sumber
set_time_limit tidak berpengaruh jika php dijalankan dalam safe mode
Baptiste Pernet
17

Cara lain untuk proses garpu adalah melalui curl. Anda dapat mengatur tugas internal Anda sebagai layanan web. Sebagai contoh:

Kemudian di skrip yang diakses pengguna Anda melakukan panggilan ke layanan:

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

Layanan Anda dapat melacak antrian tugas dengan mysql atau apa pun yang Anda suka intinya adalah: semuanya sudah termasuk dalam layanan dan skrip Anda hanya menggunakan URL. Ini membebaskan Anda untuk memindahkan layanan ke mesin / server lain jika perlu (dengan mudah dapat diskalakan).

Menambahkan otorisasi http atau skema otorisasi khusus (seperti layanan web Amazon) memungkinkan Anda membuka tugas untuk dikonsumsi oleh orang / layanan lain (jika Anda mau) dan Anda bisa membawanya lebih jauh dan menambahkan layanan pemantauan di atas untuk melacak status antrian dan tugas.

Memang butuh sedikit kerja pengaturan tetapi ada banyak manfaat.

rojoca
sumber
1
Saya tidak suka pendekatan ini karena terlalu membebani server web
Oved Yavine
7

Saya telah menggunakan Beanstalkd untuk satu proyek, dan berencana untuk kembali. Saya menemukan ini sebagai cara terbaik untuk menjalankan proses asinkron.

Beberapa hal yang telah saya lakukan adalah:

  • Pengubahan ukuran gambar - dan dengan antrian yang ringan yang diteruskan ke skrip PHP berbasis CLI, mengubah ukuran gambar besar (2mb +) bekerja dengan baik, tetapi mencoba mengubah ukuran gambar yang sama dalam contoh mod_php secara teratur mengalami masalah ruang memori (I membatasi proses PHP hingga 32MB, dan ukurannya membutuhkan lebih dari itu)
  • cek dekat waktu dekat - beanstalkd memiliki penundaan yang tersedia untuknya (sediakan pekerjaan ini hanya untuk berjalan setelah X detik) - jadi saya dapat menjalankan 5 atau 10 cek untuk suatu acara, sedikit waktu yang lalu

Saya menulis sistem berbasis Zend-Framework untuk mendekode url yang 'bagus', jadi misalnya, untuk mengubah ukuran gambar yang akan dipanggil QueueTask('/image/resize/filename/example.jpg'). URL pertama kali diterjemahkan ke array (modul, controller, action, parameter), dan kemudian dikonversi ke JSON untuk injeksi ke antrian itu sendiri.

Cli script yang berjalan lama kemudian mengambil pekerjaan dari antrian, menjalankannya (melalui Zend_Router_Simple), dan jika diperlukan, masukkan informasi ke dalam memcached untuk situs web PHP untuk mengambil sebagaimana diperlukan ketika itu selesai.

Satu kerutan yang saya lakukan juga adalah bahwa cli-script hanya berjalan selama 50 loop sebelum memulai kembali, tetapi jika memang ingin memulai kembali seperti yang direncanakan, itu akan segera melakukannya (dijalankan melalui bash-script). Jika ada masalah dan saya lakukan exit(0)(nilai default untuk exit;atau die();) pertama-tama akan berhenti selama beberapa detik.

Alister Bulman
sumber
Saya suka tampilan beanstalkd, begitu mereka menambahkan kegigihan saya pikir itu akan menjadi sempurna.
davr
Itu sudah ada di basis kode dan sedang distabilkan. Saya juga menantikan 'pekerjaan yang ditentukan', jadi saya bisa melempar barang ke sana, tapi tahu itu tidak akan ditambahkan jika sudah ada di sana. Bagus untuk acara reguler.
Alister Bulman
@AlisterBulman dapatkah Anda memberikan lebih banyak informasi atau contoh untuk "Sebuah skrip cli yang berjalan lama kemudian mengambil pekerjaan dari antrian". Saya mencoba membuat skrip cli untuk aplikasi saya.
Sasi varna kumar
7

Jika hanya soal menyediakan tugas-tugas mahal, jika php-fpm didukung, mengapa tidak menggunakan fastcgi_finish_request()fungsi?

Fungsi ini mem-flush semua data respons ke klien dan menyelesaikan permintaan. Ini memungkinkan tugas yang memakan waktu dilakukan tanpa membiarkan koneksi ke klien terbuka.

Anda tidak benar-benar menggunakan asinkronisitas dengan cara ini:

  1. Buat semua kode utama Anda terlebih dahulu.
  2. Jalankan fastcgi_finish_request().
  3. Buat semua barang berat.

Sekali lagi diperlukan php-fpm.

Denys Gorobchenko
sumber
5

Ini adalah kelas sederhana yang saya kodekan untuk aplikasi web saya. Ini memungkinkan untuk forking skrip PHP dan skrip lainnya. Bekerja pada UNIX dan 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);
        }
    }
}
Andrew Moore
sumber
4

Ini adalah metode yang sama yang telah saya gunakan selama beberapa tahun sekarang dan saya belum melihat atau menemukan sesuatu yang lebih baik. Seperti yang dikatakan orang, PHP adalah single threaded, jadi tidak banyak yang bisa Anda lakukan.

Saya sebenarnya telah menambahkan satu level ekstra untuk ini dan itu mendapatkan dan menyimpan id proses. Ini memungkinkan saya untuk mengalihkan ke halaman lain dan membuat pengguna duduk di halaman itu, menggunakan AJAX untuk memeriksa apakah prosesnya sudah selesai (id proses sudah tidak ada lagi). Ini berguna untuk kasus di mana panjang skrip akan menyebabkan browser kehabisan waktu, tetapi pengguna harus menunggu skrip itu selesai sebelum langkah berikutnya. (Dalam kasus saya itu sedang memproses file ZIP besar dengan CSV seperti file yang menambahkan hingga 30.000 catatan ke database setelah itu pengguna perlu mengkonfirmasi beberapa informasi.)

Saya juga telah menggunakan proses serupa untuk pembuatan laporan. Saya tidak yakin saya akan menggunakan "pemrosesan latar belakang" untuk sesuatu seperti email, kecuali ada masalah nyata dengan SMTP yang lambat. Sebaliknya saya mungkin menggunakan tabel sebagai antrian dan kemudian memiliki proses yang berjalan setiap menit untuk mengirim email dalam antrian. Anda harus waspada mengirim email dua kali atau masalah serupa lainnya. Saya akan mempertimbangkan proses antrian yang sama untuk tugas-tugas lain juga.

Darryl Hein
sumber
1
Metode apa yang Anda maksud dalam kalimat pertama Anda?
Simon East
3

PHP TELAH multithreading, tidak diaktifkan secara default, ada ekstensi yang disebut pthreads yang melakukan hal itu. Anda akan membutuhkan php yang dikompilasi dengan ZTS. Tautan (Utas Aman):

Contohnya

Tutorial lain

pthreads Ekstensi PECL

Omar S.
sumber
2

Merupakan ide bagus untuk menggunakan CURL seperti yang disarankan oleh rojoca.

Berikut ini sebuah contoh. Anda dapat memonitor text.txt saat skrip berjalan di latar:

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

?>
Kjeld
sumber
2
Akan sangat membantu jika kode sumber akan dikomentari. Saya tidak tahu apa yang terjadi di sana dan bagian mana yang menjadi contoh dan bagian mana yang dapat digunakan kembali untuk tujuan saya sendiri.
Thomas Tempelmann
1

Sayangnya PHP tidak memiliki kemampuan threading asli apa pun. Jadi saya pikir dalam hal ini Anda tidak punya pilihan selain menggunakan semacam kode khusus untuk melakukan apa yang ingin Anda lakukan.

Jika Anda mencari di internet untuk hal-hal threading PHP, beberapa orang telah menemukan cara untuk mensimulasikan thread pada PHP.

Peter D
sumber
1

Jika Anda mengatur header HTTP Panjang Konten di respons "Terima Kasih Untuk Mendaftar", maka browser akan menutup koneksi setelah jumlah byte yang ditentukan diterima. Ini membiarkan proses sisi server berjalan (dengan anggapan ign_user_abort disetel) sehingga dapat selesai bekerja tanpa membuat pengguna akhir menunggu.

Tentu saja Anda perlu menghitung ukuran konten respons Anda sebelum merender tajuk, tetapi cukup mudah untuk tanggapan singkat (tulis keluaran ke string, panggil strlen (), panggil panggilan (), panggil tajuk (), render string).

Pendekatan ini memiliki keuntungan karena tidak memaksa Anda untuk mengelola antrian "ujung depan", dan meskipun Anda mungkin perlu melakukan beberapa pekerjaan di ujung belakang untuk mencegah balap proses anak HTTP saling menginjak, itu adalah sesuatu yang perlu Anda lakukan sudah bagaimanapun juga.

Peter
sumber
Ini sepertinya tidak berhasil. Ketika saya menggunakan header('Content-Length: 3'); echo '1234'; sleep(5);itu meskipun browser hanya membutuhkan 3 karakter, itu masih menunggu selama 5 detik sebelum menunjukkan respons. Apa yang saya lewatkan?
Thomas Tempelmann
@ThomasTempelmann - Anda mungkin perlu memanggil flush () untuk memaksa output benar-benar di-render segera, jika tidak output akan di-buffer sampai skrip Anda keluar atau cukup data dikirim ke STDOUT untuk menyiram buffer.
Peter
Saya sudah mencoba banyak cara untuk menyiram, ditemukan di sini di SO. Tidak ada yang membantu. Dan data tersebut tampaknya dikirim juga tanpa gzip, seperti yang bisa diketahui phpinfo(). Satu-satunya hal lain yang dapat saya bayangkan adalah bahwa saya harus mencapai ukuran buffer minimum terlebih dahulu, misalnya 256 atau lebih byte.
Thomas Tempelmann
@ThomasTempelmann - Saya tidak melihat apa pun dalam pertanyaan Anda atau jawaban saya tentang gzip (biasanya masuk akal untuk membuat skenario paling sederhana bekerja terlebih dahulu sebelum menambahkan lapisan kompleksitas). Untuk mengetahui kapan server benar-benar mengirim data, Anda dapat menggunakan packet sniffer dari plugin browser (seperti fiddler, tamperdata, dll.). Kemudian, jika Anda menemukan bahwa server web benar-benar menahan semua keluaran skrip sampai keluar terlepas dari pembilasan, maka Anda perlu memodifikasi konfigurasi server web Anda (tidak ada yang dapat dilakukan skrip PHP Anda dalam kasus itu).
Peter
Saya menggunakan layanan web virtual, jadi saya memiliki sedikit kontrol atas konfigurasinya. Saya berharap dapat menemukan saran lain tentang apa yang bisa menjadi pelakunya, tetapi tampaknya jawaban Anda tidak berlaku secara universal seperti yang terlihat. Terlalu banyak hal yang bisa salah, jelas. Solusi Anda tentu jauh lebih mudah diimplementasikan daripada semua jawaban lain yang diberikan di sini. Sayang sekali itu tidak berhasil untuk saya.
Thomas Tempelmann
1

Jika Anda tidak ingin ActiveMQ yang penuh sesak, saya sarankan untuk mempertimbangkan RabbitMQ . RabbitMQ adalah pesan ringan yang menggunakan standar AMQP .

Saya sarankan untuk juga melihat ke php-amqplib - pustaka klien AMQP yang populer untuk mengakses broker pesan berbasis AMQP.

phpPhil
sumber
0

Saya pikir Anda harus mencoba teknik ini. Ini akan membantu untuk memanggil sebanyak halaman yang Anda suka semua halaman akan berjalan sekaligus secara mandiri tanpa menunggu setiap respons halaman sebagai tidak sinkron.

cornjobpage.php // mainpage

    <?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: jika Anda ingin mengirim parameter url sebagai loop kemudian ikuti jawaban ini: https://stackoverflow.com/a/41225209/6295712

Hassan Saeed
sumber
0

Memunculkan proses baru di server menggunakan exec()atau langsung di server lain menggunakan curl tidak skala semua sama sekali, jika kita pergi untuk eksekutif Anda pada dasarnya mengisi server Anda dengan proses berjalan lama yang dapat ditangani oleh server non web yang menghadap lainnya, dan menggunakan ikatan ikal ke server lain kecuali jika Anda membangun semacam load balancing.

Saya telah menggunakan Gearman dalam beberapa situasi dan saya merasa lebih baik untuk kasus penggunaan semacam ini. Saya dapat menggunakan server antrian pekerjaan tunggal untuk menangani antrian pada semua pekerjaan yang perlu dilakukan oleh server dan memutar server pekerja, yang masing-masing dapat menjalankan sebanyak contoh proses pekerja sesuai kebutuhan, dan meningkatkan jumlah server pekerja sesuai kebutuhan dan putar ke bawah saat tidak diperlukan. Ini juga memungkinkan saya mematikan proses pekerja sepenuhnya saat diperlukan dan mengantre pekerjaan sampai pekerja kembali online.

Chris Rutherfurd
sumber
-4

PHP adalah bahasa single-threaded, jadi tidak ada cara resmi untuk memulai proses asinkron dengannya selain menggunakan execatau popen. Ada posting blog tentang itu di sini . Gagasan Anda untuk antrian di MySQL juga merupakan ide bagus.

Persyaratan khusus Anda di sini adalah untuk mengirim email ke pengguna. Saya ingin tahu mengapa Anda mencoba melakukan hal itu secara tidak sinkron karena mengirim email adalah tugas yang cukup sepele dan cepat untuk dilakukan. Saya kira jika Anda mengirim banyak email dan ISP Anda memblokir Anda karena dicurigai melakukan spamming, itu mungkin salah satu alasan untuk mengantri, tetapi selain itu saya tidak dapat memikirkan alasan untuk melakukannya dengan cara ini.

Marc W
sumber
Email itu hanya sebuah contoh, karena tugas-tugas lain lebih rumit untuk dijelaskan, dan itu sebenarnya bukan inti dari pertanyaan. Cara kami dulu mengirim email, perintah email tidak akan kembali sampai server jauh menerima email. Kami menemukan bahwa beberapa server email dikonfigurasikan untuk menambahkan penundaan yang lama (seperti penundaan 10-20 detik) sebelum menerima email (mungkin untuk memerangi robot spam), dan penundaan ini kemudian akan diteruskan ke pengguna kami. Sekarang, kami menggunakan server surat lokal untuk mengantri surat yang akan dikirim, jadi yang satu ini tidak berlaku, tetapi kami memiliki tugas lain yang serupa.
David
Sebagai contoh: mengirim email melalui Google Apps Smtp dengan ssl dan port 465 membutuhkan waktu lebih lama dari biasanya.
Gixty