Comment mettre à l'échelle votre serveur Node.js à l'aide du clustering

L'évolutivité est un sujet brûlant dans la technologie, et chaque langage de programmation ou framework fournit sa propre façon de gérer des charges élevées de trafic.

Aujourd'hui, nous allons voir un exemple simple et direct sur le clustering Node.js. Il s'agit d'une technique de programmation qui vous aidera à paralléliser votre code et à accélérer les performances.

«Une seule instance de Node.js s'exécute dans un seul thread. Pour tirer parti des systèmes multicœurs, l'utilisateur voudra parfois lancer un cluster de processus Node.js pour gérer la charge. »

- Documentation Node.js

Nous allons créer un serveur Web simple en utilisant Koa, qui est vraiment similaire à Express en termes d'utilisation.

L'exemple complet est disponible dans ce référentiel Github.

Ce que nous allons construire

Nous allons créer un serveur Web simple qui agira comme suit:

  1. Notre serveur recevra une POSTdemande, nous ferons semblant que l'utilisateur nous envoie une photo.
  2. Nous allons copier une image du système de fichiers dans un répertoire temporaire.
  3. Nous allons le retourner verticalement à l'aide de Jimp, une bibliothèque de traitement d'image pour Node.js.
  4. Nous l'enregistrerons dans le système de fichiers.
  5. Nous le supprimerons et nous enverrons une réponse à l'utilisateur.

Bien sûr, ce n'est pas une application du monde réel, mais en est assez proche. Nous voulons simplement mesurer les avantages de l'utilisation du clustering.

Mise en place du projet

Je vais utiliser yarnpour installer mes dépendances et initialiser mon projet:

Puisque Node.js est monothread, si notre serveur Web tombe en panne, il restera en panne jusqu'à ce qu'un autre processus le redémarre. Nous allons donc installer pour toujours, un simple démon qui redémarrera notre serveur Web en cas de panne.

Nous installerons également Jimp, Koa et Koa Router.

Premiers pas avec Koa

Voici la structure de dossiers que nous devons créer:

Nous aurons un srcdossier qui contient deux fichiers JavaScript: cluster.jset standard.js.

Le premier sera le fichier dans lequel nous expérimenterons le clustermodule. Le second est un simple serveur Koa qui fonctionnera sans aucun clustering.

Dans le modulerépertoire, nous allons créer deux fichiers: job.jset log.js.

job.jseffectuera le travail de manipulation d'image. log.jsenregistrera chaque événement qui se produit pendant ce processus.

Le module Log

Le module Log sera une fonction simple qui prendra un argument et l'écrira dans le stdout(similaire à console.log).

Il ajoutera également l'horodatage actuel au début du journal. Cela nous permettra de vérifier quand un processus a démarré et de mesurer ses performances.

Le module Job

Je vais être honnête, ce n'est pas un script magnifique et super optimisé. C'est juste un travail facile qui nous permettra de stresser notre machine.

Le serveur Web Koa

Nous allons créer un serveur Web très simple. Il répondra sur deux routes avec deux méthodes HTTP différentes.

Nous pourrons effectuer une requête GET sur //localhost:3000/. Koa répondra avec un texte simple qui nous montrera le PID actuel (identifiant de processus).

La deuxième route n'acceptera que les requêtes POST sur le /flipchemin et exécutera le travail que nous venons de créer.

Nous allons également créer un middleware simple qui définira un en- X-Response-Timetête. Cela nous permettra de mesurer la performance.

Génial! Nous pouvons maintenant commencer à taper notre serveur node ./src/standard.jset tester nos routes.

Le problème

Utilisons ma machine comme serveur:

  • Macbook Pro 15 pouces 2016
  • Intel Core i7 à 2,7 GHz
  • 16 Go de RAM

Si je fais une requête POST, le script ci-dessus m'enverra une réponse dans ~ 3800 millisecondes. Pas si mal, étant donné que l'image sur laquelle je travaille actuellement fait environ 6,7 Mo.

Je peux essayer de faire plus de demandes, mais le temps de réponse ne diminuera pas trop. En effet, les demandes seront exécutées de manière séquentielle.

Alors, que se passerait-il si j'essayais de faire 10, 100, 1000 demandes simultanées?

J'ai créé un simple script Elixir qui effectue plusieurs requêtes HTTP simultanées:

J'ai choisi Elixir car il est vraiment facile de créer des processus parallèles, mais vous pouvez utiliser ce que vous préférez!

Test de dix requêtes simultanées - sans clustering

Comme vous pouvez le voir, nous engendrons 10 processus simultanés à partir de notre iex (un Elixir REPL).

Le serveur Node.js copiera immédiatement notre image et commencera à la retourner.

La première réponse sera enregistrée après 16 secondes et la dernière après 40 secondes.

Une telle baisse de performance dramatique! Avec seulement 10 demandes simultanées,nous avons diminué les performances du serveur Web de 950%!

Présentation du clustering

Vous vous souvenez de ce que j'ai mentionné au début de l'article?

Pour tirer parti des systèmes multicœurs, l'utilisateur voudra parfois lancer un cluster de processus Node.js pour gérer la charge.

Selon le serveur sur lequel nous allons exécuter notre application Koa, nous pourrions avoir un nombre différent de cœurs.

Chaque noyau sera responsable de la gestion de la charge individuellement. Fondamentalement, chaque requête HTTP sera satisfaite par un seul cœur.

Ainsi, par exemple, ma machine, qui a huit cœurs, traitera huit requêtes simultanées.

Nous pouvons maintenant compter le nombre de CPU dont nous disposons grâce au osmodule:

La cpus()méthode retournera un tableau d'objets décrivant nos processeurs. Nous pouvons lier sa longueur à une constante qui sera appelée numWorkers, car c'est le nombre de travailleurs que nous allons utiliser.

Nous sommes maintenant prêts à exiger le clustermodule.

Nous avons maintenant besoin d'un moyen de diviser notre processus principal en processus Ndistincts.

Nous appellerons notre processus principal masteret les autres processus workers.

Le clustermodule Node.js propose une méthode appelée isMaster. Il renverra une valeur booléenne qui nous dira si le processus actuel est dirigé par un worker ou un maître:

Génial. La règle d'or ici est que nous ne voulons pas servir notre application Koa dans le cadre du processus maître.

Nous voulons créer une application Koa pour chaque worker, donc quand une demande arrive, le premier worker gratuit s'en charge.

La cluster.fork()méthode correspondra à notre objectif:

Ok, au début, cela peut être un peu délicat.

Comme vous pouvez le voir dans le script ci-dessus, si notre script a été exécuté par le processus maître, nous allons déclarer une constante appelée workers. Cela créera un worker pour chaque cœur de notre CPU et stockera toutes les informations les concernant.

Si vous n'êtes pas sûr de la syntaxe adoptée, l'utilisation […Array(x)].map()est identique à:

Je préfère simplement utiliser des valeurs immuables lors du développement d'une application à haute concurrence.

Ajout de Koa

Comme nous l'avons déjà dit, nous ne voulons pas servir notre application Koa dans le cadre du processus maître.

Copions la structure de notre application Koa dans l' elseinstruction, nous serons donc sûrs qu'elle sera servie par un worker:

Comme vous pouvez le voir, nous avons également ajouté quelques écouteurs d'événements dans la isMasterdéclaration:

Le premier nous dira qu'un nouveau travailleur a été engendré. Le second créera un nouveau travailleur lorsqu'un autre travailleur se bloque.

De cette façon, le processus maître sera uniquement responsable de la création de nouveaux travailleurs et de leur orchestration. Chaque ouvrier servira une instance de Koa qui sera accessible sur le :3000port.

Test de dix requêtes simultanées - avec clustering

Comme vous pouvez le voir, nous avons eu notre première réponse après environ 10 secondes, et la dernière après environ 14 secondes. C'est une amélioration incroyable par rapport au temps de réponse précédent de 40 secondes!

Nous avons fait dix requêtes simultanées et le serveur Koa en a pris huit immédiatement. Lorsque le premier travailleur a envoyé sa réponse au client, il a pris l'une des demandes restantes et l'a traitée!

Conclusion

Node.

En fait, les serveurs Web Node.js ne peuvent traiter des milliers de requêtes simultanées que si vous envoyez immédiatement une réponse au client.

Une meilleure pratique serait d'ajouter une interface de messagerie pub / sous en utilisant Redis ou tout autre outil étonnant. Lorsque le client envoie une requête, le serveur démarre une communication en temps réel avec d'autres services. Cela prend en charge des travaux coûteux.

Les équilibreurs de charge aideraient également beaucoup à répartir les charges de trafic élevées.

Une fois de plus, la technologie nous offre des possibilités infinies et nous sommes sûrs de trouver la bonne solution pour faire évoluer notre application à l'infini et au-delà!