Scaling Aplikasi Node Js Dengan Multiprocessing
Anda mungkin tidak sepenuhnya memanfaatkan lingkungan server multi-core tanpa sepengetahuan Anda
Node adalah lingkungan runtime JavaScript single-threaded. Menjadi single-threaded memiliki banyak keuntungan, menjadikan Node lingkungan pengembangan back-end yang andal bagi banyak orang. Namun, menjadi single-threaded memiliki kekurangannya sendiri. Dengan Node, Anda perlu memastikan bahwa Anda hanya memiliki satu utas utama, yang selalu tersedia untuk menangani permintaan yang masuk. Ini tidak semudah yang Anda bayangkan, terutama jika aplikasi Anda perlu menangani lebih banyak permintaan, atau jika aplikasi Anda perlu melakukan tugas-tugas intensif CPU di beberapa titik. Mari kita lihat bagaimana mengatasi ini dengan multiprocessing.
Apa itu multiproses?
Multiprocessing adalah teknik penskalaan sisi server untuk node yang memungkinkan Anda menjalankan beberapa instance node. Idealnya satu untuk setiap inti prosesor. Dengan beberapa instance node, ada beberapa utas utama, jadi jika salah satu utas terisi atau macet, ada utas lain yang menangani permintaan masuk. Dengan begitu, aplikasi tidak akan terlihat seperti ini:
Nobody likes unresponsive apps.Anda dapat mengimplementasikan multiprocessing menggunakan modul cluster bawaan Node atau modul child_process-nya.
Memproses tugas intensif CPU
Jika Anda telah menggunakan Node untuk sementara waktu, dan aplikasi Anda memiliki tugas-tugas intensif CPU, Anda telah mendengar bahwa Node bukanlah pilihan yang tepat. Ini karena saat Anda melakukan tugas intensif CPU yang membutuhkan waktu cukup lama untuk diselesaikan, satu utas utama pada node diblokir dan tidak menerima permintaan baru hingga tugas selesai.
Modul Node child_process adalah solusi yang baik untuk masalah ini. Ideal untuk membuat beberapa instance node sesuai dengan alur eksekusi yang berbeda dan menyelesaikan satu tugas umum. Ini idealnya memungkinkan Anda untuk melakukan fork instance node baru (proses anak) pada inti CPU lain dan memindahkan tugas berat ke proses turunan yang baru. Ini memungkinkan utas utama proses induk untuk merespons permintaan baru, dan ketika tugas selesai, hasil tugas yang diturunkan dapat dikembalikan ke proses induk.
Mari kita lihat bagaimana menunjukkan ini dalam beberapa kode. Pertama, saya membuat algoritme untuk memeriksa apakah angka tertentu adalah bilangan prima, dengan cara yang paling tidak efisien (untuk meniru tugas intensif CPU).
// listen for messages from the parent process
process.on("message", message => {
let isPrime = checkIfPrime(message);
// send the results back to the parent process
process.send(isPrime);
// kill the child process
process.exit();
})
function checkIfPrime(number){
let isPrime = true;
for (let i = 2; i < number; i++){
if(number % i === 0){
isPrime = false;
}
}
return isPrime && number > 1;
}
Kemudian saya membuat kode server simpul sederhana untuk menjalankan fungsi di atas menggunakan proses anak bercabang yang baru ketika titik akhir API yang relevan dipanggil.
const childProcess = require('child_process')
const express = require('express')
const bodyParser = require('body-parser')
const app = express();
app.use(bodyParser.json());
app.post("/someBigProcess", (req,res) => {
const forked_child_process = childProcess.fork('./checkIfPrime.js');
// send message to the child process
forked_child_process.send(parseInt(req.body.number));
// listen for a response from the child process
forked_child_process.on("message", isTheNumberPrime => res.send(isTheNumberPrime));
})
app.listen(3000, () => {
console.log('server started on port 3000');
})
Kami kemudian menggunakan AutoCannon untuk membandingkan server dengan mengirimkan delapan permintaan menggunakan delapan koneksi simultan.
Benchmark dengan multiprocessing
Seperti yang Anda lihat, server hanya membutuhkan waktu 1,02 detik untuk memproses permintaan. Akhirnya, saya membuat kode server lain tanpa menggunakan multiprocessing dan menjalankan benchmark yang sama.
Benchmark tanpa multiprocessing
Kali ini, butuh lebih dari 74 detik bagi server untuk memproses jumlah permintaan yang sama. Ini memiliki penalti kinerja yang signifikan jika dibandingkan dengan implementasi sebelumnya yang menggunakan multiprosesor.
Ada tugas-tugas tertentu yang memakan waktu seperti operasi I/O, operasi jaringan, dan fungsi kriptografi yang dapat dilakukan oleh node secara asinkron dan non-blocking tanpa menerapkan multiprocessing secara manual. Lihat artikel sebelumnya untuk informasi selengkapnya tentang cara Node menangani tugas asinkron.
Menangani sejumlah besar permintaan
Threading tunggal dapat menjadi batasan saat menangani sejumlah besar permintaan. Karena jumlah permintaan meningkat, semua permintaan diproses hanya menggunakan satu utas, yang dapat menghasilkan waktu respons server yang lebih lama.
Modul cluster node adalah solusi untuk masalah ini. Ini memungkinkan Anda membuat beberapa instance node menurut alur eksekusi yang sama dan mendengarkan pada port yang sama, yang ideal untuk menangani sejumlah besar permintaan. Mari kita tulis kodenya dan lihat cara kerjanya.
const cluster = require('cluster')
const os = require('os')
const express = require('express')
// check if the process is the master process
if(cluster.isMaster){
// get the number of available cpu cores
const nCPUs = os.cpus().length;
// fork worker processes for each available CPU core
for(let i = 0; i< nCPUs; i++){
cluster.fork()
}
}else{
// if the process is a worker process listen for requests
const app = express();
app.get("/getTimeStamp", (_req,res) => {
res.send(Date.now().toString());
})
app.listen(3000, () => {
console.log(`worker process ${process.pid} is listening on port 3000`);
});
}
Dalam pengelompokan, ada satu proses utama yang disebut proses master, dan aplikasi ini hanya memotong proses pekerja yang tersisa, satu untuk setiap inti CPU yang tersedia. Proses pekerja ini mendengarkan permintaan pada port yang sama, dan penyeimbang beban bawaan modul cluster mendistribusikan permintaan di antara proses pekerja di bawah beban berat.
Untuk menjalankan kode di atas dan mengukur apakah pengelompokan telah meningkatkan kinerja, kami menjalankan benchmark di server yang mensimulasikan 100.000 permintaan dari delapan koneksi bersamaan.
Benchmark dengan clustering
Butuh 6,02 detik untuk server ke server 100.000 permintaan untuk pengelompokan. Kemudian saya menerapkan server serupa lainnya, tetapi menjalankan benchmark yang sama tanpa pengelompokan.
Benchmark tanpa clustering
Kali ini butuh lebih dari 12 detik bagi server untuk memproses jumlah permintaan yang sama. Hal ini dapat menghasilkan kinerja yang jauh lebih rendah dibandingkan dengan implementasi sebelumnya yang menggunakan pengelompokan, dan bahkan dapat secara signifikan lebih rendah dalam kasus penggunaan di dunia nyata.
Kekurangan dari multiprocessing
Meningkatkan kinerja multiprocessing itu mahal. Menerapkan multiprocessing berarti menjalankan beberapa instance node, yang dapat menghabiskan banyak memori. Oleh karena itu, Anda harus selalu memastikan bahwa lingkungan server Anda dapat menahan lonjakan penggunaan memori yang dapat terjadi dengan multiprosesor.
Multiprocessing dapat memerlukan komunikasi antarproses untuk aplikasi tertentu, terlepas dari modul node yang digunakan, dan dapat menjadi sedikit rumit untuk dipelihara dalam jangka panjang.
Kesimpulan
Multiprocessing dapat dengan mudah diimplementasikan menggunakan modul bawaan Node untuk mendapatkan hasil maksimal dari lingkungan server yang tersedia. Multiprocessing dapat digunakan untuk meningkatkan kinerja server dan ketersediaan server saat melakukan tugas-tugas intensif CPU atau memproses sejumlah besar permintaan.
Terima kasih telah membaca!
Sumber :https://betterprogramming.pub/scaling-node-js-applications-with-multiprocessing-b0c25511832a