Tutorials > How to use Cluster to increase Node.js performance

How to use Cluster to increase Node.js performance

Published on: 15 March 2021

Development Node.js

Introduction

Node.js is an open source framework for server-side Javascript code execution, which allows you to set up an easily scalable development environment.

One of the best solutions that Node.js provides is to divide a single process into several sub-processes, called workers. Therefore, through a Cluster module , these complex processes can be divided into smaller, simpler processes, significantly speeding up the applications in Node .

In this tutorial you will find all the information to start using Cluster and take advantage of multi-core systems with Node.js to improve the performance of your web server.

How the Cluster module works on Node.js

The cluster is a collection of small child processes (" workers ") of a single parent process in Node .

Using the fork () method of the Node child_processes module, workers are created as child processes of a parent process, whose task is, instead, that of controlling them.

To start including a cluster in your application, open your app's .js file and enter, in the variable declaration, as follows:

var cluster = require('cluster');

Now, the cluster module will need to figure out which part of the code is the parent or master process , and which part is instead that of the workers .

Use the following syntax to identify the master :

if(cluster.isMaster){...}

Inside the curly brackets, specify the instructions of the master. The first instructions to be specified are those to initialize the child processes, using the fork () function  :

cluster.fork();

Within this method, methods and properties regarding the affected worker object can be specified.

A cluster module contains several events. The most common are the online event, to indicate the activation of a worker, and exit , to indicate the end of the worker .

Some examples of the most common use of the Cluster module are shown below .

Examples

How the Cluster module is used in a Node.js app

In the first example, set up a small server that responds to incoming requests by returning the ID of the worker process that handled your request.

The parent process, in this case, will consist of 4 child processes.

Here is the sample code that implements this use case:

var cluster = require('cluster');
var http = require('http');
var numCPUs = 4;

if (cluster.isMaster) {
 for (var i = 0; i < numCPUs; i++) {
  cluster.fork();
 }
} else {
 http.createServer(function(req, res) {
  res.writeHead(200);
  res.end('process ' + process.pid + ' says hello!');
 }).listen(8000);
}

By saving this code in a file, which can have a name of your choice, all you have to do is run it by using the command:

$ node file_name.js

Subsequently, by connecting to the IP address of your server, through the specified port (8000), the execution of this small program can be controlled.

Note: to specify the port, just enter " : port number " as a suffix of your server address .

How to developing an easily scalable Express server

One of the most famous web frameworks for Node.js is Express,  which is written in Javascript and hosted inside a Node.js runtime environment.

In this second example, it is shown how to create an easily scalable Express server and how to allow a single server process to take advantage of the cluster module with a few lines of code.

var cluster = require('cluster');

if(cluster.isMaster) {
 var numWorkers = require('os').cpus().length;

 console.log('Master cluster setting up ' + numWorkers + ' workers...');

 for(var i = 0; i < numWorkers; i++) {
  cluster.fork();
 }

 cluster.on('online', function(worker) {
  console.log('Worker ' + worker.process.pid + ' is online');
 });

 cluster.on('exit', function(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 {
 var app = require('express')();
 app.all('/*', function(req, res) {res.send('process ' + process.pid + ' says hello!').end();})

 var server = app.listen(8000, function() {
  console.log('Process ' + process.pid + ' is listening to all incoming requests');
 });
}

Analyzing and interpreting the code:

  • The number of CPU cores is retrieved via the os module. The module contains an ad-hoc function called cpus () which returns an array consisting of the cores. The number of cores in use is obtained by measuring its length,. This helps understand how many workers could be used to optimize the system.
  • Another part of the code describes the end of the life cycle of the worker , signaled by the exit event. Through a callback function, a new worker can be placed in response to the "death" of another, to maintain the intended number of workers .
  • Finally, the online event is described to report that a worker has been created and is ready to receive a new request.

Advanced operations

Getting masters and workers to communicate

It is important to manage the exchange of messages between the parent processes and their children in order to assign tasks or perform other operations.

To view the messages, set up a procedure that recognizes the message event for both the master and worker side:

worker.on('message', function(message){
 console.log(message);
});

Within the code, the worker object is the reference returned by the fork () method .

Now, switch to listening to messages by the master as follows :

process.on('message', function(message) {
 console.log(message);
});

Messages can be strings or JSON objects. To send one, for example, from parent to child process (from master to worker ), you can try the command:

worker.send('hello from the master');

To send the message in the opposite direction, write:

process.send('hello from worker with id: ' + process.pid);

To learn more about the content of messages, they could be considered as JSON objects containing additional information. Here is an example:

worker.send({
 type: 'task 1',
 from: 'master',
 data: {
  // the data that you want to transfer
 }
});

Resetting the downtime

The best result that can be obtained thanks to clusters is zero-downtime.

This can be achieved by trying to manage the termination and restart of workers, one at a time, following application changes. This will allow you to leave the application running at the previous stage while the one with the new changes applied is being loaded.

To make it happen, some principles should be kept in mind:

  • The master process must continue to run all the time and only the workers are affected by the restart;
  • The master process must be short and must only be in charge of managing its subprocesses.
  • There’s a need for a way to let the master know that one of its workers has to be restarted. For example, you can require an input from the user, or check for changes to the files.

To successfully restart a worker, it is essential to first try to close them safely and only "kill" them if they don't respond.

To implement this method, try to send a message specifying the shutdown type :

workers[wid].send({type: 'shutdown', from: 'master'});

Finally, specify the instructions to execute in case of a shutdown request :

process.on('message', function(message) {
 if(message.type === 'shutdown') {
  process.exit(0);
 }
});

Once the function that allows you to request and close a worker has been completed, these instructions have to be applied to all existing threads managed by that master .

The cluster module allows you to have a reference to all the workers in execution and all the instructions of the restart procedure can also be grouped within a single function that will be called whenever you want to restart all the workers .

function restartWorkers() {
 var wid, workerIds = [];

 for(wid in cluster.workers) {
  workerIds.push(wid);
 }

 workerIds.forEach(function(wid) {
  cluster.workers[wid].send({
 text: 'shutdown',
 from: 'master'
  });

  setTimeout(function() {
 if(cluster.workers[wid]) {
  cluster.workers[wid].kill('SIGKILL');
 }
  }, 5000);
 });
};

Through the workers object, all the IDs of the threads running in real time can be obtained.

With this code, in fact, the following operations will have been defined:

  • Get the id of the currently running workers and save it in an array named workerIds;
  • Request safe shutdown of every running worker ;
  • If safe shutdown does not occur within 5 seconds, then force shutdown via the kill command.

Conclusions

At this point, you should know the advantages of using Cluster, especially with your Node web server, and some ways to use it effectively to improve the performance of your web server.

Do not forget to deepen the characteristics of C luster and to fully understand how it can be adapted to your web server in order to avoid any bugs and malfunctions.