Clustering in Node.js

I recently had to setup a Node.js application for production deployment for a site with a pretty decent amount of traffic -- about 1000 QPS. I decided to play around with Node's built in 'cluster' module.

I come from the Ruby world, so I wanted my end product to be something like what you get when using Unicorn. Here's a little walk through.

TL;DR

Here's the gist.

Bare-bones

To start, I wanted to get a simple server.js to start my app.js with eight workers. The number of workers are up to you, but I would base it on the number of CPU's you have, to keep things simple. I haven't done enough profiling and research to be sure, but I believe the number of CPU's should be a hard cap on this.

So to start, I required cluster, configured it to use app.js and created eight forks.

/**
 * Libraries
 **/
var cluster = require('cluster');

/**
 * Setup
 **/
var workers = 8;

cluster.setupMaster({
    exec : "app.js",
});

/**
 * Fork Workers
 **/
for (var i = 0; i < workers; i++) {
    cluster.fork();
}

Hello? Say something!

The above works, but doesn't have any of the pretty logging that Unicorn does. Being a DevOps guy, I want to know what my server is doing.

So next, I added some simple messaging to console.log.

/**
 * Libraries
 **/
var cluster = require('cluster');

/**
 * Setup
 **/
var workers = parseInt(process.env.CLUSTER_WORKERS, 10);

cluster.setupMaster({
    exec : "app.js",
});

/**
 * Startup Messaging
 **/
console.log("Master starting:");

/**
 * Fork Workers
 **/
for (var i = 0; i < workers; i++) {
        console.log("Starting worker: " + i)
    cluster.fork();
}

Talk to me, baby!

Better, but still, I want more. I want to see what's going on when my server starts, stop and forks. I want to know when it started as well.

So next, I added the dateformat npm, built a function to wrap my messaging (never know when I'm going to want to change where it goes) and added a few event handlers to alert me about what's going on.

/**
 * Libraries
 **/
var cluster = require('cluster');
var datefmt = require('dateformat');

/**
 * Setup
 **/
var workers = 8;

cluster.setupMaster({
    exec : "app.js",
});

/**
 * Utilities
 **/
function say(message) {
    console.log("[SERVER] " + message);
};

/**
 * Startup Messaging
 **/
say("Master starting:");
say("time        => " + datefmt(new Date(), "ddd, dd mmm yyyy hh:MM:ss Z"));
say("pid         => " + process.pid);
say("environment => " + process.env.NODE_ENV);

/**
 * Fork Workers
 **/
say("Workers starting:");

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

/**
 * Worker Event Handlers
 **/
cluster.on('exit', function(worker, code, signal) {
    say('worker      => with pid: ' + worker.process.pid + ', died. Restarting...');
});

cluster.on('online', function(worker) {
    say('worker      => start with pid: ' + worker.process.pid + '.');
});

The polish, don't forget the polish...

That's works really nicely. I get good startup messaging and the server tells me when my forks are running and when they exit.

What's missing? Well, for the final touches, I wanted to make the number of workers configurable for testing on smaller development VMs and I decided to restart my forks, should they fail.

/**
 * Libraries
 **/
var cluster = require('cluster');
var datefmt = require('dateformat');

/**
 * Setup
 **/
var workers = parseInt(process.env.CLUSTER_WORKERS, 8);

cluster.setupMaster({
    exec : "app.js",
});

/**
 * Utilities
 **/
function say(message) {
    console.log("[SERVER] " + message);
};

/**
 * Startup Messaging
 **/
say("Master starting:");
say("time        => " + datefmt(new Date(), "ddd, dd mmm yyyy hh:MM:ss Z"));
say("pid         => " + process.pid);
say("environment => " + process.env.NODE_ENV);

/**
 * Fork Workers
 **/
say("Workers starting:");

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

/**
 * Worker Event Handlers
 **/
cluster.on('exit', function(worker, code, signal) {
    say('worker      => with pid: ' + worker.process.pid + ', died. Restarting...');
    cluster.fork();
});

cluster.on('online', function(worker) {
    say('worker      => start with pid: ' + worker.process.pid + '.');
});

Startup?

For startup, you can simply run node server.js or nohup node server.js, but I recommend using the forever npm in production situations.

Here's my forever startup command, which I've put in to a Makefile (get the gist) as a start task.

forever -p ./logs  \
            -l server_out.log \
            -o ./logs/server_out.log \
            -e ./logs/server_err.log \
            --append \
            --plain \
            --minUptime 1000 \
            --spinSleepTime 1000 \
    start server.js

Published on in Node.js