Satu proses Node.js hanya memakai satu core CPU untuk menjalankan JavaScript — event loop yang sudah kita pahami bekerja pada satu thread. Server produksi modern memiliki banyak core, dan membiarkan sebagian besarnya menganggur adalah pemborosan ketika permintaan terus berdatangan. Module node:cluster menjawab keadaan itu: satu proses primary menurunkan beberapa proses worker, semuanya berbagi port yang sama sehingga dari luar tampak seperti satu server gemuk. Chapter ini membangun cluster pertama kita, membedah cara kerja pembagian koneksi, dan membahas batas-batasnya.
Satu Port, Banyak Proses #
Dua peran bekerja dalam pola cluster. Primary adalah proses induk yang kita jalankan; worker adalah proses anak hasil cluster.fork()
— bukan system call fork, melainkan proses Node.js baru yang menjalankan berkas yang sama dengan environment tambahan. Perannya dibedakan lewat cluster.isPrimary: primary masuk cabang penurunan worker, worker masuk cabang server.
Bagian yang sering dianggap sulit ternyata sederhana: ketika worker memanggil server.listen(port), pekerjaan binding port sesungguhnya dikerjakan oleh primary. Koneksi TCP yang masuk lalu dibagikan ke worker — pada Linux dan macOS dengan kebijakan round-robin bawaan, koneksi berikutnya bergiliran ke worker berikutnya. Seluruh mekanisme ini terdokumentasi rapi pada bagian how it works
dokumentasi resmi.
Membangun Cluster Pertama #
Program berikut menempatkan kedua peran dalam satu berkas. Primary meminjam port ephemeral sebentar untuk mengetahui angkanya, menurunkan dua worker lewat environment PORT, menunggu kabar “siap” dari keduanya, lalu mengirim enam permintaan HTTP dan mencatat worker mana yang melayani:
// Satu berkas, dua peran: primary menurunkan worker,
// worker menjalankan HTTP server di port yang sama.
import cluster from "node:cluster";
import http from "node:http";
if (cluster.isPrimary) {
// Pinjam satu port ephemeral lalu lepaskan, supaya semua worker
// berbagi port yang sama tanpa menyetel angka port secara manual.
const penjajak = http.createServer();
penjajak.listen(0, "127.0.0.1", () => {
const port = penjajak.address().port;
penjajak.close(() => mulai(port));
});
} else {
const idSaya = cluster.worker.id;
http.createServer((req, res) => {
res.writeHead(200, { "Content-Type": "text/plain" });
res.end(`dilayani worker-${idSaya}`);
}).listen(process.env.PORT, "127.0.0.1", () => {
process.send("siap");
});
}
async function mulai(port) {
const semuaSiap = new Promise((resolve) => {
let jumlah = 0;
cluster.on("message", (worker, pesan) => {
if (pesan === "siap" && ++jumlah === 2) resolve();
});
});
for (let i = 0; i < 2; i++) {
cluster.fork({ PORT: String(port) });
}
console.log("primary menurunkan 2 worker");
await semuaSiap;
console.log("kedua worker siap berbagi port yang sama");
const pelayan = new Set();
let totalSukses = 0;
for (let gelombang = 0; gelombang < 2; gelombang++) {
const kirim = [];
for (let i = 0; i < 3; i++) {
kirim.push(
fetch(`http://127.0.0.1:${port}/halo`).then(async (r) => {
if (r.status === 200) totalSukses += 1;
pelayan.add(await r.text());
})
);
}
await Promise.all(kirim);
}
console.log("total permintaan :", 6);
console.log("berhasil dengan 200 :", totalSukses);
console.log("pelayan yang terlihat :", [...pelayan].sort().join(", "));
const semuaKeluar = Promise.all(
Object.values(cluster.workers).map(
(w) => new Promise((selesai) => w.on("exit", selesai))
)
);
for (const w of Object.values(cluster.workers)) {
w.kill();
}
await semuaKeluar;
console.log("semua worker telah berhenti, primary keluar");
}Output yang Dihasilkan:
primary menurunkan 2 worker
kedua worker siap berbagi port yang sama
total permintaan : 6
berhasil dengan 200 : 6
pelayan yang terlihat : dilayani worker-1, dilayani worker-2
semua worker telah berhenti, primary keluarMari kita bedah baris-per-barisnya:
- Baris 6-13 (cabang primary): proses induk membuat server sekunder bertugas tunggal — meminjam port ephemeral lewat
listen(0). Begitu angkanya diketahui, server pinjaman ditutup dan alur berpindah kemulai(port); pola ini menghindari menyetel angka port tetap yang bisa bertabrakan. - Baris 14-22 (cabang worker): setiap worker menjalankan HTTP server sungguhan dengan
cluster.worker.idsebagai identitas — primary memberi nomor urut mulai dari 1, dan nomor itu kita sisipkan ke body respons sebagai bukti siapa yang melayani. Setelahlistenberhasil, worker melapor"siap"ke primary lewat kanal IPC. - Baris 24-30 (
semuaSiap): primary menunggu pesan"siap"dari dua worker sebelum mengirim permintaan — tanpa sinkronisasi ini, permintaan pertama bisa tiba sebelum worker sempat mendengarkan. - Baris 32-34 (
cluster.fork): dua worker diturunkan dengan environmentPORTberisi angka port pinjaman tadi;fork()mengembalikan objectWorkerdan pendaftaran dicluster.workersberlangsung otomatis. - Baris 39-52 (pengiriman permintaan): dua gelombang berisi tiga permintaan serentak dikirim lewat
fetch. Body respons dikumpulkan ke sebuahSetlalu dicetak terurut — inilah bukti kedua worker sama-sama melayani, sebab primary membagi koneksi antar mereka. Kami mencetak jumlah dan status saja tanpa durasi: pembagian koneksi pasti terjadi, tetapi waktu tempuhnya bervariasi antar eksekusi. - Baris 58-67 (pembersihan): sebelum keluar, primary menghentikan semua worker lewat
kill()dan menunggu eventexitmasing-masing — menutup proses anak dengan rapi adalah bagian dari contoh yang layak dicontoh, bukan formalitas.
Kapan Clustering Membantu dan Kapan Tidak #
Clustering melipatgandakan kapasitas memproses pekerjaan I/O-bound: tiap worker punya event loop sendiri, sehingga saat satu worker sibuk menangani ratusan koneksi, worker lain tetap melayani. Untuk pekerjaan CPU-bound hasilnya berbeda: komputasi berat tetap memblokir event loop worker pemiliknya, hanya sekarang ada beberapa event loop yang bisa penuh bergantian. Tabel berikut merangkum pilihan skalanya:
| Kebutuhan | Alat yang tepat | Alasan |
|---|---|---|
| Melayani lebih banyak koneksi I/O | node:cluster | Tiap worker punya event loop sendiri di core sendiri |
| Mempercepat komputasi CPU satu tugas | worker_threads | Satu proses berbagi memori, biaya komunikasi kecil — Chapter Pola Worker Pool |
| Menambah kapasitas lintas mesin | Load balancer eksternal | Cluster bekerja di dalam satu mesin saja |
Mengelola Cluster di Produksi #
Mengelola cluster secara manual berarti menangani banyak hal di luar kode: memulai ulang worker yang mati, memuat ulang kode tanpa waktu henti, dan menjalankan banyak aplikasi dalam satu mesin. Process manager seperti PM2 mengerjakan semuanya lewat satu perintah — mode cluster-nya menurunkan worker sebanyak core yang tersedia dan otomatis mengganti worker yang berhenti:
pm2 start server.mjs -i max
pm2 ls
pm2 reload serverFlag -i max meminta PM2 membuat satu worker per core; reload memulai ulang worker secara bergantian sehingga selalu ada yang melayani selama pembaruan kode. Kami menampilkan perintah ini tanpa contoh output karena PM2 tidak terpasang di lingkungan Course ini — konsep penurunan worker dan pembagian port yang sudah kita bangun manual adalah mesin yang sama yang diotak-atik PM2 di belakang layar.
Cluster memperbanyak event loop dalam satu mesin, dan itu menutup satu jenis kemacetan. Masih ada satu pola lagi untuk kebutuhan yang berbeda: komputasi berat yang harus selesai cepat tanpa membanjiri mesin dengan proses-proses penuh. Alih-alih memperbanyak proses, kita menyewakan beberapa thread pekerja untuk satu antrean tugas — topik Chapter Pola Worker Pool.