Node Cluster Fundamentals
Purpose of Clustering
1. Improve Application Performance
- A single Node.js process is single-threaded, meaning it can only handle one request at a time. With high concurrency, a single process can become a bottleneck.
- By creating multiple worker processes, each capable of handling requests independently, the application’s throughput and response speed are significantly enhanced.
2. Utilize Multi-Core CPUs
- Modern servers typically feature multi-core CPUs, but due to Node.js’s event loop mechanism, a single process cannot fully leverage these cores.
- The
clustermodule allows creating a worker process for each CPU core, ensuring all available cores are utilized, thereby improving hardware resource efficiency.
3. Load Balancing
- In cluster mode, the master process listens for network requests and distributes them to idle worker processes.
- This load balancing mechanism ensures no single worker process is overloaded while others remain idle, achieving more even resource allocation.
Introduction to Node.js Cluster Module
Role of the Cluster Module
- The
clustermodule is a built-in Node.js module designed for creating and managing a group of worker processes. - It provides APIs to control the lifecycle of worker processes, including starting, restarting, shutting down, and inter-process communication.
Master Process
The master process is the initial process in a cluster, typically started directly by a Node.js script. It is responsible for creating and managing all worker processes, listening for network connections, and distributing connections to workers.
Starting Worker Processes:
- The master process uses the
cluster.fork()method to create multiple worker processes, typically based on the number of CPU cores to maximize hardware utilization.
Listening for Ports and Distributing Connections:
- The master process listens on a network port to receive client connection requests.
- Upon receiving a new connection, the master selects a worker process with lower current load (e.g., using a round-robin strategy) and forwards the connection to it.
- This mechanism ensures load balancing, preventing any worker process from becoming overloaded due to excessive requests.
Managing Worker Process Lifecycle:
- The master process monitors the status of all worker processes, including whether they are running normally or need restarting.
- If a worker process exits abnormally, the master can automatically restart it to ensure cluster stability and high availability.
- The master can also control worker behavior by sending signals or messages, such as gracefully shutting down a process.
Worker Processes
- Worker processes are child processes created by the master using
cluster.fork(). - Each worker process has its own event loop and memory space, allowing it to execute application logic independently.
- If a worker process exits, the master can automatically restart a new one to maintain cluster stability.
Executing Application Logic:
- Each worker process runs the same business logic but has its own isolated memory space and event loop.
- This ensures that each worker can handle requests independently without interference from other processes.
Isolation and Independence:
- The isolation between worker processes ensures that a crash in one process does not affect others.
- Each worker process has its own V8 engine instance, enabling parallel task execution and improving overall processing capacity.
Automatic Restart Mechanism:
- If a worker process terminates due to an error or exception, the master automatically restarts a new worker to replace it.
- This mechanism enhances application robustness, allowing the application to continue running even if some worker processes fail.
Node Cluster Implementation
Structure of the Cluster Module
- Master Process: The initial process in the cluster, responsible for creating and managing all worker processes.
- Worker Processes: Child processes created by the master, each with its own event loop and memory space, capable of handling requests independently.
Steps to Implement Clustering
Determine the Master Process
Use the cluster.isMaster property to check if the current process is the master process.
if (cluster.isMaster) {
// Master process code
} else {
// Worker process code
}Create Worker Processes
In the master process, use cluster.fork() to create multiple worker processes. Typically, the number of workers matches the number of CPU cores.
const os = require('os');
const numCPUs = os.cpus().length;
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}Listen for Worker Process Events
The master process should listen for the exit event to automatically restart a worker process if it exits unexpectedly.
cluster.on('exit', (worker, code, signal) => {
console.log(`Worker ${worker.process.pid} died`);
cluster.fork(); // Restart worker process
});Worker Process Code
In worker processes, implement the application logic, such as creating an HTTP server.
if (!cluster.isMaster) {
const http = require('http');
const server = http.createServer((req, res) => {
res.writeHead(200);
res.end('Hello from worker ' + process.pid + '\n');
});
server.listen(8000);
}Example Code
// cluster-master.js
const cluster = require('cluster');
const os = require('os');
const http = require('http');
const numCPUs = os.cpus().length;
if (cluster.isMaster) {
console.log(`Master ${process.pid} is running`);
// Fork workers
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}
cluster.on('exit', (worker, code, signal) => {
console.log(`Worker ${worker.process.pid} died`);
console.log(`Starting a new worker`);
cluster.fork();
});
} else {
// Workers share TCP connection
// In this case, an HTTP server
http.createServer((req, res) => {
res.writeHead(200);
res.end('Hello World\n');
}).listen(8000);
}Inter-Process Communication
In a Node.js cluster, communication between the master and worker processes is a critical feature, enabling data sharing, coordination, and event responses. The cluster module provides mechanisms like process.send() and process.on('message') for this purpose.
Sending Messages with process.send()
The process.send() method allows worker processes to send messages to the master process asynchronously, avoiding execution blocking.
// Worker process code
if (!cluster.isMaster) {
process.on('message', (msg) => {
console.log(`Received message: ${msg}`);
});
// Send message to master process
process.send({ type: 'status', data: 'Worker is ready' });
}Receiving Messages with process.on('message')
The process.on('message') method listens for message events in the master or worker processes, triggering a callback with the received message.
// Master process code
if (cluster.isMaster) {
cluster.on('listening', (worker, address) => {
console.log(`Worker ${worker.process.pid} is listening at ${address.address}:${address.port}`);
});
cluster.on('message', (worker, msg) => {
console.log(`Received message from worker ${worker.process.pid}:`, msg);
});
}Communication Example
Below is a complete example demonstrating how the master process receives messages from workers and responds:
// cluster-master.js
const cluster = require('cluster');
const os = require('os');
if (cluster.isMaster) {
const numCPUs = os.cpus().length;
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}
cluster.on('message', (worker, msg) => {
if (msg.type === 'status') {
console.log(`Worker ${worker.process.pid} says: ${msg.data}`);
}
});
} else {
// Worker process code
process.on('message', (msg) => {
console.log(`Received message: ${msg}`);
});
// Send initialization status message
process.send({ type: 'status', data: 'Worker is ready' });
// Listen for master process commands
process.on('message', (msg) => {
if (msg.command === 'shutdown') {
process.exit();
}
});
}In this example:
- The worker process sends an initialization status message to the master upon startup.
- The master listens for these messages and logs the worker’s status.
- The master can send commands (e.g.,
shutdown) to workers, which exit upon receiving them.
Load Balancing
Load balancing in Node.js clusters involves distributing incoming network requests fairly across worker processes to prevent overloading and maximize resource utilization. By default, the cluster module uses a simple round-robin strategy, but this may not suffice for all scenarios. Below, we explore advanced load balancing strategies with code examples.
Understanding Default Load Balancing
By default, when the master process receives a new connection, it forwards it to the next available worker process using an internal round-robin mechanism.
// Master process code
if (cluster.isMaster) {
const http = require('http');
const server = http.createServer();
server.on('request', (req, res) => {
// Forward request to next worker
cluster.nextTick(() => {
let worker = cluster.workers[nextWorkerId];
worker.send({ type: 'request', req: req, res: res });
nextWorkerId = (nextWorkerId + 1) % Object.keys(cluster.workers).length;
});
});
server.listen(8000);
}Implementing Custom Load Balancing Strategies
If the default round-robin strategy is insufficient, you can implement custom load balancing. For example, you can distribute requests based on the current load of worker processes or specific request attributes (e.g., URL or source IP).
// Master process code
if (cluster.isMaster) {
const http = require('http');
const server = http.createServer();
server.on('connection', (socket) => {
let leastBusyWorker = null;
let minLoad = Infinity;
// Find the least busy worker
for (let id in cluster.workers) {
const worker = cluster.workers[id];
if (worker.busy < minLoad) {
leastBusyWorker = worker;
minLoad = worker.busy;
}
}
// Forward connection to the selected worker
यदि (leastBusyWorker) {
leastBusyWorker.emit('message', { type: 'connection', socket: socket });
leastBusyWorker.busy++;
}
});
server.listen(8000);
}In this example, we maintain a busy counter to track each worker’s current load. When a new connection is assigned, the counter increments, and it decrements when the connection ends, ensuring the master always knows which worker has the lowest load.
Updating Worker Load
To make the load balancing strategy effective, workers must update their load status. This can be achieved by listening for message events and notifying the master when tasks are completed.
// Worker process code
if (!cluster.isMaster) {
process.on('message', (msg) => {
if (msg.type === 'connection') {
const socket = msg.socket;
socket.on('end', () => {
// Notify master to reduce load after task completion
process.send({ type: 'load', value: -1 });
});
// Handle connection...
}
});
}Complete Load Balancing Code
Combining the above snippets, here’s a complete load balancing implementation:
// cluster-master.js
const cluster = require('cluster');
const http = require('http');
if (cluster.isMaster) {
const numCPUs = require('os').cpus().length;
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}
const server = http.createServer();
server.on('connection', (socket) => {
let leastBusyWorker = null;
let minLoad = Infinity;
for (let id in cluster.workers) {
const worker = cluster.workers[id];
if (worker.busy < minLoad) {
leastBusyWorker = worker;
minLoad = worker.busy;
}
}
if (leastBusyWorker) {
leastBusyWorker.emit('message', { type: 'connection', socket: socket });
leastBusyWorker.busy++;
}
});
server.listen(8000);
cluster.on('message', (worker, msg) => {
if (msg.type === 'load') {
worker.busy += msg.value;
}
});
}
// worker.js
if (!cluster.isMaster) {
process.on('message', (msg) => {
if (msg.type === 'connection') {
const socket = msg.socket;
socket.on('end', () => {
process.send({ type: 'load', value: -1 });
});
// Handle connection...
}
});
}This code demonstrates how to implement a custom load balancing strategy in a Node.js cluster to manage resources efficiently and improve application responsiveness.



