Satu proses Node menjalankan JavaScript-mu di satu thread, jadi cuma memakai satu core CPU. Di server 4 core, tiga core lainnya menganggur. Clustering menjalankan beberapa salinan catatan-api sekaligus, satu per core, di belakang port 3000 yang sama. Bayangin warung yang buka empat loket. Ada satu penerima tamu di pintu yang mengarahkan pembeli bergiliran: loket 1, 2, 3, 4, lalu kembali ke 1. Tiap loket punya buku catatannya sendiri, dan yang ditulis loket 1 gak diketahui loket 2. Di Node, penerima tamu itu primary, loketnya worker, dan buku catatannya adalah memori tiap proses.
🧪 Tebakan cepat:
catatan-apijalan dengan 4 worker. KamuPOST /catatanuntuk "Olahraga" dan dapat 201. Lalu kamuGET /catatan. Berapa catatan yang kamu terima? Tebak dulu, jawabannya ada di bagian "Memori gak dibagi antar worker".
Satu proses, satu core
Di pelajaran Event Loop dan Worker Threads kamu sudah lihat: kode JavaScript jalan di satu thread. Worker Threads memindahkan hitungan berat ke thread lain di dalam proses yang sama. Clustering beda: dia menjalankan proses Node utuh beberapa kali, masing-masing dengan event loop dan memorinya sendiri. Berapa yang muat? os.availableParallelism() (Node 18.14 ke atas) memberi jumlah core yang boleh dipakai, misalnya 4. Versi lama memakai os.cpus().length dari pelajaran Built-in Modules.
Membuat cluster dengan modul cluster
app.js tetap seperti biasa. Tambahkan satu logger yang mencetak pid (nomor proses) worker yang menjawab, lalu buat file cluster.js:
// app.js: logger di paling atas, sisanya sama
app.use((req, res, next) => {
console.log(`[worker ${process.pid}] ${req.method} ${req.url}`);
next();
});
// cluster.js
const cluster = require("node:cluster");
const os = require("node:os");
if (cluster.isPrimary) {
const jumlah = os.availableParallelism();
console.log(`Primary ${process.pid} jalan, fork ${jumlah} worker`);
for (let i = 0; i < jumlah; i++) {
cluster.fork(); // jalankan file ini lagi sebagai worker
}
cluster.on("exit", (worker, code, signal) => {
console.log(`Worker ${worker.process.pid} mati (${signal || code}), fork pengganti`);
cluster.fork();
});
} else {
require("./app.js"); // worker: jalankan server seperti biasa
}
cluster.isPrimarybernilaitruedi proses yang kamu jalankan sendiri. Nama lamanyacluster.isMaster, sudah deprecated sejak Node 16.cluster.fork()menjalankan file yang sama sekali lagi sebagai proses baru. Di proses ituisPrimarybernilaifalse, jadi dia masuk keelsedan menjalankanapp.js.- Tiap worker memanggil
app.listen(3000), tapi gak kenaEADDRINUSEseperti di pelajaran HTTP Server Dasar: di cluster, port-nya dipegang primary, dan worker cuma menerima koneksi yang dioper. cluster.on("exit", ...)jalan saat sebuah worker mati. Di situ primary mem-fork penggantinya.
Hasil node cluster.js (pid-nya contoh, tiap kali beda):
$ node cluster.js
Primary 4810 jalan, fork 4 worker
Server jalan di http://localhost:3000
Server jalan di http://localhost:3000
Server jalan di http://localhost:3000
Server jalan di http://localhost:3000
Empat baris "Server jalan", satu dari tiap worker. Output semua worker muncul di terminal primary.
Siapa menjawab request: round-robin
Primary menerima tiap koneksi baru dan memberikannya ke worker bergiliran (round-robin). Lima kali curl -s localhost:3000/catatan mencetak ini di terminal server:
| Request ke- | Worker yang menjawab |
|---|---|
| 1 | [worker 4811] GET /catatan |
| 2 | [worker 4812] GET /catatan |
| 3 | [worker 4813] GET /catatan |
| 4 | [worker 4814] GET /catatan |
| 5 | [worker 4811] GET /catatan, giliran kembali ke awal |
Giliran ke-i (mulai 0) jatuh ke worker nomor i % jumlahWorker: sisa bagi itulah yang membuat urutannya berputar. Pembagiannya per koneksi, bukan per request: browser yang menjaga satu koneksi tetap terbuka (keep-alive) mengirim request-request berikutnya ke worker yang sama. Di Windows, pembagiannya diserahkan ke sistem operasi.
Memori gak dibagi antar worker
Array catatan di app.js ada di memori tiap worker, jadi ada empat salinan yang terpisah. Nyalakan ulang cluster, lalu:
| Langkah | Request | Worker | catatan di worker itu |
Jawaban |
|---|---|---|---|---|
| 1 | POST /catatan "Olahraga" |
4811 | 3 | 201, id: 3 |
| 2 | GET /catatan |
4812 | 2 | 2 catatan, "Olahraga" gak ada |
| 3 | GET /catatan |
4813 | 2 | 2 catatan |
| 4 | GET /catatan |
4814 | 2 | 2 catatan |
| 5 | GET /catatan |
4811 | 3 | 3 catatan |
Itu jawaban tebakannya: tergantung giliran. Cuma worker yang menyimpan yang memberi 3, tiga lainnya memberi 2. Lebih parah lagi, POST berikutnya yang jatuh ke worker 4812 juga membuat id: 3, dengan isi yang beda. Obatnya: simpan data di luar proses, di database (pelajaran Koneksi Database) atau Redis, supaya semua worker membaca tempat yang sama. Hal yang sama berlaku untuk session di memori dan penghitung rate limit.
Worker mati, primary menggantinya
Error yang gak ditangkap mematikan proses. Tanpa cluster, seluruh server ikut mati. Dengan cluster, cuma satu worker:
Worker 4812 mati (1), fork pengganti
Server jalan di http://localhost:3000
Request berikutnya dibagi ke worker yang masih hidup, dan pengganti dengan pid baru (misalnya 4815) ikut antre giliran. Isi memori worker yang mati hilang bersamanya.
PM2 cluster mode: yang biasa dipakai
Di production, cluster.js jarang ditulis sendiri. PM2 (process manager untuk Node) melakukan hal yang sama dengan modul cluster, ditambah restart otomatis, log, dan reload tanpa downtime:
$ pm2 start app.js -i 4 --name catatan-api
[PM2] Starting /home/budi/catatan-api/app.js in cluster_mode (4 instances)
[PM2] Done.
-i 4menjalankan 4 worker,-i maxsebanyak core.pm2 reload catatan-apimengganti worker satu per satu, jadi selalu ada yang menjawab.pm2 lsmenampilkan tiap worker dengan modecluster, statusonline, dan kolom↺(berapa kali di-restart).
🤔 Cluster, Worker Threads, atau replika container?
| Situasi | Pilih |
|---|---|
| API di VPS 4 core, satu core 100% saat ramai | cluster atau PM2 cluster mode |
| Satu endpoint menghitung berat (bikin PDF, hash) | Worker Threads |
| Jalan di Kubernetes atau Docker Swarm | 1 proses per container, tambah replika |
| API kecil yang lebih banyak menunggu database | Gak perlu, 1 proses cukup |
Cluster menambah berapa banyak request yang bisa dijawab bersamaan, bukan kecepatan satu request. Request yang makan 3 detik CPU tetap 3 detik.
Di latihan: cluster tiruan
Simulator latihan gak bisa mem-fork proses, jadi kode awalnya memberi object cluster tiruan: fork() membuat worker dengan pid 101, 102, 103, dan mencetak Forked worker pid=.... Kamu mencetak Primary 1 forking 3 workers, mem-fork 3 worker dengan for, lalu membagi 6 request dengan cluster.workers[i % cluster.workers.length] sambil mencetak "Request " + (i + 1) + " handled by worker " + worker.pid. Request ke-4 kembali ke worker 101.
⚠️ Jebakan yang sering ketemu
- Data penting di memori proses. Array, session, atau penghitung rate limit jadi beda di tiap worker. Batas 100 request per menit di memori berubah jadi sampai 400 dengan 4 worker.
- Lupa fork pengganti di
exit. Tiap crash mengurangi worker, sampai gak ada yang tersisa untuk menjawab. - Fork lebih banyak dari jumlah core. Prosesnya cuma berebut CPU, dan tiap worker makan memorinya sendiri (puluhan MB).
- Berharap cluster mempercepat satu request yang berat. Request itu tetap memblokir worker-nya. Untuk hitungan berat, pakai Worker Threads.
TL;DR: Satu proses Node memakai satu core. cluster membuat primary (cluster.isPrimary, dulu isMaster) yang mem-fork satu worker per core (os.availableParallelism()), dan semuanya berbagi port 3000. Primary membagi koneksi bergiliran (round-robin): giliran ke-i jatuh ke worker i % jumlah. Tiap worker punya memori sendiri, jadi data harus di database atau Redis, bukan di array. Saat worker mati, cluster.on("exit") mem-fork pengganti. Di production, pakai PM2: pm2 start app.js -i max. Cluster menambah kapasitas, bukan kecepatan satu request.