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:

js
// 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:

text
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 keluar

Mari kita bedah baris-per-barisnya:

  1. 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 ke mulai(port); pola ini menghindari menyetel angka port tetap yang bisa bertabrakan.
  2. Baris 14-22 (cabang worker): setiap worker menjalankan HTTP server sungguhan dengan cluster.worker.id sebagai identitas — primary memberi nomor urut mulai dari 1, dan nomor itu kita sisipkan ke body respons sebagai bukti siapa yang melayani. Setelah listen berhasil, worker melapor "siap" ke primary lewat kanal IPC.
  3. 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.
  4. Baris 32-34 (cluster.fork): dua worker diturunkan dengan environment PORT berisi angka port pinjaman tadi; fork() mengembalikan object Worker dan pendaftaran di cluster.workers berlangsung otomatis.
  5. Baris 39-52 (pengiriman permintaan): dua gelombang berisi tiga permintaan serentak dikirim lewat fetch. Body respons dikumpulkan ke sebuah Set lalu 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.
  6. Baris 58-67 (pembersihan): sebelum keluar, primary menghentikan semua worker lewat kill() dan menunggu event exit masing-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:

KebutuhanAlat yang tepatAlasan
Melayani lebih banyak koneksi I/Onode:clusterTiap worker punya event loop sendiri di core sendiri
Mempercepat komputasi CPU satu tugasworker_threadsSatu proses berbagi memori, biaya komunikasi kecil — Chapter Pola Worker Pool
Menambah kapasitas lintas mesinLoad balancer eksternalCluster 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:

bash Menjalankan aplikasi dengan PM2 mode cluster
pm2 start server.mjs -i max
pm2 ls
pm2 reload server

Flag -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.