Kinerja I/O sisi server: Node vs. PHP vs. Java vs. Go
Diterbitkan: 2022-03-11Memahami model Input/Output (I/O) aplikasi Anda dapat berarti perbedaan antara aplikasi yang menangani beban yang dikenakannya, dan aplikasi yang kusut saat menghadapi kasus penggunaan di dunia nyata. Mungkin meskipun aplikasi Anda kecil dan tidak melayani beban tinggi, itu mungkin jauh lebih penting. Tetapi karena beban lalu lintas aplikasi Anda meningkat, bekerja dengan model I/O yang salah dapat membawa Anda ke dunia yang menyakitkan.
Dan seperti kebanyakan situasi di mana beberapa pendekatan mungkin dilakukan, ini bukan hanya masalah mana yang lebih baik, ini masalah memahami pengorbanannya. Mari berjalan-jalan melintasi lanskap I/O dan melihat apa yang bisa kita mata-matai.
Pada artikel ini, kita akan membandingkan Node, Java, Go, dan PHP dengan Apache, membahas bagaimana bahasa yang berbeda memodelkan I/O mereka, kelebihan dan kekurangan masing-masing model, dan menyimpulkan dengan beberapa benchmark dasar. Jika Anda khawatir tentang kinerja I/O aplikasi web Anda berikutnya, artikel ini untuk Anda.
Dasar-dasar I/O: Penyegaran Cepat
Untuk memahami faktor-faktor yang terlibat dengan I/O, pertama-tama kita harus meninjau konsep-konsep di tingkat sistem operasi. Meskipun tidak mungkin bahwa Anda harus berurusan dengan banyak konsep ini secara langsung, Anda selalu menanganinya secara tidak langsung melalui lingkungan runtime aplikasi Anda. Dan detailnya penting.
Panggilan Sistem
Pertama, kami memiliki panggilan sistem, yang dapat dijelaskan sebagai berikut:
- Program Anda (di “user land,” seperti yang mereka katakan) harus meminta kernel sistem operasi untuk melakukan operasi I/O atas namanya.
- Sebuah "syscall" adalah cara program Anda meminta kernel melakukan sesuatu. Spesifik bagaimana ini diimplementasikan bervariasi antara OS tetapi konsep dasarnya sama. Akan ada beberapa instruksi khusus yang mentransfer kontrol dari program Anda ke kernel (seperti panggilan fungsi tetapi dengan beberapa saus khusus khusus untuk menangani situasi ini). Secara umum, syscalls memblokir, artinya program Anda menunggu kernel kembali ke kode Anda.
- Kernel melakukan operasi I/O yang mendasarinya pada perangkat fisik yang bersangkutan (disk, kartu jaringan, dll.) dan membalas ke syscall. Di dunia nyata, kernel mungkin harus melakukan beberapa hal untuk memenuhi permintaan Anda termasuk menunggu perangkat siap, memperbarui status internalnya, dll., tetapi sebagai pengembang aplikasi, Anda tidak peduli tentang itu. Itulah tugas kernel.
Memblokir vs. Panggilan Non-pemblokiran
Sekarang, saya baru saja mengatakan di atas bahwa syscalls memblokir, dan itu benar secara umum. Namun, beberapa panggilan dikategorikan sebagai "non-blocking", yang berarti kernel menerima permintaan Anda, memasukkannya ke dalam antrian atau buffer di suatu tempat, dan kemudian segera kembali tanpa menunggu I/O yang sebenarnya terjadi. Jadi itu "memblokir" hanya untuk jangka waktu yang sangat singkat, cukup lama untuk membuat permintaan Anda antre.
Beberapa contoh (dari syscalls Linux) mungkin membantu memperjelas: - read()
adalah panggilan pemblokiran - Anda memberikan pegangan yang mengatakan file mana dan buffer tempat pengiriman data yang dibacanya, dan panggilan kembali ketika data ada di sana. Perhatikan bahwa ini memiliki keuntungan menjadi bagus dan sederhana. - epoll_create()
, epoll_ctl()
dan epoll_wait()
adalah panggilan yang, masing-masing, memungkinkan Anda membuat grup pegangan untuk mendengarkan, menambah/menghapus penangan dari grup itu dan kemudian memblokir hingga ada aktivitas apa pun. Ini memungkinkan Anda mengontrol secara efisien sejumlah besar operasi I/O dengan satu utas, tetapi saya terlalu cepat. Ini bagus jika Anda membutuhkan fungsionalitasnya, tetapi seperti yang Anda lihat, tentu saja lebih rumit untuk digunakan.
Sangat penting untuk memahami urutan besarnya perbedaan waktu di sini. Jika inti CPU berjalan pada 3GHz, tanpa masuk ke pengoptimalan yang dapat dilakukan CPU, ia melakukan 3 miliar siklus per detik (atau 3 siklus per nanodetik). Panggilan sistem non-pemblokiran mungkin memerlukan urutan 10 detik untuk diselesaikan - atau "relatif beberapa nanodetik". Panggilan yang memblokir informasi yang diterima melalui jaringan mungkin membutuhkan waktu lebih lama - katakanlah misalnya 200 milidetik (1/5 detik). Dan katakanlah, misalnya, panggilan tanpa pemblokiran membutuhkan waktu 20 nanodetik, dan panggilan pemblokiran membutuhkan waktu 200.000.000 nanodetik. Proses Anda hanya menunggu 10 juta kali lebih lama untuk panggilan pemblokiran.
Kernel menyediakan sarana untuk melakukan pemblokiran I/O ("baca dari koneksi jaringan ini dan beri saya datanya") dan I/O non-pemblokiran ("beri tahu saya jika ada koneksi jaringan ini yang memiliki data baru"). Dan mekanisme mana yang digunakan akan memblokir proses panggilan untuk jangka waktu yang sangat berbeda.
Penjadwalan
Hal ketiga yang penting untuk diikuti adalah apa yang terjadi ketika Anda memiliki banyak utas atau proses yang mulai memblokir.
Untuk tujuan kami, tidak ada perbedaan besar antara utas dan proses. Dalam kehidupan nyata, perbedaan terkait kinerja yang paling mencolok adalah bahwa karena utas berbagi memori yang sama, dan masing-masing proses memiliki ruang memorinya sendiri, membuat proses yang terpisah cenderung memakan lebih banyak memori. Tetapi ketika kita berbicara tentang penjadwalan, intinya adalah daftar hal-hal (utas dan proses yang sama) yang masing-masing perlu mendapatkan sepotong waktu eksekusi pada inti CPU yang tersedia. Jika Anda memiliki 300 utas yang berjalan dan 8 inti untuk menjalankannya, Anda harus membagi waktu sehingga masing-masing mendapat bagiannya, dengan setiap inti berjalan untuk waktu yang singkat dan kemudian pindah ke utas berikutnya. Ini dilakukan melalui "saklar konteks", membuat CPU beralih dari menjalankan satu utas/proses ke yang berikutnya.
Sakelar konteks ini memiliki biaya yang terkait dengannya - mereka membutuhkan waktu. Dalam beberapa kasus yang cepat, mungkin kurang dari 100 nanodetik, tetapi tidak jarang dibutuhkan 1000 nanodetik atau lebih lama tergantung pada detail implementasi, kecepatan/arsitektur prosesor, cache CPU, dll.
Dan semakin banyak utas (atau proses), semakin banyak pengalihan konteks. Ketika kita berbicara tentang ribuan utas, dan ratusan nanodetik untuk masing-masing utas, segalanya bisa menjadi sangat lambat.
Namun, panggilan non-pemblokiran pada dasarnya memberi tahu kernel "telepon saya hanya ketika Anda memiliki beberapa data atau peristiwa baru di salah satu dari koneksi ini." Panggilan non-pemblokiran ini dirancang untuk menangani beban I/O besar secara efisien dan mengurangi pengalihan konteks.
Dengan saya sejauh ini? Karena sekarang tibalah bagian yang menyenangkan: Mari kita lihat apa yang dilakukan beberapa bahasa populer dengan alat-alat ini dan menarik beberapa kesimpulan tentang pengorbanan antara kemudahan penggunaan dan kinerja… dan informasi menarik lainnya.
Sebagai catatan, sementara contoh yang ditampilkan dalam artikel ini sepele (dan sebagian, dengan hanya bit yang relevan yang ditampilkan); akses database, sistem caching eksternal (memcache, et. semua) dan apa pun yang memerlukan I/O akan berakhir dengan melakukan semacam panggilan I/O di bawah tenda yang akan memiliki efek yang sama seperti contoh sederhana yang ditunjukkan. Juga, untuk skenario di mana I/O digambarkan sebagai "pemblokiran" (PHP, Java), permintaan dan respons HTTP membaca dan menulis dengan sendirinya memblokir panggilan: Sekali lagi, lebih banyak I/O tersembunyi di sistem dengan masalah kinerja yang menyertainya. untuk memperhitungkan.
Ada banyak faktor yang digunakan untuk memilih bahasa pemrograman untuk sebuah proyek. Bahkan ada banyak faktor ketika Anda hanya mempertimbangkan kinerja. Tetapi, jika Anda khawatir bahwa program Anda akan dibatasi terutama oleh I/O, jika kinerja I/O berhasil atau tidak untuk proyek Anda, ini adalah hal-hal yang perlu Anda ketahui.
Pendekatan “Keep It Simple”: PHP
Kembali di tahun 90-an, banyak orang memakai sepatu Converse dan menulis skrip CGI di Perl. Kemudian PHP datang dan, sebanyak beberapa orang suka mengotak-atiknya, itu membuat halaman web dinamis menjadi lebih mudah.
Model yang digunakan PHP cukup sederhana. Ada beberapa variasi untuk itu tetapi server PHP rata-rata Anda terlihat seperti:
Permintaan HTTP masuk dari browser pengguna dan mengenai server web Apache Anda. Apache membuat proses terpisah untuk setiap permintaan, dengan beberapa optimasi untuk menggunakannya kembali untuk meminimalkan berapa banyak yang harus dilakukan (proses pembuatan, secara relatif, lambat). Apache memanggil PHP dan memerintahkannya untuk menjalankan file .php
yang sesuai pada disk. Kode PHP mengeksekusi dan memblokir panggilan I/O. Anda memanggil file_get_contents()
di PHP dan di bawah tenda itu membuat read()
syscalls dan menunggu hasilnya.
Dan tentu saja kode sebenarnya hanya disematkan langsung ke halaman Anda, dan operasi memblokir:
<?php // blocking file I/O $file_data = file_get_contents('/path/to/file.dat'); // blocking network I/O $curl = curl_init('http://example.com/example-microservice'); $result = curl_exec($curl); // some more blocking network I/O $result = $db->query('SELECT id, data FROM examples ORDER BY id DESC limit 100'); ?>
Dalam hal bagaimana ini terintegrasi dengan sistem, seperti ini:
Cukup sederhana: satu proses per permintaan. Panggilan I/O hanya memblokir. Keuntungan? Ini sederhana dan berhasil. Kerugian? Pukul dengan 20.000 klien secara bersamaan dan server Anda akan terbakar. Pendekatan ini tidak berskala dengan baik karena alat yang disediakan oleh kernel untuk menangani I/O volume tinggi (epoll, dll.) tidak digunakan. Dan untuk menambah kerugian, menjalankan proses terpisah untuk setiap permintaan cenderung menggunakan banyak sumber daya sistem, terutama memori, yang sering kali merupakan hal pertama yang Anda habiskan dalam skenario seperti ini.
Catatan: Pendekatan yang digunakan untuk Ruby sangat mirip dengan PHP, dan secara umum, cara bergelombang tangan mereka dapat dianggap sama untuk tujuan kita.
Pendekatan Multithreaded: Java
Jadi Java datang, tepat saat Anda membeli nama domain pertama Anda dan itu keren untuk hanya mengatakan "dot com" secara acak setelah sebuah kalimat. Dan Java memiliki multithreading yang dibangun ke dalam bahasa, yang (terutama ketika dibuat) cukup mengagumkan.
Sebagian besar server web Java bekerja dengan memulai utas eksekusi baru untuk setiap permintaan yang masuk dan kemudian di utas ini akhirnya memanggil fungsi yang Anda, sebagai pengembang aplikasi, tulis.
Melakukan I/O di Java Servlet cenderung terlihat seperti:
public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // blocking file I/O InputStream fileIs = new FileInputStream("/path/to/file"); // blocking network I/O URLConnection urlConnection = (new URL("http://example.com/example-microservice")).openConnection(); InputStream netIs = urlConnection.getInputStream(); // some more blocking network I/O out.println("..."); }
Karena metode doGet
kami di atas sesuai dengan satu permintaan dan dijalankan di utasnya sendiri, alih-alih proses terpisah untuk setiap permintaan yang membutuhkan memorinya sendiri, kami memiliki utas terpisah. Ini memiliki beberapa keuntungan yang bagus, seperti dapat berbagi status, data yang di-cache, dll. antar utas karena mereka dapat mengakses memori satu sama lain, tetapi dampaknya pada cara berinteraksi dengan jadwal masih hampir identik dengan apa yang sedang dilakukan di PHP contoh sebelumnya. Setiap permintaan mendapat utas baru dan berbagai operasi I/O memblokir di dalam utas itu hingga permintaan sepenuhnya ditangani. Utas dikumpulkan untuk meminimalkan biaya pembuatan dan penghancurannya, tetapi tetap saja, ribuan koneksi berarti ribuan utas yang buruk bagi penjadwal.
Tonggak penting adalah bahwa di versi 1.4 Java (dan peningkatan yang signifikan lagi di 1.7) memperoleh kemampuan untuk melakukan panggilan I/O non-pemblokiran. Sebagian besar aplikasi, web dan lainnya, tidak menggunakannya, tetapi setidaknya tersedia. Beberapa server web Java mencoba memanfaatkan ini dengan berbagai cara; namun, sebagian besar aplikasi Java yang digunakan masih berfungsi seperti yang dijelaskan di atas.
Java membuat kita lebih dekat dan tentu saja memiliki beberapa fungsionalitas out-of-the-box yang baik untuk I/O, tetapi itu masih tidak benar-benar menyelesaikan masalah apa yang terjadi ketika Anda memiliki aplikasi terikat I/O berat yang ditumbuk ke tanah dengan ribuan benang pemblokiran.
I/O tanpa pemblokiran sebagai Warga Negara Kelas Satu: Node
Anak populer di blok ketika datang ke I/O yang lebih baik adalah Node.js. Siapapun yang pernah mengenal Node secara singkat telah diberitahu bahwa Node “non-blocking” dan menangani I/O secara efisien. Dan ini benar dalam pengertian umum. Tetapi iblis ada dalam perincian dan sarana yang digunakan sihir ini untuk mencapai hal-hal yang berkaitan dengan kinerja.
Pada dasarnya perubahan paradigma yang diterapkan Node adalah bahwa alih-alih pada dasarnya mengatakan "tulis kode Anda di sini untuk menangani permintaan", mereka malah mengatakan "tulis kode di sini untuk mulai menangani permintaan." Setiap kali Anda perlu melakukan sesuatu yang melibatkan I/O, Anda membuat permintaan dan memberikan fungsi panggilan balik yang akan dipanggil oleh Node setelah selesai.

Kode Node tipikal untuk melakukan operasi I/O dalam permintaan berjalan seperti ini:
http.createServer(function(request, response) { fs.readFile('/path/to/file', 'utf8', function(err, data) { response.end(data); }); });
Seperti yang Anda lihat, ada dua fungsi panggilan balik di sini. Yang pertama dipanggil saat permintaan dimulai, dan yang kedua dipanggil saat data file tersedia.
Apa yang dilakukan pada dasarnya adalah memberi Node kesempatan untuk menangani I/O secara efisien di antara panggilan balik ini. Skenario di mana akan lebih relevan adalah di mana Anda melakukan panggilan database di Node, tapi saya tidak akan repot dengan contoh karena prinsip yang sama persis: Anda memulai panggilan database, dan memberikan Node fungsi panggilan balik, itu melakukan operasi I/O secara terpisah menggunakan panggilan non-pemblokiran dan kemudian memanggil fungsi panggilan balik Anda saat data yang Anda minta tersedia. Mekanisme antrian panggilan I/O dan membiarkan Node menanganinya dan kemudian mendapatkan panggilan balik disebut “Event Loop.” Dan itu bekerja dengan cukup baik.
Namun ada tangkapan untuk model ini. Di bawah tenda, alasannya lebih berkaitan dengan bagaimana mesin JavaScript V8 (mesin JS Chrome yang digunakan oleh Node) diimplementasikan 1 daripada yang lainnya. Kode JS yang Anda tulis semuanya berjalan dalam satu utas. Pikirkan tentang itu sejenak. Ini berarti bahwa sementara I/O dilakukan menggunakan teknik non-pemblokiran yang efisien, JS Anda dapat yang melakukan operasi terikat-CPU berjalan dalam satu utas, setiap potongan kode memblokir yang berikutnya. Contoh umum di mana ini mungkin muncul adalah mengulang catatan database untuk memprosesnya dalam beberapa cara sebelum mengeluarkannya ke klien. Berikut ini contoh yang menunjukkan cara kerjanya:
var handler = function(request, response) { connection.query('SELECT ...', function (err, rows) { if (err) { throw err }; for (var i = 0; i < rows.length; i++) { // do processing on each row } response.end(...); // write out the results }) };
Meskipun Node menangani I/O secara efisien, loop for
dalam contoh di atas menggunakan siklus CPU di dalam satu-satunya utas utama Anda. Ini berarti bahwa jika Anda memiliki 10.000 koneksi, loop tersebut dapat membuat seluruh aplikasi Anda merayapi, bergantung pada berapa lama waktu yang dibutuhkan. Setiap permintaan harus berbagi sepotong waktu, satu per satu, di utas utama Anda.
Premis seluruh konsep ini didasarkan pada bahwa operasi I/O adalah bagian yang paling lambat, sehingga paling penting untuk menanganinya secara efisien, bahkan jika itu berarti melakukan pemrosesan lainnya secara serial. Ini benar dalam beberapa kasus, tetapi tidak dalam semua hal.
Poin lainnya adalah, dan meskipun ini hanya opini, menulis banyak panggilan balik bersarang bisa sangat melelahkan dan beberapa berpendapat bahwa itu membuat kode secara signifikan lebih sulit untuk diikuti. Tidak jarang melihat callback bersarang empat, lima, atau bahkan lebih banyak level jauh di dalam kode Node.
Kami kembali lagi ke trade-off. Model Node bekerja dengan baik jika masalah kinerja utama Anda adalah I/O. Namun, kelemahannya adalah Anda dapat masuk ke fungsi yang menangani permintaan HTTP dan memasukkan kode intensif CPU dan membuat setiap koneksi merangkak jika Anda tidak hati-hati.
Secara alami Non-blocking: Go
Sebelum saya masuk ke bagian Go, sudah sepantasnya saya mengungkapkan bahwa saya adalah seorang fanboy Go. Saya telah menggunakannya untuk banyak proyek dan saya secara terbuka mendukung keunggulan produktivitasnya, dan saya melihatnya dalam pekerjaan saya ketika saya menggunakannya.
Yang mengatakan, mari kita lihat bagaimana berurusan dengan I/O. Salah satu fitur utama dari bahasa Go adalah ia memiliki penjadwalnya sendiri. Alih-alih setiap utas eksekusi yang sesuai dengan utas OS tunggal, ia bekerja dengan konsep "goroutines." Dan runtime Go dapat menetapkan goroutine ke utas OS dan menjalankannya, atau menangguhkannya dan tidak mengaitkannya dengan utas OS, berdasarkan apa yang dilakukan goroutine itu. Setiap permintaan yang masuk dari server HTTP Go ditangani di Goroutine terpisah.
Diagram cara kerja penjadwal terlihat seperti ini:
Di bawah tenda, ini diimplementasikan oleh berbagai titik di runtime Go yang mengimplementasikan panggilan I/O dengan membuat permintaan untuk menulis/membaca/menghubungkan/dll, membuat goroutine saat ini tertidur, dengan informasi untuk membangunkan goroutine kembali ketika tindakan lebih lanjut dapat diambil.
Akibatnya, runtime Go melakukan sesuatu yang tidak jauh berbeda dengan apa yang dilakukan Node, kecuali bahwa mekanisme panggilan balik dibangun ke dalam implementasi panggilan I/O dan berinteraksi dengan penjadwal secara otomatis. Itu juga tidak mengalami batasan karena harus menjalankan semua kode handler Anda di utas yang sama, Go akan secara otomatis memetakan Goroutine Anda ke banyak utas OS yang dianggap sesuai berdasarkan logika dalam penjadwalnya. Hasilnya adalah kode seperti ini:
func ServeHTTP(w http.ResponseWriter, r *http.Request) { // the underlying network call here is non-blocking rows, err := db.Query("SELECT ...") for _, row := range rows { // do something with the rows, // each request in its own goroutine } w.Write(...) // write the response, also non-blocking }
Seperti yang Anda lihat di atas, struktur kode dasar dari apa yang kami lakukan menyerupai pendekatan yang lebih sederhana, namun mencapai I/O non-pemblokiran di bawah tenda.
Dalam kebanyakan kasus, ini akhirnya menjadi "yang terbaik dari kedua dunia." I/O non-pemblokiran digunakan untuk semua hal penting, tetapi kode Anda terlihat seperti memblokir dan dengan demikian cenderung lebih mudah dipahami dan dipelihara. Interaksi antara penjadwal Go dan penjadwal OS menangani sisanya. Ini bukan sihir yang lengkap, dan jika Anda membangun sistem yang besar, ada baiknya meluangkan waktu untuk memahami lebih detail tentang cara kerjanya; tetapi pada saat yang sama, lingkungan yang Anda dapatkan "out-of-the-box" berfungsi dan berskala cukup baik.
Go mungkin memiliki kesalahan, tetapi secara umum, cara menangani I/O tidak termasuk di dalamnya.
Kebohongan, Kebohongan Terkutuk, dan Tolok Ukur
Sulit untuk memberikan waktu yang tepat pada peralihan konteks yang terlibat dengan berbagai model ini. Saya juga bisa berargumen bahwa itu kurang berguna bagi Anda. Jadi sebagai gantinya, saya akan memberi Anda beberapa tolok ukur dasar yang membandingkan kinerja server HTTP keseluruhan dari lingkungan server ini. Ingatlah bahwa banyak faktor yang terlibat dalam kinerja seluruh jalur permintaan/respons HTTP ujung-ke-ujung, dan angka-angka yang disajikan di sini hanyalah beberapa contoh yang saya kumpulkan untuk memberikan perbandingan dasar.
Untuk setiap lingkungan ini, saya menulis kode yang sesuai untuk dibaca dalam file 64k dengan byte acak, menjalankan hash SHA-256 di dalamnya N beberapa kali (N ditentukan dalam string kueri URL, misalnya .../test.php?n=100
) dan cetak hash yang dihasilkan dalam hex. Saya memilih ini karena ini adalah cara yang sangat sederhana untuk menjalankan benchmark yang sama dengan beberapa I/O yang konsisten dan cara yang terkontrol untuk meningkatkan penggunaan CPU.
Lihat catatan benchmark ini untuk detail lebih lanjut tentang lingkungan yang digunakan.
Pertama, mari kita lihat beberapa contoh konkurensi rendah. Menjalankan 2000 iterasi dengan 300 permintaan bersamaan dan hanya satu hash per permintaan (N=1) memberi kita ini:
Sulit untuk menarik kesimpulan hanya dari satu grafik ini, tetapi bagi saya tampaknya, pada volume koneksi dan komputasi ini, kita melihat waktu yang lebih berkaitan dengan eksekusi umum bahasa itu sendiri, lebih dari itu I/O. Perhatikan bahwa bahasa yang dianggap "bahasa scripting" (pengetikan longgar, interpretasi dinamis) berkinerja paling lambat.
Tetapi apa yang terjadi jika kita meningkatkan N menjadi 1000, masih dengan 300 permintaan bersamaan - beban yang sama tetapi iterasi hash 100x lebih banyak (lebih banyak beban CPU secara signifikan):
Tiba-tiba, kinerja Node turun secara signifikan, karena operasi intensif CPU di setiap permintaan saling memblokir. Dan yang cukup menarik, kinerja PHP menjadi jauh lebih baik (relatif terhadap yang lain) dan mengalahkan Java dalam pengujian ini. (Perlu dicatat bahwa dalam PHP implementasi SHA-256 ditulis dalam C dan jalur eksekusi menghabiskan lebih banyak waktu dalam loop itu, karena kita sedang melakukan 1000 iterasi hash sekarang).
Sekarang mari kita coba 5000 koneksi bersamaan (dengan N=1) - atau sedekat mungkin. Sayangnya, untuk sebagian besar lingkungan ini, tingkat kegagalan tidak signifikan. Untuk bagan ini, kita akan melihat jumlah total permintaan per detik. Semakin tinggi semakin baik :
Dan gambarnya terlihat sangat berbeda. Ini adalah tebakan, tetapi sepertinya pada volume koneksi yang tinggi, overhead per koneksi yang terlibat dengan pemijahan proses baru dan memori tambahan yang terkait dengannya di PHP+Apache tampaknya menjadi faktor dominan dan menghambat kinerja PHP. Jelas, Go adalah pemenangnya di sini, diikuti oleh Java, Node dan terakhir PHP.
Sementara faktor-faktor yang terlibat dengan throughput Anda secara keseluruhan banyak dan juga sangat bervariasi dari aplikasi ke aplikasi, semakin Anda memahami tentang keberanian apa yang terjadi di bawah tenda dan pengorbanan yang terlibat, semakin baik Anda.
Singkatnya
Dengan semua hal di atas, cukup jelas bahwa seiring dengan perkembangan bahasa, solusi untuk menangani aplikasi skala besar yang melakukan banyak I/O telah berevolusi dengannya.
Agar adil, baik PHP dan Java, terlepas dari deskripsi dalam artikel ini, memiliki implementasi I/O non-pemblokiran yang tersedia untuk digunakan dalam aplikasi web. Tapi ini tidak biasa seperti pendekatan yang dijelaskan di atas, dan biaya operasional petugas pemeliharaan server menggunakan pendekatan tersebut perlu diperhitungkan. Belum lagi bahwa kode Anda harus terstruktur dengan cara yang sesuai dengan lingkungan seperti itu; Aplikasi web PHP atau Java "normal" Anda biasanya tidak akan berjalan tanpa modifikasi yang signifikan dalam lingkungan seperti itu.
Sebagai perbandingan, jika kami mempertimbangkan beberapa faktor signifikan yang memengaruhi kinerja serta kemudahan penggunaan, kami mendapatkan ini:
Bahasa | Utas vs. Proses | I/O tanpa pemblokiran | Kemudahan penggunaan |
---|---|---|---|
PHP | Proses | Tidak | |
Jawa | Utas | Tersedia | Membutuhkan Panggilan Balik |
Node.js | Utas | Ya | Membutuhkan Panggilan Balik |
Pergi | Utas (Goroutine) | Ya | Tidak Perlu Panggilan Balik |
Utas umumnya akan jauh lebih hemat memori daripada proses, karena mereka berbagi ruang memori yang sama sedangkan proses tidak. Menggabungkannya dengan faktor-faktor yang terkait dengan I/O non-pemblokiran, kita dapat melihat bahwa setidaknya dengan faktor-faktor yang dipertimbangkan di atas, saat kita bergerak ke bawah daftar, pengaturan umum yang terkait dengan I/O meningkat. Jadi jika saya harus memilih pemenang dalam kontes di atas, itu pasti Go.
Meski begitu, dalam praktiknya, memilih lingkungan untuk membangun aplikasi Anda terkait erat dengan keakraban tim Anda dengan lingkungan tersebut, dan produktivitas keseluruhan yang dapat Anda capai dengannya. Jadi mungkin tidak masuk akal bagi setiap tim untuk terjun dan mulai mengembangkan aplikasi dan layanan web di Node atau Go. Memang, menemukan pengembang atau keakraban tim internal Anda sering disebut sebagai alasan utama untuk tidak menggunakan bahasa dan/atau lingkungan yang berbeda. Yang mengatakan, waktu telah berubah selama lima belas tahun terakhir atau lebih, banyak.
Semoga di atas membantu melukiskan gambaran yang lebih jelas tentang apa yang terjadi di bawah tenda dan memberi Anda beberapa ide tentang bagaimana menangani skalabilitas dunia nyata untuk aplikasi Anda. Selamat memasukkan dan mengeluarkan!