Pour comprendre la mise à l'échelle des applications Node.js, nous devons d'abord comprendre quel problème la mise à l'échelle résout.
Vous savez que Node.js est monotâche, il a son propre thread principal. Ainsi, au lieu de servir chaque requête entrante au serveur sur un thread séparé, il sert chaque requête à travers le thread principal. Je vais expliquer cela plus en détail car cela peut être un peu déroutant.
Ainsi, si la requête bloque le thread principal, cela peut affecter les autres requêtes entrantes. Supposons que vous ayez une simple route /user-profile et qu'il faille 1 seconde pour compléter la requête. Si 3 requêtes /user-profile sont faites en même temps, voici ce qui se passera :
- l'une des requêtes sera traitée en 1 seconde,
- une autre en 2 secondes (parce qu'elle doit attendre 1 seconde)
- et la troisième en 3 secondes (en attendant 2 secondes que les 2 requêtes précédentes se terminent).
Cependant, Node.js peut décharger de nombreuses tâches sur le noyau du système d'exploitation. Le noyau du système d'exploitation est multi-thread. Voici quelques exemples de ces tâches :
- 1fs.readFile()
- dns.lookup()
- zlib.gzip()
- crypto.pbkdf2()
Les deux premières tâches sont très gourmandes en entrées/sorties, et les deux dernières sont gourmandes en ressources processeur.
Ainsi, lorsque le système d'exploitation exécute des tâches, le thread principal de Node.js peut basculer vers d'autres requêtes. Le système d'exploitation permet à Node.js d'être raisonnablement rapide grâce à son modèle de thread principal unique.
Le problème survient lorsque Node.js a trop de tâches qui ne peuvent pas être déléguées au noyau du système d'exploitation. Ces tâches bloquent alors le thread principal de Node.js, car les demandes commencent à s'accumuler dans la file d'attente.
Il peut en résulter un scénario peu glorieux comme celui-ci :
- une requête arrivant peut devoir attendre qu'un serveur Node.js occupé prenne en charge et traite la requête
- plus le serveur est bloqué, plus le temps d'attente est long
- plus le nombre de demandes est élevé, plus le temps d'attente est long
Étant donné que Node.js traite toutes les demandes par l'intermédiaire d'un seul thread principal, il n'est pas possible de traiter des demandes supplémentaires en ajoutant simplement des unités centrales. Un seul thread ne peut pas fonctionner sur plus d'un cœur de CPU à la fois.
Voici à quoi ressemble ce scénario :
Les demandes sont en attente et le processus Node.js, très occupé, essaie d'accomplir autant de tâches qu'il le peut. Nous avons là un processeur qui est en feu alors que les autres sont au repos.
C'est le problème de la mise à l'échelle. Lorsque l'ajout de matériel ne résout pas les problèmes de performance.
Solution 1 - Worker threads
Nous pouvons exploiter les Worker threads pour exécuter du code JavaScript plus intensif en termes de CPU et libérer le thread principal de Node.js.
Voici à quoi cela ressemble :
Chaque demande est toujours traitée par le thread principal de Node.js, mais le code JavaScript intensif est exécuté dans des Worker threads. Ces Worker threads rendent compte de l'exécution à leur patron, le thread principal de Node.js.
La partie délicate des Worker threads et de leur utilisation efficace est d'identifier et d'isoler les parties intensives du code. Cela peut s'avérer difficile en fonction de la complexité de la logique métier de votre application. Le module Node.js worker_threads doit exécuter ces parties intensives du code.
Voici à quoi cela ressemble avec des Worker threads :
Lorsque le code JavaScript intensif s'exécute sur des workers, le processus Node.js peut utiliser des cœurs de processeur supplémentaires. De plus, le thread principal est libéré pour répondre à davantage de demandes.
Cette méthode est connue sous le nom de mise à l'échelle verticale. La mise à l'échelle verticale consiste à ajouter des ressources supplémentaires au serveur.
Solution 2 - plusieurs instances Node.js
Nous pouvons exécuter plusieurs instances de notre processus de serveur Node.js pour répondre aux demandes. L'équilibreur de charge peut répartir ces demandes entrantes entre les processus Node.js :
Cette approche peut s'avérer plus pratique car elle ne nécessite pas d'étapes telles que l'identification du code JavaScript à forte intensité de CPU, la communication des threads, etc. Avec cette approche, nous utilisons tous les cœurs de processeurs disponibles.
Une autre variante de cette approche consiste à utiliser différentes machines/ordinateurs pour exécuter les processus Node.js :
Dans cette variante, l'équilibreur de charge répartit les demandes entrantes entre les processus Node.js exécutés sur différentes machines. Si la charge augmente, d'autres machines peuvent être ajoutées pour accroître la capacité.
Cette méthode est connue sous le nom de mise à l'échelle horizontale. La mise à l'échelle horizontale consiste à augmenter la capacité en ajoutant des nœuds/serveurs à la configuration.
Cette configuration de mise à l'échelle horizontale peut être construite de manière rentable dans le cloud (Amazon Web Services, Google Cloud Platform, Microsoft Azure, etc.)
Dans le cloud, un serveur est une machine virtuelle qui peut être rapidement ajoutée ou supprimée à l'aide d'une simple commande. Nous pouvons mettre en place une configuration rentable en déployant une machine virtuelle de faible capacité qui peut être ajoutée/supprimée en fonction de la charge des demandes.
Une chose importante à retenir lors de la mise à l'échelle des applications Node.js de cette manière est de respecter une règle : Le processus Node.js qui sert une requête ne doit rien stocker dans sa mémoire qui soit spécifique à une session.
Si cette règle n'est pas respectée, les requêtes consécutives de cette session seront traitées par la même instance de serveur Node.js. Ainsi, même si cette instance est très occupée et que d'autres sont libres, elle doit toujours servir cette requête spécifique.
Pour éviter cela, les processus Node doivent stocker les données spécifiques à la session dans le magasin accessible à tous les autres processus Node de la configuration :
De cette manière, toute instance de serveur Node peut répondre à des demandes consécutives provenant de n'importe quelle session en recherchant ces données de session dans le magasin commun. Une base de données comme Postgres ou MySQL ou un magasin en mémoire comme Memcached ou Redis peuvent stocker des données spécifiques à une session.
Gestion d'une configuration mise à l'échelle
Lorsque vous avez une application Node.js mise à l'échelle, vous avez besoin d'outils de gestion pour cette configuration. D'après mon expérience, ces 3 outils sont les plus populaires et les plus éprouvés :
- Nginx avec systemd
- pm2
- Docker
Il n'y a pas de solution miracle. Chacun de ces outils peut être utilisé en fonction de votre configuration.
Nginx avec systemd est idéal lorsque l'automatisation DevOps et une configuration horizontalement extensible ne sont pas nécessaires. Nginx peut jouer le rôle d'équilibreur de charge et distribuer les requêtes entrantes. Systemd peut gérer les processus Node.js (par exemple, redémarrer en cas de crash) et s'occuper des logs avec Syslog.
Le pm2 est idéal lorsque vous recherchez une solution basée sur Node et que vous n'avez pas besoin d'automatisation DevOps et d'une configuration horizontalement évolutive. Il peut faire de l'équilibrage de charge avec son module cluster. Il peut également démarrer, arrêter et redémarrer les processus Node tout en gérant les journaux.
Docker est le mieux adapté lorsque vous avez besoin d'une automatisation DevOps et d'une configuration horizontalement évolutive. Il fonctionne également très bien en combinaison avec des services cloud. Docker peut être utilisé pour gérer les processus Node.js s'exécutant dans les conteneurs Docker. Les services cloud peuvent permettre d'ajouter/supprimer des machines virtuelles à l'aide d'une simple commande. L'automatisation DevOps peut démarrer/arrêter automatiquement les machines virtuelles en fonction de la charge des demandes.
Chaque machine virtuelle exécute un conteneur Docker dans des instances Node.js en direct. Dans ce cas, l'équilibreur de charge peut être un service de gestion basé sur le cloud, comme AWS Application Load Balancer.
La gestion des tâches et des journaux de Node se fait via les services de conteneurs Docker. L'automatisation DevOps crée des machines virtuelles à partir d'images Docker et met à jour l'équilibreur de charge avec les changements.
Verdict
Il n'existe pas de formule magique ou de meilleure recette pour gérer la mise à l'échelle des applications Node.js.
Si vous êtes à court d'idées, vous pouvez utiliser certains des outils décrits ci-dessus. Ces outils ont passé le test du temps, mais n'hésitez pas à gérer votre installation selon vos propres règles.
Si vous avez une petite application Node.js, vous n'avez pas besoin de compliquer les choses, vous n'avez pas besoin d'avoir une automatisation complète et des tonnes de conteneurs Docker. En revanche, si vous avez une application complexe avec beaucoup de services, envisagez les machines virtuelles et les conteneurs en combinaison avec d'autres options.
Source : How to scale Node.js applications ? (Mensur Durakovic)
Et vous ?
Quel est votre avis sur le sujet ?
Voir aussi :
Node.js 20.6 est disponible avec une prise en charge des fichiers de configuration .env et l'intégration du garbage collector C++ "Oilpan"
The Art of Node - Une introduction à Node.js