Express.js Cluster Module
The Cluster module in Node.js allows you to create child processes (workers) that share the same server port, enabling you to take advantage of multi-core systems and improve the performance of your Express.js applications. This guide covers key concepts, examples, and best practices for using the Cluster module in Express.js applications.
Key Concepts of Cluster Module
- Cluster: A module that enables the creation of child processes to handle concurrent connections.
- Master Process: The primary process that forks worker processes.
- Worker Process: A child process created by the master process to handle requests.
- Forking: Creating child processes from a parent process.
Setting Up a Cluster
Set up a cluster to fork worker processes and handle incoming requests:
Example: Basic Cluster Setup
// cluster-setup.js
const cluster = require('cluster');
const http = require('http');
const numCPUs = require('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`);
});
} else {
// Workers can share any TCP connection
const express = require('express');
const app = express();
const port = 3000;
app.get('/', (req, res) => {
res.send('Hello, World!');
});
app.listen(port, () => {
console.log(`Worker ${process.pid} started`);
});
}
Handling Worker Events
Handle events related to worker processes, such as exit and online events:
Example: Handling Worker Events
// cluster-events.js
const cluster = require('cluster');
const os = require('os');
const numCPUs = os.cpus().length;
if (cluster.isMaster) {
console.log(`Master ${process.pid} is running`);
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}
cluster.on('online', (worker) => {
console.log(`Worker ${worker.process.pid} is online`);
});
cluster.on('exit', (worker, code, signal) => {
console.log(`Worker ${worker.process.pid} died with code: ${code}, and signal: ${signal}`);
console.log('Starting a new worker');
cluster.fork();
});
} else {
const express = require('express');
const app = express();
const port = 3000;
app.get('/', (req, res) => {
res.send('Hello, World!');
});
app.listen(port, () => {
console.log(`Worker ${process.pid} started`);
});
}
Communicating with Workers
Master and worker processes can communicate using the send()
method:
Example: Master-Worker Communication
// cluster-communication.js
const cluster = require('cluster');
const os = require('os');
const numCPUs = os.cpus().length;
if (cluster.isMaster) {
console.log(`Master ${process.pid} is running`);
for (let i = 0; i < numCPUs; i++) {
const worker = cluster.fork();
worker.on('message', (msg) => {
console.log(`Master received message from worker ${worker.process.pid}:`, msg);
});
worker.send({ msg: 'Hello Worker' });
}
cluster.on('exit', (worker, code, signal) => {
console.log(`Worker ${worker.process.pid} died`);
cluster.fork();
});
} else {
const express = require('express');
const app = express();
const port = 3000;
process.on('message', (msg) => {
console.log(`Worker ${process.pid} received message from master:`, msg);
});
app.get('/', (req, res) => {
process.send({ msg: 'Hello Master' });
res.send('Hello, World!');
});
app.listen(port, () => {
console.log(`Worker ${process.pid} started`);
});
}
Balancing Load Across Workers
Distribute the incoming load evenly across the workers to optimize resource usage:
Example: Load Balancing
// load-balancing.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`);
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}
cluster.on('exit', (worker, code, signal) => {
console.log(`Worker ${worker.process.pid} died`);
cluster.fork();
});
} else {
const express = require('express');
const app = express();
const port = 3000;
app.get('/', (req, res) => {
res.send('Hello, World!');
});
app.listen(port, () => {
console.log(`Worker ${process.pid} started`);
});
}
Graceful Shutdown of Workers
Ensure workers are shut down gracefully to handle ongoing requests before exiting:
Example: Graceful Shutdown
// graceful-shutdown.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`);
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}
cluster.on('exit', (worker, code, signal) => {
console.log(`Worker ${worker.process.pid} died`);
cluster.fork();
});
process.on('SIGTERM', () => {
for (const id in cluster.workers) {
cluster.workers[id].kill('SIGTERM');
}
});
} else {
const express = require('express');
const app = express();
const port = 3000;
app.get('/', (req, res) => {
res.send('Hello, World!');
});
app.listen(port, () => {
console.log(`Worker ${process.pid} started`);
});
process.on('SIGTERM', () => {
console.log(`Worker ${process.pid} exiting...`);
process.exit();
});
}
Best Practices for Using Cluster Module
- Graceful Shutdown: Implement graceful shutdown to handle ongoing requests before exiting.
- Monitor Workers: Monitor worker processes and restart them if they crash.
- Load Balancing: Distribute the load evenly across worker processes to optimize resource usage.
- Communication: Use the
send()
method to facilitate communication between master and worker processes. - Security: Ensure secure communication and validation of data between processes.
Testing Cluster Module
Test your cluster-based code using frameworks like Mocha, Chai, and Supertest:
Example: Testing Cluster Module
// Install Mocha, Chai, and Supertest
// npm install --save-dev mocha chai supertest
// test/cluster.test.js
const chai = require('chai');
const expect = chai.expect;
const request = require('supertest');
const cluster = require('cluster');
const express = require('express');
const numCPUs = require('os').cpus().length;
if (cluster.isMaster) {
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}
} else {
const app = express();
app.get('/', (req, res) => {
res.send('Hello, World!');
});
const server = app.listen(0, () => {
process.env.PORT = server.address().port;
});
}
describe('GET /', () => {
it('should return Hello, World!', (done) => {
const port = process.env.PORT;
request(`http://localhost:${port}`)
.get('/')
.expect(200)
.end((err, res) => {
if (err) return done(err);
expect(res.text).to.equal('Hello, World!');
done();
});
});
});
// Define test script in package.json
// "scripts": {
// "test": "mocha"
// }
// Run tests with NPM
// npm run test
Key Points
- Cluster: A module that enables the creation of child processes to handle concurrent connections.
- Master Process: The primary process that forks worker processes.
- Worker Process: A child process created by the master process to handle requests.
- Follow best practices for using the Cluster module, such as implementing graceful shutdown, monitoring workers, load balancing, enabling communication between processes, and ensuring security.
Conclusion
The Cluster module in Node.js allows you to create child processes (workers) that share the same server port, enabling you to take advantage of multi-core systems and improve the performance of your Express.js applications. By understanding and implementing the key concepts, examples, and best practices covered in this guide, you can effectively manage clusters in your Express.js applications. Happy coding!