Setelah rilis PHP7, menjadi mungkin untuk menulis aplikasi berumur panjang dengan biaya yang relatif rendah. Untuk programmer, proyek-proyek seperti prooph
, broadway
, prooph
, messenger
telah tersedia, penulis yang mengambil solusi untuk masalah yang paling umum. Tetapi bagaimana jika Anda mengambil langkah kecil ke depan, mempelajari pertanyaan itu?
Mari kita coba mencari tahu nasib sepeda lain, yang memungkinkan Anda untuk mengimplementasikan aplikasi Publikasikan / Berlangganan.
Untuk memulainya, kami akan mencoba meninjau tren saat ini secara singkat di dunia PHP, serta melihat sekilas operasi asinkron.
PHP dibuat untuk mati
Untuk waktu yang lama, PHP terutama digunakan dalam alur kerja permintaan / respons. Dari sudut pandang pengembang, ini cukup mudah, karena tidak perlu khawatir tentang kebocoran memori, memonitor koneksi.
Semua pertanyaan akan dieksekusi dalam isolasi satu sama lain, sumber daya yang digunakan akan dibebaskan, dan koneksi, misalnya, ke database akan ditutup ketika proses selesai.
Sebagai contoh, Anda dapat mengambil aplikasi CRUD biasa yang ditulis berdasarkan kerangka kerja Symfony. Untuk membaca dari database dan mengembalikan JSON, perlu melakukan sejumlah langkah (untuk menghemat ruang dan waktu, tidak termasuk langkah-langkah untuk menghasilkan / menjalankan opcode):
- Analisis konfigurasi;
- Kompilasi kontainer;
- Meminta Routing
- Pemenuhan;
- Rendering hasilnya.
Seperti dalam kasus PHP (menggunakan akselerator), kerangka kerja secara aktif menggunakan caching (beberapa tugas tidak akan selesai pada permintaan berikutnya), serta inisialisasi tertunda. Dimulai dengan versi 7.4, preload akan tersedia, yang selanjutnya akan mengoptimalkan inisialisasi aplikasi.
Namun, tidak mungkin untuk sepenuhnya menghapus semua biaya overhead untuk inisialisasi.
Mari kita bantu PHP bertahan hidup
Solusi untuk masalah ini terlihat sangat sederhana: jika Anda menjalankan aplikasi setiap kali terlalu mahal, Anda perlu menginisialisasi sekali dan kemudian hanya mengirimkan permintaan ke sana, mengendalikan eksekusi mereka.
Ada proyek dalam ekosistem PHP seperti php-pm dan RoadRunner . Keduanya secara konseptual melakukan hal yang sama:
- Proses induk dibuat yang bertindak sebagai pengawas;
- Kumpulan proses anak dibuat;
- Ketika permintaan diterima, master mengambil proses dari kumpulan dan meneruskan permintaan itu. Klien tertunda pada saat ini;
- Setelah tugas selesai, master mengembalikan hasilnya ke klien, dan proses anak dikirim kembali ke kumpulan.
Jika ada proses anak yang mati, supervisor membuatnya lagi dan menambahkannya ke kolam. Kami membuat daemon dari aplikasi kami dengan satu tujuan tunggal: untuk menghapus overhead inisialisasi, secara signifikan meningkatkan kecepatan pemrosesan permintaan. Ini adalah cara yang paling mudah untuk meningkatkan produktivitas, tetapi bukan satu-satunya.
Catatan:
banyak contoh dari seri "ambil ReactPHP dan percepat Laravel N kali" berjalan di jaringan. Penting untuk memahami perbedaan antara melakukan demonisasi (dan, sebagai hasilnya, menghemat waktu saat bootstrap aplikasi) dan multitasking.
Saat menggunakan php-pm atau roadrunner, kode Anda tidak menjadi non-pemblokiran. Anda cukup menghemat waktu pada inisialisasi.
Membandingkan php-pm, roadrunner dan ReactPHP / Amp / Swoole secara definisi salah.
PHP dan I / O
Interaksi dengan I / O di PHP secara default dieksekusi dalam mode blocking. Ini berarti bahwa jika kami menjalankan permintaan untuk memperbarui informasi dalam tabel, aliran eksekusi akan berhenti menunggu jawaban dari database. Semakin banyak panggilan seperti itu dalam proses memproses permintaan, semakin lama sumber daya server idle. Memang, dalam proses pemrosesan permintaan, kita perlu pergi ke database beberapa kali, menulis sesuatu ke log, dan mengembalikan hasilnya kepada klien, pada akhirnya - juga operasi pemblokiran.
Bayangkan Anda adalah operator call center dan Anda perlu menelepon 50 pelanggan dalam satu jam.
Anda menekan nomor pertama, dan nomor itu sedang sibuk (pelanggan membahas melalui telepon seri terakhir dari Game of Thrones dan apa seri yang digulirkan ke dalamnya).
Dan sekarang Anda sedang duduk dan berusaha meraihnya sebelum kemenangan. Waktu terus berjalan, shift hampir berakhir. Setelah kehilangan 40 menit untuk mencapai pelanggan pertama, Anda melewatkan kesempatan untuk menghubungi orang lain dan diterima secara alami dari bos.
Tetapi Anda dapat melakukan sebaliknya: jangan menunggu sampai pelanggan pertama bebas dan segera setelah Anda mendengar bunyi bip, tutup telepon dan mulailah memutar nomor berikutnya. Anda dapat kembali ke yang pertama sedikit kemudian.
Dengan pendekatan ini, peluang menelepon jumlah maksimum orang sangat meningkat, dan kecepatan pekerjaan Anda tidak bergantung pada tugas paling lambat.
Kode yang tidak memblokir utas eksekusi (tidak menggunakan pemblokiran panggilan I / O, serta fungsi seperti sleep()
), disebut asinkron.
Mari kita kembali ke aplikasi Symfony CRUD kami. Hampir tidak mungkin untuk membuatnya bekerja dalam mode asinkron karena banyaknya penggunaan fungsi pemblokiran: semua bekerja dengan konfigurasi, cache, logging, rendering respon, interaksi dengan database.
Tetapi ini semua adalah konvensi, mari kita coba melempar Symfony dan menggunakan Amp , yang menyediakan implementasi Event Loop (termasuk sejumlah binder), Janji dan Coroutine, sebagai ceri pada kue untuk menyelesaikan masalah kita.
Janji adalah salah satu cara untuk mengatur kode asinkron. Sebagai contoh, kita perlu mengakses beberapa sumber daya http.
Kami membuat objek permintaan dan meneruskannya ke transportasi, yang dijanjikan Promise kepada kami berisi keadaan saat ini. Ada tiga kemungkinan status:
- Berhasil: permintaan kami berhasil diselesaikan;
- Kesalahan: selama eksekusi permintaan, terjadi kesalahan (misalnya, server mengembalikan respons 500);
- Menunggu: Pemrosesan permintaan belum dimulai.
Setiap Janji memiliki satu metode (dalam contoh, Janji diurai oleh Amp ) - onResolve()
, di mana fungsi panggilan balik dengan dua argumen dilewatkan
$promise->onResolve( static function(?/Throwable $throwable, $result): void { if(null !== $throwable) { return; } } );
Setelah kami menerima Janji, muncul pertanyaan: siapa yang akan memantau statusnya dan memberi tahu kami tentang perubahan status?
Untuk ini, Event Loop digunakan.
Intinya, Perulangan Kejadian adalah penjadwal yang memantau eksekusi. Segera setelah tugas selesai (tidak peduli bagaimana), callable yang kami berikan kepada Promise akan dipanggil.
Sedangkan untuk nuansa, saya akan merekomendasikan membaca artikel dari Nikita Popov: Multitasking kooperatif menggunakan coroutine . Ini akan membantu untuk memberikan kejelasan tentang apa yang terjadi dan di mana generatornya.
Berbekal pengetahuan baru, mari kita coba kembali ke tugas rendering JSON kami.
Contoh memproses permintaan http yang masuk menggunakan amphp / http-server .
Segera setelah kami menerima permintaan, pembacaan asinkron dari database dilakukan (kami mendapatkan Janji) dan setelah selesai pengguna akan menerima JSON yang didambakan, dibentuk berdasarkan data yang diterima.
Jika kita perlu mendengarkan satu port dari beberapa proses, kita dapat melihat ke amphp / cluster
Perbedaan utama adalah bahwa satu proses tunggal dapat melayani beberapa permintaan sekaligus karena fakta bahwa utas eksekusi tidak diblokir. Klien akan menerima jawabannya ketika pembacaan dari database selesai, dan sementara tidak ada jawaban, Anda dapat mulai melayani permintaan berikutnya.
Dunia asinkron PHP yang luar biasa
Penafian
Asynchronous PHP dianggap dalam konteks eksotik dan tidak dianggap sesuatu yang sehat / normal. Pada dasarnya, mereka akan menunggu untuk tertawa dalam gaya "ambil GO / Kotlin, bodoh", dll. Saya tidak akan mengatakan bahwa orang-orang ini salah, tetapi ...
Ada sejumlah proyek yang membantu menulis kode PHP yang tidak menghalangi. Dalam kerangka artikel ini, saya tidak akan sepenuhnya menganalisis semua pro dan kontra, tetapi saya akan mencoba untuk memeriksa masing-masing secara dangkal.
Kerangka kerja asinkron yang ditulis berbeda dengan yang lain dalam C dan dikirim sebagai ekstensi ke PHP. Ia mungkin memiliki indikator kinerja terbaik saat ini.
Ada implementasi saluran, corutin dan hal-hal lezat lainnya, tetapi ia memiliki 1 minus besar - dokumentasi. Meskipun sebagian dalam bahasa Inggris, menurut saya itu tidak terlalu detail, dan apinya sendiri tidak terlalu jelas.
Sedangkan untuk komunitas, itu juga tidak semua sederhana dan tidak ambigu. Secara pribadi, saya tidak tahu satu pun orang hidup yang menggunakan Swoole dalam pertempuran. Mungkin saya akan mengatasi ketakutan saya dan pindah ke dia, tetapi ini tidak akan terjadi dalam waktu dekat.
Untuk minus, Anda juga dapat menambahkan bahwa untuk berkontribusi ke proyek (menggunakan permintaan tarik) dengan perubahan apa pun juga sulit jika Anda tidak tahu C di tingkat yang tepat.
Jika kehilangan kecepatan untuk kompetitornya (berbicara tentang Swoole), maka itu tidak terlalu terlihat dan perbedaan dalam sejumlah skenario dapat diabaikan.
Ini memiliki integrasi dengan ReactPHP, yang pada gilirannya memperluas jumlah implementasi masalah infrastruktur. Untuk menghemat ruang, saya akan menjelaskan kontra bersama dengan ReactPHP.
Plus termasuk komunitas yang cukup besar dan sejumlah besar contoh. Kontra mulai muncul dalam proses penggunaan - ini adalah konsep Janji.
Jika Anda perlu melakukan beberapa operasi asinkron, kode berubah menjadi tempat sampah tak berujung lalu (di sini adalah contoh koneksi sederhana ke RabbiqMQ tanpa membuat pertukaran / antrian dan pengikat mereka).
Dengan beberapa perbaikan dengan file (dianggap norma), Anda bisa mendapatkan implementasi coroutine, yang akan membantu menyingkirkan Promise hell.
Tanpa proyek recoilphp / recoil, menggunakan ReactPHP, menurut saya, tidak mungkin dalam aplikasi yang waras.
Selain itu, selain yang lainnya, orang merasa bahwa perkembangannya telah sangat melambat. Tidak cukup, misalnya, kerja normal dengan PostgreSQL.
Menurut pendapat saya, opsi terbaik yang ada saat ini.
Selain Janji yang biasa, ada implementasi Coroutine, yang sangat memudahkan proses pengembangan dan kode terlihat paling akrab bagi programmer PHP.
Pengembang terus-menerus menambah dan meningkatkan proyek, dengan umpan balik juga tidak ada masalah.
Sayangnya, dengan semua kelebihan kerangka kerja, komunitasnya relatif kecil, tetapi pada saat yang sama ada implementasi, misalnya bekerja dengan PostgreSQL, serta semua hal-hal dasar (sistem file, klien http, DNS, dll).
Saya masih tidak begitu mengerti nasib proyek ext-async, tetapi orang-orang tetap melakukannya. Apa yang akan terjadi pada ini dalam versi ke-3, waktu akan memberi tahu.
Memulai
Jadi, kami menyortir bagian teoretis sedikit, saatnya untuk melanjutkan berlatih dan mengisi benjolan.
Pertama, kami sedikit memformalkan persyaratan:
- Pesan asinkron (konsep
message
itu sendiri dapat dibagi menjadi 2 jenis)
command
: menunjukkan kebutuhan untuk menyelesaikan tugas. Tidak mengembalikan hasil (setidaknya dalam kasus komunikasi asinkron);event
: melaporkan setiap perubahan status (misalnya, sebagai hasil dari suatu perintah).
- Format non-blocking untuk bekerja dengan I / O;
- Kemampuan untuk dengan mudah meningkatkan jumlah prosesor;
- Kemampuan menulis penangan pesan dalam bahasa apa pun.
Pesan apa pun pada dasarnya adalah struktur sederhana dan hanya dibagikan oleh semantik. Penamaan pesan sangat penting dari sudut pandang memahami jenis dan tujuan (meskipun hal ini diabaikan dalam contoh).
Untuk daftar persyaratan, implementasi sederhana dari pola Publikasikan / Berlangganan paling cocok.
Untuk memastikan eksekusi terdistribusi, kami akan menggunakan RabbitMQ sebagai broker pesan.
Prototipe ini ditulis menggunakan ReactPHP , Bunny, dan DoctrineDBAL .
Pembaca yang penuh perhatian mungkin memperhatikan bahwa Dbal menggunakan panggilan pemblokiran pdo / mysqli secara internal, tetapi pada tahap saat ini ini tidak terlalu penting, karena Anda harus memahami apa yang harus terjadi pada akhirnya.
Salah satu masalah adalah kurangnya perpustakaan untuk bekerja dengan PostgreSQL. Ada beberapa konsep, tetapi ini tidak cukup untuk pekerjaan penuh (lebih lanjut tentang ini di bawah).
Setelah penelitian singkat, ReactPHP dihilangkan untuk Amp, karena relatif sederhana dan sangat aktif berkembang.
Transportasi RabbitMQ
Tetapi dengan semua kelebihan Amp, ada 1 masalah: Amp tidak memiliki driver untuk RabbitMQ ( Kelinci hanya mendukung ReactPHP).
Secara teori, Amp memungkinkan Anda untuk menggunakan Janji dari pesaing. Tampaknya semuanya harus sederhana, tetapi ReactPHP menggunakan Event Loop untuk bekerja dengan soket di perpustakaan.
Pada satu titik waktu, jelas, dua Event Loops yang berbeda tidak dapat dimulai, jadi saya tidak dapat menggunakan fungsi adapt () .
Sayangnya, kualitas kode dalam kelinci meninggalkan banyak yang harus diinginkan dan tidak mungkin untuk mengganti satu implementasi dengan yang lain. Agar tidak menghentikan pekerjaan, diputuskan untuk menulis ulang perpustakaan sedikit sehingga berfungsi dengan Amp dan tidak mengarah ke menghalangi aliran eksekusi.
Adaptasi ini terlihat sangat menakutkan, sepanjang waktu saya sangat malu akan hal itu, tetapi yang paling penting, itu berhasil. Nah, karena tidak ada yang lebih permanen daripada sementara, adaptor tetap mengantisipasi seseorang yang tidak terlalu malas untuk berurusan dengan implementasi pengemudi.
Dan orang seperti itu ditemukan. Proyek PHPinnacle , antara lain, menyediakan implementasi adaptor yang dirancang untuk Amp.
Nama penulisnya adalah Anton Shabovta, yang akan berbicara tentang asynchronous php dalam kerangka kerja PHP Russia dan tentang mengembangkan driver untuk PHP di masa depan .
PostgreSQL
Fitur kedua dari karya ini adalah interaksi dengan database. Dalam kondisi PHP "tradisional", semuanya sederhana: kami memiliki koneksi dan semua permintaan dieksekusi secara berurutan.
Dalam kasus eksekusi asinkron, kita harus dapat secara bersamaan menjalankan beberapa permintaan (misalnya, 3 transaksi). Agar dapat melakukan ini, implementasi kumpulan koneksi diperlukan.
Mekanisme kerjanya cukup sederhana:
- kami membuka koneksi N saat startup (atau menunda inisialisasi, bukan intinya);
- jika perlu, kami mengambil koneksi dari kolam, memastikan bahwa tidak ada orang lain yang dapat menggunakannya;
- Kami mengeksekusi permintaan dan menghancurkan koneksi atau mengembalikannya ke kolam (lebih disukai).
Pertama, ini memungkinkan kita untuk memulai beberapa transaksi sekaligus, dan kedua, mempercepat pekerjaan karena kehadiran koneksi yang sudah terbuka. Amp memiliki komponen amphp / postgres . Dia mengurus koneksi: memonitor jumlah mereka, seumur hidup, dan semua ini tanpa menghalangi aliran eksekusi.
Ngomong-ngomong, saat menggunakan, misalnya, ReactPHP, Anda harus mengimplementasikannya sendiri jika Anda ingin bekerja dengan database.
Mutex
Untuk operasi aplikasi yang efektif dan, yang paling penting, yang tepat, perlu untuk mengimplementasikan sesuatu yang mirip dengan mutex. Kita dapat membedakan 3 skenario untuk penggunaannya:
- Dalam kerangka satu proses, mekanisme memori yang sederhana cocok tanpa kelebihan;
- Jika kita ingin memberikan penguncian dalam beberapa proses, maka kita dapat menggunakan sistem file (tentu saja, dalam mode non-pemblokiran);
- Jika dalam konteks beberapa server, maka Anda sudah perlu memikirkan sesuatu seperti Zookeeper.
Mutex diperlukan untuk menyelesaikan masalah kondisi ras . Bagaimanapun, kita tidak tahu (dan kita tidak bisa tahu) dalam urutan apa tugas kita akan dilakukan, tetapi bagaimanapun kita harus memastikan integritas data.
Logging / Konteks
Untuk penebangan, Monolog sudah menjadi standar, tetapi dengan beberapa peringatan: kita tidak dapat menggunakan penangan bawaan, karena akan mengarah pada kunci.
Untuk menulis ke stdOut, Anda dapat mengambil amphp / log , atau menulis pesan sederhana yang dikirim ke Graylog.
Karena pada satu saat waktu, kami dapat memproses banyak tugas, dan saat merekam log, Anda perlu memahami dalam konteks apa data ditulis. Selama percobaan, diputuskan untuk membuat trace_id
( trace_id
terdistribusi ). Intinya adalah bahwa seluruh rantai panggilan harus disertai oleh pengenal pass-through yang dapat dilacak. Selain itu, pada saat menerima pesan, package_id
dihasilkan, yang menunjukkan dengan tepat pesan yang diterima.
Dengan demikian, menggunakan kedua pengidentifikasi, kita dapat dengan mudah melacak apa yang dirujuk catatan tertentu. Masalahnya adalah bahwa dalam PHP tradisional semua catatan yang kita dapatkan di log terutama dalam urutan di mana mereka ditulis. Dalam kasus eksekusi asinkron, tidak ada pola dalam urutan entri.
Mengakhiri
Lain dari nuansa pengembangan asinkron adalah mengendalikan shutdown daemon kita. Jika Anda hanya mematikan proses, maka semua tugas yang sedang berjalan tidak akan selesai dan data akan hilang. Dalam pendekatan yang biasa, ada masalah seperti itu, tetapi tidak begitu hebat, karena hanya satu tugas yang dilakukan pada satu waktu.
Untuk menyelesaikan eksekusi dengan benar, kita perlu:
- Berhenti berlangganan dari antrian. Dengan kata lain, buatlah mustahil untuk menerima pesan baru;
- Selesaikan semua tugas yang tersisa (tunggu untuk menyelesaikan janji);
- Dan hanya setelah itu selesai skrip.
Kebocoran, debugging
Bertentangan dengan kepercayaan umum, dalam PHP modern tidaklah begitu mudah untuk menghadapi situasi di mana kebocoran memori terjadi. Perlu untuk melakukan sesuatu yang benar-benar salah.
Namun, pernah berhadapan dengan ini, tetapi karena kecerobohan dangkal. Selama implementasi detak jantung, timer baru ditambahkan setiap 40 detik untuk menanyakan koneksi. Tidak sulit menebak bahwa setelah beberapa waktu penggunaan memori mulai merayap dan cukup cepat.
Selain itu, antara lain, ia menulis sebuah pengamat sederhana yang secara opsional akan mulai setiap 10 menit dan memanggil gc_collect_cycles () dan gc_mem_caches () .
Tetapi pemaksaan awal pemulung bukanlah sesuatu yang perlu dan mendasar.
Agar dapat terus melihat penggunaan memori, MemoryUsageProcessor standar telah ditambahkan ke pencatatan .
Jika Anda mendapatkan gagasan bahwa Event Loop memblokir sesuatu, ini juga dapat dengan mudah diperiksa: cukup sambungkan LoopBlockWatcher .
Tetapi Anda perlu memastikan bahwa pengamat ini tidak memulai di lingkungan produksi. Fitur ini digunakan secara eksklusif selama pengembangan.
Hasil
: php-service-bus , Message Based .
, :
composer create-project php-service-bus/skeleton pub-sub-example cd pub-sub-example docker-compose up --build -d
, , .
/bin/consumer
, .
/src
3 : Ping
; Pong
: ; PingService
: , .
PingService
, 2 :
public function handle(Ping $command, KernelContext $context): Promise { return $context->delivery(new Pong()); } public function whenPong(Pong $event, KernelContext $context): void { $context->logContextMessage('Pong message received'); }
handle
( 1 ). @CommandHandler
;
- Promise , RabbitMQ (
delivery()
). , RabbitMQ .
whenPong
— Pong
. . @EventListener
;
, — . , , , . php-service-bus , , .
2 : , ( ) . , , (, ).
Ping
, Pong
. .
, RabbitMQ:
tools/ping
, php-service-bus , Message based .
Ping\Pong, — , , Hello, world
.
, .
- , , , Saga pattern (Process manager) .
, symfony/messenger .
, , .