Skip to main content

How to scale your Node.js server using clustering

Scalability is a hot topic in tech, and every programming language or framework provides its own way of handling high loads of traffic.
Today, we’re going to see an easy and straightforward example about Node.js clustering. This is a programming technique which will help you parallelize your code and speed up performance.
“A single instance of Node.js runs in a single thread. To take advantage of multi-core systems, the user will sometimes want to launch a cluster of Node.js processes to handle the load.”
Node.js Documentation
We’re gonna create a simple web server using Koa, which is really similar to Express in terms of use.
The complete example is available in this Github repository.
We’ll build a simple web server which will act as follows:
  1. Our server will receive a POST request, we’ll pretend that user is sending us a picture.
  2. We’ll copy an image from the filesystem into a temporary directory.
  3. We’ll flip it vertically using Jimp, an image processing library for Node.js.
  4. We’ll save it to the file system.
  5. We’ll delete it and we’ll send a response to the user.
Of course, this is not a real world application, but is pretty close to one. We just want to measure the benefits of using clustering.
I’m gonna use yarn to install my dependencies and initialize my project:
$ yarn init
$ yarn add -D forever
$ yarn add jimp koa koa-router

Since Node.js is single threaded, if our web server crashes, it will remain down until some other process will restarts it. So we’re gonna install forever, a simple daemon which will restart our web server if it ever crashes.
We’ll also install Jimp, Koa and Koa Router.
This is the folder structure we need to create:
We’ll have an src folder which contains two JavaScript files: cluster.js and standard.js .
The first one will be the file where we’ll experiment with the cluster module. The second is a simple Koa server which will work without any clustering.
In the module directory, we’re gonna create two files: job.js and log.js.
job.js will perform the image manipulation work. log.js will log every event that occurs during that process.
Log module will be a simple function which will take an argument and will write it to the stdout (similar to console.log).
It will also append the current timestamp at the beginning of the log. This will allow us to check when a process started and to measure its performance.
module.exports = function log(args) {
    return process.stdout.write(`[${new Date()}] - ${args}\n`)
  }
  node-log.js


I’ll be honest, this is not a beautiful and super-optimized script. It’s just an easy job which will allow us to stress our machine.
const fs   = require('fs')
const jimp = require('jimp')
const log  = require('./log')

module.exports = function runJob() {

  return new Promise(async (resolvereject=> {

    const randomNumber = () => Math.floor(Math.random() * 1995) * 15
    const destFileName = `${__dirname}/../imgs/dest/${randomNumber()}-img.jpg`

    log(`Copying ${destFileName}`)
    fs.copyFileSync(`${__dirname}/../imgs/landscape.jpg`destFileName)

    log(`Flipping ${destFileName}`)
    const image = await jimp.read(destFileName)
    image.flip(truefalse)

    log(`Deleting ${destFileName}`)
    fs.unlink(destFileName, (err=> {
      return err ? reject(err) : resolve('success')
    })
    
  })

}
node-job.js 

We’re gonna create a very simple webserver. It will respond on two routes with two different HTTP methods.
We’ll be able to perform a GET request on http://localhost:3000/. Koa will respond with a simple text which will show us the current PID (process id).
The second route will only accept POST requests on the /flip path, and will perform the job that we just created.
We’ll also create a simple middleware which will set an X-Response-Time header. This will allow us to measure the performance.
const Koa    = require('koa')
const Router = require('koa-router')
const runJob = require('./modules/job')
const log    = require('./modules/log')
const router = new Router()
const app    = new Koa()

router.get('/'async ctx => ctx.body = `PID ${process.pid} listening here!`)
      .post('/flip'async ctx => {
        const res = await runJob()
        ctx.body  = res
      })

app.use(async (ctxnext=> {
      await next();
      const rt = ctx.response.get('X-Response-Time');
      log(`${ctx.method} ${ctx.url} - ${rt}`);
    })
    .use(async (ctxnext=> {
      const start = Date.now();
      await next();
      const ms = Date.now() - start;
      ctx.set('X-Response-Time'`${ms}ms`);
    })
    .use(router.routes())
    .listen(3000)
koa-standard.js


The Node.js server will immediately copy our image and start to flip it.
The first response will be logged after 16 seconds and the last one after 40 seconds.
Such a dramatic performance decrease! With just 10 concurrent requests, we decreased the webserver performance by 950%!
:
emember what I mentioned at the beginning of the article?
To take advantage of multi-core systems, the user will sometimes want to launch a cluster of Node.js processes to handle the load.
Depending on which server we’re gonna run our Koa application, we could have a different number of cores.
Every core will be responsible for handling the load individually. Basically, each HTTP request will be satisfied by a single core.
So for example — my machine, which has eight cores, will handle eight concurrent requests.
We can now count how many CPUs we have thanks to the os module:
const { cpus } = require('os') const numWorkers = cpus().length
The cpus() method will return an array of objects that describe our CPUs. We can bind its length to a constant which will be called numWorkers, ’cause that’s the number of workers that we’re gonna use.
We’re now ready to require the cluster module.
const cluster = require('cluster')
We now need a way of splitting our main process into N distinct processes.
We’ll call our main process master and the other processes workers.
Node.js cluster module offers a method called isMaster. It will return a boolean value that will tell us if the current process is directed by a worker or master:
const cluster = require('cluster') const isMaster = cluster.isMaster
Great. The golden rule here is that we don’t want to serve our Koa application under the master process.
We want to create a Koa application for each worker, so when a request comes in, the first free worker will take care of it.
The cluster.fork() method will fit our purpose:
const cluster    = require('cluster')
const { cpus }   = require('os')
const numWorkers = cpus().length
const isMaster   = cluster.isMaster

if (isMaster) {

  process.stdout.write('I am master!')
  const workers = [...Array(numWorkers)].map(_ => cluster.fork())
  
else {

  process.stdout.write('I am a worker!')
  
}

Ok, at first that may be a little tricky.
As you can see in the script above, if our script has been executed by the master process, we’re gonna declare a constant called workers. This will create a worker for each core of our CPU, and will store all the information about them.

If you feel unsure about the adopted syntax, using […Array(x)].map() is just the same as:

let workers = []
  
for(let i = 0i < numWorkersi++) {
  workers.push(cluster.fork())
}
Let’s copy our Koa app structure into the else statement, so we will be sure that it will be served by a worker:

const cluster  = require('cluster')
const { cpus } = require('os')
const log      = require('./modules/log')

const isMaster   = cluster.isMaster
const numWorkers = cpus().length

if (isMaster) {

  log(`Forking ${numWorkers} workers`)
  const workers = [...Array(numWorkers)].map(_ => cluster.fork())

  cluster.on('online', (worker=> log(`Worker ${worker.process.pid} is online`))
  cluster.on('exit', (workerexitCode=> {
    log(`Worker ${worker.process.id} exited with code ${exitCode}`)
    log(`Starting a new worker`)
    cluster.fork()
  })
  
else {

  const Koa    = require('koa')
  const Router = require('koa-router')
  const runJob = require('./modules/job')
  const router = new Router()
  const app    = new Koa()

  router.get('/'async ctx => ctx.body = `PID ${process.pid} listening here!`)
        .post('/flip'async ctx => {
          const res = await runJob()
          ctx.body  = res
        })

  app.use(async (ctxnext=> {
        await next();
        const rt = ctx.response.get('X-Response-Time');
        log(`${ctx.method} ${ctx.url} - ${rt}`);
      })
      .use(async (ctxnext=> {
        const start = Date.now();
        await next();
        const ms = Date.now() - start;
        ctx.set('X-Response-Time'`${ms}ms`);
      })
      .use(router.routes())
      .listen(3000)

}
view rawkoa-cluster.js hosted with ❤ by GitHub

As you can see, we also added a couple of event listeners in the isMaster statement:

cluster.on('online', (worker=> log(`Worker ${worker.process.pid} is online`))
  
cluster.on('exit', (workerexitCode=> {
  log(`Worker ${worker.process.id} exited with code ${exitCode}`)
  log(`Starting a new worker`)
  cluster.fork()
}
The first one will tell us that a new worker has been spawned. The second one will create a new worker when one other worker crashes.
That way, the master process will only be responsible for creating new workers and orchestrating them. Every worker will serve an instance of Koa which will be accessible on the :3000 port.


Comments

Popular posts from this blog

How to use Ngx-Charts in Angular ?

Charts helps us to visualize large amount of data in an easy to understand and interactive way. This helps businesses to grow more by taking important decisions from the data. For example, e-commerce can have charts or reports for product sales, with various categories like product type, year, etc. In angular, we have various charting libraries to create charts.  Ngx-charts  is one of them. Check out the list of  best angular chart libraries .  In this article, we will see data visualization with ngx-charts and how to use ngx-charts in angular application ? We will see, How to install ngx-charts in angular ? Create a vertical bar chart Create a pie chart, advanced pie chart and pie chart grid Introduction ngx-charts  is an open-source and declarative charting framework for angular2+. It is maintained by  Swimlane . It is using Angular to render and animate the SVG elements with all of its binding and speed goodness and uses d3 for the excellent math functio...

JavaScript new features in ES2019(ES10)

The 2019 edition of the ECMAScript specification has many new features. Among them, I will summarize the ones that seem most useful to me. First, you can run these examples in  node.js ≥12 . To Install Node.js 12 on Ubuntu-Debian-Mint you can do the following: $sudo apt update $sudo apt -y upgrade $sudo apt update $sudo apt -y install curl dirmngr apt-transport-https lsb-release ca-certificates $curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash - $sudo apt -y install nodejs Or, in  Chrome Version ≥72,  you can try those features in the developer console(Alt +j). Array.prototype.flat && Array.prototype. flatMap The  flat()  method creates a new array with all sub-array elements concatenated into it recursively up to the specified depth. let array1 = ['a','b', [1, 2, 3]]; let array2 = array1.flat(); //['a', 'b', 1, 2, 3] We should also note that the method excludes gaps or empty elements in the array: let array1 ...

Understand Angular’s forRoot and forChild

  forRoot   /   forChild   is a pattern for singleton services that most of us know from routing. Routing is actually the main use case for it and as it is not commonly used outside of it, I wouldn’t be surprised if most Angular developers haven’t given it a second thought. However, as the official Angular documentation puts it: “Understanding how  forRoot()  works to make sure a service is a singleton will inform your development at a deeper level.” So let’s go. Providers & Injectors Angular comes with a dependency injection (DI) mechanism. When a component depends on a service, you don’t manually create an instance of the service. You  inject  the service and the dependency injection system takes care of providing an instance. import { Component, OnInit } from '@angular/core'; import { TestService } from 'src/app/services/test.service'; @Component({ selector: 'app-test', templateUrl: './test.component.html', styleUrls: ['./test.compon...