Node.js cluster power

In de eerste tutorial rond Node.js heb ik een eenvoudige test gedaan om te kijken wat het snelste was; Node.js, Java of PHP. Uit die resultaten bleek uiteraard dat Java de snelste was, de reden hiervoor is dat Node.js op zichzelf binnen een single core blijft.

In deze tutorial ga ik meer ingaan op de cluster module die ervoor zorgt dat je wel gebruik kan maken van meerdere, of al je cores.

Cluster webserver

In dit eerste voorbeeld ga ik een webserver maken die op alle cores een instantie zal draaien. Ze zullen allemaal op dezelfde port listenen en zullen elkaar afwisselen zodat alle requests over de meerdere cores verdeeld worden.

Het voorbeeld dat ik in deze tutorial ga gebruiken is afkomstig van mijn vorige tutorial, waar ik uitgebreid beschreven heb hoe je een web server kon maken. Net zoals in die tutorial ga ik eerst mijn code hier plaatsen en daarna stap voor stap alles uitleggen.

var cluster = require("cluster");
var os = require("os");
var http = require("http");
var url = require("url");
var fs = require("fs");
var getMime = require("simple-mime")("text/html");

if (cluster.isMaster) {
    for (var i = 0;i < os.cpus().length;i++) {
        cluster.fork();
    }

    cluster.on('fork', function(worker) {
        console.log('worker ' + worker.process.pid + ' created');
    });
    cluster.on('exit', function(worker, code, signal) {
        console.log('worker ' + worker.process.pid + ' died');
    });
} else {
    http.createServer(
        function (request, response) {
            console.log("Request served through " + cluster.worker.process.pid);
            var _PATH = url.parse(request.url, true).pathname;
            _PATH = "web" + (_PATH.charAt(_PATH.length - 1) == "/" ? "/index.html" : _PATH);

            request.on("end", function() {
                fs.exists(_PATH, function(isExisting) {
                var _DATA = "";
                if (isExisting) {
                    fs.readFile(_PATH, function(err, data) {
                        if (err) {
                            response.writeHead(500);
                            response.end();
                        } else {
                            response.writeHead(200, {
                                "Content-Type" : getMime(_PATH)
                            }); 
                            response.end(data, 'utf-8');
                        }
                    });
                } else {
                    response.writeHead(404);
                    response.end();
                }
            });
        });
    }).listen(8090, function() {
        console.log("Server on " + cluster.worker.process.pid + " is listening");
    });
}

Zoals je kan zien in de module imports is er niet veel speciaals te merken, behalve dat we nu gebruik maken van de module

cluster

(uiteraard) en de module

os

.
De JavaScript code is voor zowel master als slave (worker) hetzelfde, om dus een onderscheid te kunnen maken hebben we een boolean

cluster.isMaster

die

true

geeft indien het huidige proces de master is.

De code in de

if

branch is nieuwe code, maar de code in de

else

branch is dezelfde code als vorige Node.js tutorial, behalve dat we hier geen logging meer hebben.

De master zal verantwoordelijk zijn voor het aanmaken van worker-threads, deze kunnen we aanmaken door te forken dmv de

cluster.fork()

functie. Om een goede balans te behouden heb ik een worker thread gemaakt voor elke core, het aantal (beschikbare) cores kan je bekomen dmv de os-module die via

os.cpus()

een array van alle cores geeft, de lengte daarvan is dan uiteraard het aantal cores.

De cluster bevat ook een aantal events, waarvan we er twee gebruiken, namelijk

'fork'

en

'exit'

die logischerwijs het event zijn indien een worker thread aangemaakt wordt of als er één verdwijnt (wat normaal niet voorkomt).
Dmv van de

cluster.on()

functie kunnen we een event handler definiëren wat in dit geval niet meer is dan een simple log wegschrijven naar de console. De event handler zorgt ervoor dat we een worker object meekrijgen, via

worker.process.pid

kunnen we dan het process ID van de worker opvragen.

In de worker threads gebeurt het échte werk. Hier wordt een webserver aangemaakt dankzij de http module (zie vorige tutorial), het enige wat hier extra is, is dat we via

cluster.worker

het huidige worker-object kunnen ophalen.
Net zoals in de logging ga ik het process ID ophalen dmv

cluster.worker.process.pid

en ga ik het gebruiken om te loggen welke worker een request afgehandeld heeft.

Uitvoeren

Net zoals alle andere Node scripts voer je deze uit dmv het commando

node <bestandsnaam>

Als je dan net zoals in vorige tutorial de website bezoekt, zal je in je log te zien krijgen welke worker de request afgehandeld heeft.

Screenshot from 2013-02-21 21:24:00

Ziezo, je hebt nu je cluster zonder echt al te veel extra te schrijven (uiteindelijk was enkel het forken nodig, de rest was logging).

Cluster rekenkracht

In dit tweede voorbeeld ga ik het calculate-primes script herschrijven uit de eerste tutorial, waaruit bleek dat Java de snelste was. Dit voorbeeld zal iets ingewikkelder zijn omdat we, in tegenstelling tot eerder, nu de workers met de master moeten laten communiceren en omgekeerd.

De code hiervoor is de volgende:

var cluster = require("cluster");
var os = require("os");
var workerIds = [];

var responses = 0;
var counter = 0;
var max = 100000000;
var d = Date.now();

if (cluster.isMaster) {
    for (var i = 0;i < os.cpus().length;i++) {
        cluster.fork();
    }

    cluster.on('fork', function(worker) {
        worker.on('message', function(msg) {
            counter += msg.primes;
            responses++;
            if (responses == os.cpus().length) {
                console.log(counter);
                console.log(Date.now() - d);
            }
        });
        console.log('worker ' + worker.process.pid + ' created');
    });

    cluster.on('online', function(worker) {
        workerIds.push(worker.id);
        if (workerIds.length == os.cpus().length) {
            var counter = 0;
            var size = max / workerIds.length;
            for (var i = 0;i < workerIds.length;i++) {
                cluster.workers[workerIds[i]].send({ 
                    min:  size * i,
                    max: (size * (i + 1)) - 1
                });
            }
        }
    });

    cluster.on('exit', function(worker, code, signal) {
        console.log('worker ' + worker.process.pid + ' died');
    });
} else {
    process.on('message', function(msg) {
        var primeCounter = 0;
        for (var i = msg.min;i <= msg.max;i++) {
            if (isPrime(i)) {
                primeCounter++;
            }
        }
        process.send({ primes: primeCounter });
    });
}

function isPrime(number) {
    if (number == 2) {
        return true;
    }
    var j = Math.sqrt(number);
    var isPrime = true;
    for (var i = 2;i <= j && isPrime;i++) {
        isPrime = (number % i != 0);
    }
    return isPrime;
}

Net zoals in het vorige voorbeeld maken we hier gebruik van de

cluster
  • en de
os

-module. Ook hier gaan we een controleren wie de master is en wie niet dmv

cluster.isMaster

.

De

'fork'

event handler is iets uitgebreider dan in vorige tutorial, we gaan hier namelijk een event handler voor het

'message'

event van de worker maken. Dit event gaan we gebruiken om de response terug te sturen van de “berekeningen” die elke worker maakt.
Elke worker gaat namelijk voor een aantal van de getallen bepalen of het een priemgetal is of niet en het aantal terug naar de master sturen. Indien de master een message gekregen heeft van elke worker thread, dan gaan we de output tonen. Deze controle maken we via de responses variabel die we telkens met één optellen totdat we aan het aantal CPU’s geraken.

Het

'online'

event is het event dat gebruikt wordt indien een worker écht opgestart is in tegenstelling tot het

'fork'

event dat uitgevoerd wordt indien de worker opgestart wordt.
Als alle workers opgestart zijn, dan kan de master thread beginnen met delegeren. In dit geval bestaat het delegeren uit het verdelen van de getallen tussen alle workers door een bericht te sturen naar de workers met de range waarvoor ze moeten uitzoeken of het priemgetallen zijn of niet. Dit bericht versturen doen we door de

send()

functie aan te spreken op de juiste worker, in dit geval

cluster.worker[workerIds[i]].send()

.

In de worker threads is er ook een

'message'

event, dat de meegegeven range ontvangt en dan controleert of het een priemgetal is. Op het einde wordt dan het antwoord terug gestuurd in een bericht, dit doen we via de

process.send()

functie.

Communicatie tussen master en workers

Even samengevat, de two-way communicatie tussen master en workers is vrij eenvoudig op te zetten. In de cluster koppel je het

'message'

event aan de workers, dit is de event-handler die in actie treedt als de workers een bericht versturen. Dit kan op verschillende manieren, of je doet het via de

cluster.workers

array of je doet het via het worker-object dat je op de een of andere manier meekrijgt (uit andere event handlers bijvoorbeeld).
Om in de workers een bericht te ontvangen koppel je het

'message'

event aan het process object.

Een bericht versturen van je master naar je workers doe je via  de

send()

functie die je aan een worker object koppelt. Net zoals het event kan je het worker-object op verschillende terug vinden, via de

cluster.workers

array of via een andere event handler.
Een bericht versturen van je workers naar de master doe je dan weer via de

process.send()

functie.

Let wel op met berichten sturen, het versturen van berichten tussen master en worker kost redelijk veel tijd en indien je niet alles optimaal maakt kan je zelfs vrij snel een memory leak krijgen. Test je code dus regelmatig alvorens in productie te gaan.

De resultaten

Natuurlijk is deze tutorial niets als we niet vergelijken met wat de resultaten nu zijn.

# Node.js (single core) Node.js (cluster) Java
1.000.000 760 ms 286 ms 340 ms
10.000.000 19406 ms 5071 ms 8424 ms
100.000.000 747847 ms 126688 ms 459907 ms

Zoals je kan zien zijn deze resultaten vrij indrukwekkend, vooral voor 100 miljoen resultaten.

Met deze resultaten sluiten we dan ook deze tutorial af.

Download project

Download – node-cluster-example.tar.gz (1,4 MB)

Tagged , , .

g00glen00b

Consultant at Cronos and Tech lead at Aquafin. Usually you can find me trying out new libraries and technologies. Loves both Java and JavaScript.