Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Nodejs Clustering with Sticky-Session

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
  // In this case it is an HTTP server



  var sticky = require('sticky-session');
  var express = require('express');
  var app = express();

  app.get('/', function (req, res) {
      console.log('worker: ' + cluster.worker.id);
      res.send('Hello World!');
  });


  var server = http.createServer(app);
      sticky.listen(server,3000);

  console.log(`Worker ${process.pid} started`);
}

I looked up the documentation for nodejs clustering and sticky-session and another stack overflow answer regarding this

  var cluster = require('cluster');
  var http = require('http');
  var sticky = require('sticky-session');
  var express = require('express');
  var app = express();

  app.get('/', function (req, res) {
      console.log('worker: ' + cluster.worker.id);
      res.send('Hello World!');
  });


  var server = http.createServer(app);
      sticky.listen(server,3000);

If the above snippet is run without forking it works fine but else never works as shown in the clustered example above in which the threads are started but server is never initialised .

I read there is alternative of sticky-cluster can somebody give a proper authoritative answer on this topic which will be useful for people looking for the same and the another main issue comes with this is the app.locals object which is used to store variables for an app instance and the occurrence multiple server instances causes this to break as values will be different across different instances so this approach causes a big issue and app breaks so .When answering please don't copy paste some code please give a detailed answer detailing the approach its benefit and short comings.

I am not looking for a answer that is limited to using sticky-sessions nodejs module, I welcome all other approaches in which all cores of the processor are used and but ensuring session continuity .

If it involves RedisStore or MongoDb store its ok,What I want to know is about a standard approach in case of nodejs application with clustering with session continuity

https://github.com/indutny/sticky-session

https://nodejs.org/api/cluster.html

https://stackoverflow.com/a/37769107/3127499

like image 418
codefreaK Avatar asked Jul 12 '18 08:07

codefreaK


2 Answers

There is a small problem in your code. "sticky-session" module already uses node.js "cluster" module within.You dont need to "fork()" because sticky-session will already do it for you. Lets find out how:

var cluster = require('cluster'); // Only required if you want the worker id
var sticky = require('sticky-session');

var server = require('http').createServer(function(req, res) {
  res.end('worker: ' + cluster.worker.id);
});
sticky.listen(server, 3000);

calling sticky.listen() will already spawn workers for you.See the listen() implementation below

 function listen(server, port, options) {
  if (!options)
    options = {};

  if (cluster.isMaster) {
    var workerCount = options.workers || os.cpus().length;

    var master = new Master(workerCount, options.env);
    master.listen(port);
    master.once('listening', function() {
      server.emit('listening');
    });
    return false;
  }
  return true;
}

This line var master = new Master(workerCount, options.env) is responsible for spawning workers. see the Master() implementation below:

function Master(workerCount, env) {
  net.Server.call(this, {
    pauseOnConnect: true
  }, this.balance);

  this.env = env || {};

  this.seed = (Math.random() * 0xffffffff) | 0;
  this.workers = [];

  debug('master seed=%d', this.seed);

  this.once('listening', function() {
    debug('master listening on %j', this.address());

    for (var i = 0; i < workerCount; i++)
      // spawning workers
      this.spawnWorker();
  });
}

So indeed when you call sticky.listen(server,port) you are actually calling cluster.fork().hence you should not explicitly again call fork(). Now your code should look like:

var cluster = require('cluster'); // Only required if you want the worker id
var sticky = require('sticky-session');

var server = require('http').createServer(function(req, res) {
  res.end('worker: ' + cluster.worker.id);
});

//sticky.listen() will return false if Master
if (!sticky.listen(server, 3000)) { 
  // Master code
  server.once('listening', function() {
    console.log('server started on 3000 port');
  });
} else {
  // Worker code
}

One important thing to remember is that spawned workers will have its own EVENTLOOP and memory hence resources are not shared among each other. You can use "REDIS" or other npm modules such as "memored" to share resources among different workers.

Hope this solves your both issues.

like image 159
Vasi Anurag Avatar answered Sep 22 '22 03:09

Vasi Anurag


I think you are confusing sticky session with shared memory store.

Let me try to help:

  • Sticky-sessions module is balancing requests using their IP address. Thus client will always connect to same worker server, and socket.io will work as expected, but on multiple processes!

Implementing sticky sessions means that you now have multiple nodes accepting connections. However, it DOES NOT guarantee that these nodes will SHARE the same memory, as each worker has their own eventloop and internal memory state.

In other words, data being processed by one node may not be available to other worker nodes, which explains the issue you pointed out.

...another main issue comes with this is the app.locals object which is used to store variables for an app instance and the occurrence multiple server instances causes this to break as values will be different across different instances so this approach causes a big issue and app breaks...

Thus, to resolve this, we would require using something like Redis so that data can be shared across multiple nodes.

Hope this helps!

like image 41
alchuang Avatar answered Sep 26 '22 03:09

alchuang