Clusteriser votre application Node.js

Clusteriser votre application Node.js

Les application Node.js sont par nature mono-threadées, or les serveurs, de nos jours, sont presque* toujours multi-core. Pour exploiter l’ensemble des capacités de ces serveurs, il est nécessaire de pouvoir exploiter tous les cores.Pour cela, il existe principalement 2 techniques ...

Cluster

Les application Node.js sont par nature mono-threadées, or les serveurs, de nos jours, sont presque* toujours multi-core. Pour exploiter l’ensemble des capacités de ces serveurs, il est nécessaire de pouvoir exploiter tous les cores.

Pour cela, il existe principalement 2 techniques:

  • Lancer plusieurs instances d’une application Node.js sur différents avec un reverse proxy pour load balancer les requêtes entrantes
  • Lancer une application Node.js en mode cluster

Dans l’idéal, il faut lancer autant d’instances qu’il y a de cores sur la machine. Cela permet de partager au mieux la puissance de la machine entre les différentes instances sans pour autant dégrader les performances en partager les cores entre plusieurs instances.

Nous allons dans cet article nous intéresser au second cas de figure, c’est à dire le lancement d’application Node.js en mode cluster.

* Le terme “presque” est utilisé ici car de nombreux serveurs cloud d’entrée de gamme restent mono-threadés (VPS et instances EC2 1er prix, …).

Le module cluster

Le module cluster, bien que marqué comme ayant une API expérimental dans la documentation de Node.js, est aujourd’hui largement utilisé.

Son principe est simple, lorsqu’une application est lancée en mode cluster, un premier process est démarré en mode master. Le process master n’a pas pour rôle de traiter les requêtes entrantes à proprement parler, mais plutôt à les dispatcher aux aux process forkés qui eux sont dédiés au traitement des requêtes. Il sont lancés dans le mode worker.

La responsabilité de l’instanciation de forks en mode worker est du ressort du process master, et les règles sont de fork sont laissées à la responsabilité du développeur. Tout au long de la vie du cluster, des événements sont générés aussi bien par le master que par les workers. Il est important de s’y abonner pour être capable de réagir à des changements dans le cluster (Crash d’un worker, par exemple).

Un exemple simple de cluster est proposé par la documentation:

cluster = require("cluster")
http = require("http")
numCPUs = require("os").cpus().length

if cluster.isMaster
  # Fork workers.
  i = 0
  while i < numCPUs
    cluster.fork()
    i++
  cluster.on "exit", (worker, code, signal) ->
    console.log "worker " + worker.process.pid + " died"
else
  # Workers can share any TCP connection
  # In this case its a HTTP server
  http.createServer((req, res) ->
    res.writeHead 200
    res.end "hello world\n"
  ).listen 8000

Bien que les API de clusterisation proposent une API assez simple à gérer, il faut néanmoins s’occuper de plusieurs points de détail, et le risque de mal faire est rapidement arrivé. Il est donc conseillé de s’appuyer sur un module tiers pour traiter ce sujet.

Le module recluster, très intéressant de part sa simplicité et sa maturité, permet de s’affranchir de toute cette complexité de mise en oeuvre.

recluster

Pour l’installer, il suffit de taper la commande suivante:

npm install recluster --save

Pour instancer une application en mode cluster, il suffit d’ajouter le script suivant (cluster.coffee, par exemple) à votre application:

recluster = require("recluster")
path = require("path")

cluster = recluster(path.join(__dirname, "server.js"), { ### Options ### })
cluster.run()

process.on "SIGUSR2", ->
  console.log "Got SIGUSR2, reloading cluster..."
  cluster.reload()

console.log "spawned cluster, kill -s SIGUSR2", process.pid, "to reload"

Le module recluster attend en paramètre le chemin du script. Il suffit ensuite de lancer le fichier cluster.js au lieu du fichier server.js, et le tour est joué ! (Les exemples étant en CoffeeScript, il est nécessaire d’avoir au préalable compilé les fichiers CoffeeScript).

Zero downtime reloading

Le modules recluster permet de mettre à jour une application sans coupure de service, il faut pour cela appeler la fonction reload sur la variable cluster.

Dans l’exemple ci-dessus, le signal SIGUSR2, permet d’indiquer au programme que le cluster doit être rechargé. Celui-ci rechargera les différents workers en se basant sur les options passées en paramètre (timeout de rechargement, etc). Les requêtes en cours de traitement seront honorés sans coupure brutale, dans la limite d’un timeout défini par les options de configuration du module.

Cette fonctionnalité est particulièrement intéressantes lors que l’application doit être mise à jour sans coupure de service. Ainsi, la base de code peut-être mise à jour, puis les workers redémarrés un par un une fois les requêtes en cours traitées.

Il est possible de s’appuyer sur d’autres conditions pour recharger les workers d’un cluster, par exemple, il est possible de s’appuyer sur la modification du fichier package.json pour déclencher un rechargement avec le code suivant:

fs = require 'fs'
fs.watchFile "package.json", (curr, prev) ->
    console.log "Package.json changed, reloading cluster..."
    cluster.reload()

Conclusion

Ne vous laissez pas impressionner par la notion de clusterisation. Elle est très simple à mettre en oeuvre dans le monde Node.js grâce à une API de base faisant parti des core modules, et de nombreux modules s’appuyant dessus.