Bonjour, Habr! Je vous présente la traduction de l'article "Tout ce que vous devez savoir sur Node.js" de Jorge Ramón.

De nos jours, la plate-forme Node.js est l'une des plates-formes les plus populaires pour créer des API REST efficaces et évolutives. Il convient également pour la création d'applications mobiles hybrides, de programmes de bureau et même pour l'IoT.
Je travaille avec la plateforme Node.js depuis plus de 6 ans et j'adore ça. Ce message essaie principalement d'être un guide sur le fonctionnement réel de Node.js.
Commençons !!
Ce qui sera discuté:
Monde avant Node.js
Serveur multithread
Les applications Web écrites suivant l'architecture client / serveur fonctionnent comme suit - le client demande la ressource nécessaire au serveur et le serveur envoie la ressource en réponse. Dans ce schéma, le serveur répond à la demande et met fin à la connexion.
Ce modèle est efficace car chaque requête adressée au serveur consomme des ressources (mémoire, temps processeur, etc.). Afin de traiter chaque demande ultérieure du client, le serveur doit terminer le traitement de la précédente.
Est-ce à dire que le serveur ne peut traiter qu'une seule demande à la fois? Pas vraiment! Lorsque le serveur reçoit une nouvelle demande, il crée un thread distinct pour le traiter.
Le flux , en termes simples, est le temps et les ressources que le CPU alloue pour exécuter un petit bloc d'instructions. Cela dit, le serveur peut traiter plusieurs demandes à la fois, mais une seule par thread. Un tel modèle est également appelé modèle de thread par demande .

Pour traiter N requêtes, le serveur a besoin de N threads. Si le serveur reçoit N + 1 requêtes, il doit attendre jusqu'à ce qu'un des threads soit disponible.
Dans la figure ci-dessus, le serveur peut traiter jusqu'à 4 requêtes (threads) à la fois et lorsqu'il reçoit les 3 requêtes suivantes, ces requêtes doivent attendre jusqu'à ce que l'un de ces 4 threads devienne disponible.
Une façon de se débarrasser des restrictions est d'ajouter plus de ressources (mémoire, cœurs de processeur, etc.) au serveur, mais ce n'est pas la meilleure solution ....
Et, bien sûr, n'oubliez pas les limites technologiques.
Blocage entrée / sortie
Le nombre limité de threads sur le serveur n'est pas le seul problème. Vous vous demandez peut-être pourquoi un seul thread ne peut pas traiter plusieurs demandes en même temps? tout cela en raison du blocage des opérations d'E / S.
Supposons que vous développez une boutique en ligne et que vous ayez besoin d'une page où l'utilisateur peut afficher une liste de tous les produits.
L'utilisateur frappe sur http://yourstore.com/products et le serveur rend un fichier HTML avec tous les produits de la base de données en réponse. Pas du tout compliqué, non?
Mais que se passe-t-il en coulisses?
- Lorsqu'un utilisateur frappe sur
/products
méthode ou une fonction particulière doit être exécutée afin de traiter la demande. Un petit morceau de code (le vôtre ou votre framework) analyse l'URL de la requête et recherche une méthode ou une fonction appropriée. Le flux est en cours d'exécution . 
- Maintenant, la méthode ou la fonction souhaitée est exécutée, comme dans le premier paragraphe, le thread fonctionne.

- Puisque vous êtes un bon développeur, vous enregistrez tous les journaux système dans un fichier, et bien sûr, pour être sûr que le routeur exécute la méthode / fonction souhaitée - vous enregistrez également la ligne "Méthode X en cours d'exécution !!". Mais tout cela bloque les opérations le flux d' entrée / sortie est en attente .

- Tous les journaux sont enregistrés et les lignes de fonction suivantes sont exécutées. Le fil fonctionne à nouveau .

- Il est temps d'accéder à la base de données et d'obtenir tous les produits - une simple requête comme les
SELECT * FROM products
fait son travail, mais devinez quoi? Oui, il s'agit d'une opération d'E / S bloquante. Le flux attend . 
- Vous avez reçu un tableau ou une liste de tous les produits, mais assurez-vous d'avoir promis tout cela. Le flux attend .

- Vous avez maintenant tous les produits et il est temps de rendre le modèle pour la future page, mais avant cela, vous devez les lire. Le flux attend .

- Le moteur de rendu fait son travail et envoie une réponse au client. Le fil fonctionne à nouveau .

- Le flux est libre, comme un oiseau dans le ciel.

Les opérations d'E / S sont-elles lentes? Eh bien, cela dépend du spécifique. Regardons le tableau:
Les opérations de lecture du réseau et du disque sont trop lentes. Imaginez le nombre de demandes ou d'appels vers des API externes que votre système pourrait gérer pendant cette période.
Pour résumer: les opérations d'E / S font attendre le thread et gaspillent les ressources.
Problème C10K
Le problème
C10k (eng. C10k; connexions 10k - problème de 10 000 connexions)
Au début des années 2000, les machines serveur et client étaient lentes. Le problème est survenu lors du traitement de 10 000 connexions client à la même machine en parallèle.
Mais pourquoi le modèle traditionnel de thread par demande (thread sur demande) ne pouvait-il pas résoudre ce problème? Eh bien, utilisons un peu de mathématiques.
L'implémentation native des threads alloue plus de 1 Mo de mémoire par flux, ce qui laisse - pour 10 mille threads, 10 Go de RAM sont requis et cela uniquement pour la pile de flux. Oui, et n'oubliez pas, nous sommes au début des années 2000 !!
Aujourd'hui, les ordinateurs serveurs et clients fonctionnent plus rapidement et plus efficacement et presque tous les langages de programmation ou framework peuvent faire face à ce problème. Mais en fait, le problème n'est pas réglé. Pour 10 millions de connexions client à une machine, le problème revient à nouveau (mais maintenant c'est le problème C10M ).
Sauvetage JavaScript?
Attention Spoilers
!!!
Node.js résout réellement le problème C10K ... mais comment?!
Le JavaScript côté serveur n'était pas quelque chose de nouveau et d'inhabituel au début des années 2000, à cette époque, il y avait déjà des implémentations au-dessus de la JVM (machine virtuelle java) - RingoJS et AppEngineJS, qui travaillaient sur le modèle de thread par demande.
Mais s'ils ne pouvaient pas résoudre le problème, comment Node.js?! Tout cela parce que JavaScript est monothread .
Node.js et la boucle d'événements
Node.js
Node.js est une plate-forme serveur qui fonctionne sur le moteur Google Chrome - V8, qui peut compiler du code JavaScript en code machine.
Node.js utilise un modèle piloté par les événements et une architecture d' E / S non bloquante , ce qui le rend léger et efficace. Ce n'est pas un framework, ni une bibliothèque, c'est un runtime JavaScript.
Écrivons un petit exemple:
E / S non bloquantes
Node.js utilise des opérations d'entrée / sortie non bloquantes, qu'est-ce que cela signifie:
- Le thread principal ne sera pas bloqué par les opérations d'E / S.
- Le serveur continuera de traiter les demandes.
- Nous devrons travailler avec du code asynchrone .
Écrivons un exemple dans lequel le serveur envoie une page HTML en réponse à une demande à /home
et pour toutes les autres demandes - «Hello World». Pour envoyer une page HTML, vous devez d'abord la lire à partir d'un fichier.
home.html
<html> <body> <h1>This is home page</h1> </body> </html>
index.js
const http = require('http'); const fs = require('fs'); const server = http.createServer(function(request, response) { if (request.url === '/home') { fs.readFile(`${ __dirname }/home.html`, function (err, content) { if (!err) { response.setHeader('Content-Type', 'text/html'); response.write(content); } else { response.statusCode = 500; response.write('An error has ocurred'); } response.end(); }); } else { response.write('Hello World'); response.end(); } }); server.listen(8080);
Si l'URL demandée est /home
, le module natif fs
est utilisé pour lire le fichier home.html
.
Les fonctions qui entrent dans http.createServer
et fs.readFile
comme arguments sont des rappels . Ces fonctions seront exécutées à un moment donné dans le futur (la première dès que le serveur reçoit la demande, et la seconde lorsque le fichier est lu sur le disque et placé dans le tampon).
Pendant que le fichier est lu depuis le disque, Node.js peut traiter d'autres requĂŞtes et mĂŞme relire le fichier et tout cela en un seul flux ... mais comment?!
Boucle d'événement
La boucle d'événements est la magie qui se produit à l'intérieur de Node.js. C'est littéralement une boucle sans fin et en fait un thread.
Libuv est une bibliothèque C qui implémente ce modèle et fait partie du noyau Node.js. Vous pouvez en savoir plus sur libuv ici .
Un cycle d'événements comporte 6 phases, chaque exécution des 6 phases est appelée un tick .
- timers : dans cette phase, les rappels planifiés par les méthodes
setTimeout()
et setInterval()
sont exécutés; - rappels en attente : presque tous les rappels sont exécutés, à l'exception
close
événements de close
, des temporisateurs et de setImmediate()
; - inactif, préparer : utilisé uniquement à des fins internes;
- sondage : responsable de la réception de nouveaux événements d'E / S. Node.js peut bloquer à ce stade;
- check : les rappels provoqués par la méthode
setImmediate()
sont exécutés à ce stade; - fermer les rappels : par exemple
socket.on('close', ...)
;
Eh bien, il n'y a qu'un seul thread, et ce thread est une boucle d'événements, mais alors qui effectue toutes les E / S?
Faites attention
!!!
Lorsqu'une boucle d'événement doit effectuer une opération d'E / S, elle utilise le thread OS du pool de threads et lorsque la tâche est terminée, le rappel est mis en file d'attente pendant la phase de rappels en attente .
N'est-ce pas cool?
Le problème des tâches gourmandes en CPU
Node.js semble parfait! Vous pouvez créer ce que vous voulez.
Écrivons une API pour calculer les nombres premiers.
Un nombre premier est un nombre entier (naturel) supérieur à un et divisible par seulement 1 et par lui-même.
Étant donné un nombre N, l'API doit calculer et renvoyer les premiers N premiers de la liste (ou du tableau).
primes.js
function isPrime(n) { for(let i = 2, s = Math.sqrt(n); i <= s; i++) { if(n % i === 0) return false; } return n > 1; } function nthPrime(n) { let counter = n; let iterator = 2; let result = []; while(counter > 0) { isPrime(iterator) && result.push(iterator) && counter--; iterator++; } return result; } module.exports = { isPrime, nthPrime };
index.js
const http = require('http'); const url = require('url'); const primes = require('./primes'); const server = http.createServer(function (request, response) { const { pathname, query } = url.parse(request.url, true); if (pathname === '/primes') { const result = primes.nthPrime(query.n || 0); response.setHeader('Content-Type', 'application/json'); response.write(JSON.stringify(result)); response.end(); } else { response.statusCode = 404; response.write('Not Found'); response.end(); } }); server.listen(8080);
prime.js
est l'implémentation des calculs nécessaires: la fonction isPrime
vérifie si le nombre est premier, et nthPrime renvoie N de tels nombres.
Le fichier index.js
est responsable de la création du serveur et utilise le module prime.js
pour traiter chaque demande de /primes
. Le nombre N est jeté à travers la chaîne de requête dans l'URL.
Pour obtenir les 20 premiers nombres premiers, nous devons faire une demande Ă http://localhost:8080/primes?n=20
.
Supposons que 3 clients nous frappent et tentent d'accéder à notre API d'E / S non bloquante:
- La première interroge 5 nombres premiers toutes les secondes.
- Le second demande 1000 nombres premiers chaque seconde
- Le troisième demande 10 000 000 000 de nombres premiers, mais ...
Lorsque le troisième client envoie une demande, le thread principal est bloqué et c'est le principal symptôme du problème des tâches gourmandes en CPU . Lorsque le thread principal est occupé à effectuer une tâche «lourde», il devient inaccessible aux autres tâches.
Mais qu'en est-il de libuv? Si vous vous souvenez, cette bibliothèque aide Node.js à effectuer des opérations d'entrée / sortie à l'aide de threads du système d'exploitation en évitant de bloquer le thread principal et vous avez absolument raison, c'est la solution à notre problème, mais pour que cela soit possible, notre module doit être écrit dans la langue C ++ pour que libuv puisse fonctionner avec.
Heureusement, à partir de la version 10.5, le module natif Worker Threads a été ajouté à Node.js.
Les travailleurs et leurs flux
Comme nous l'indique la documentation :
Les travailleurs sont utiles pour effectuer des opérations JavaScript gourmandes en CPU; ne les utilisez pas pour les opérations d'entrée / sortie, les mécanismes déjà intégrés dans Node.js gèrent plus efficacement ces tâches que le thread de travail.
Correction du code
Il est temps de réécrire notre code:
primes-workerthreads.js
const { workerData, parentPort } = require('worker_threads'); function isPrime(n) { for(let i = 2, s = Math.sqrt(n); i <= s; i++) if(n % i === 0) return false; return n > 1; } function nthPrime(n) { let counter = n; let iterator = 2; let result = []; while(counter > 0) { isPrime(iterator) && result.push(iterator) && counter--; iterator++; } return result; } parentPort.postMessage(nthPrime(workerData.n));
index-workerthreads.js
const http = require('http'); const url = require('url'); const { Worker } = require('worker_threads'); const server = http.createServer(function (request, response) { const { pathname, query } = url.parse(request.url, true); if (pathname === '/primes') { const worker = new Worker('./primes-workerthreads.js', { workerData: { n: query.n || 0 } }); worker.on('error', function () { response.statusCode = 500; response.write('Oops there was an error...'); response.end(); }); let result; worker.on('message', function (message) { result = message; }); worker.on('exit', function () { response.setHeader('Content-Type', 'application/json'); response.write(JSON.stringify(result)); response.end(); }); } else { response.statusCode = 404; response.write('Not Found'); response.end(); } }); server.listen(8080);
Dans le index-workerthreads.js
, chaque demande Ă /primes
crée une instance de la classe Worker
(Ă partir du module natif worker_threads
) pour télécharger et exécuter le primes-workerthreads.js
dans le thread de travail. Lorsque la liste des nombres premiers est calculée et prête, l'événement de message
est déclenché - le résultat tombe dans le flux principal car le travailleur n'a plus de travail, il déclenche également l'événement de exit
, permettant au flux principal d'envoyer des données au client.
primes-workerthreads.js
a un peu changé. Il importe workerData
(il s'agit d'une copie des paramètres transmis depuis le thread principal) et parentPort
via lequel le résultat du travail du travailleur est renvoyé au thread principal.
Essayons maintenant Ă nouveau notre exemple et voyons ce qui se passe:
Le thread principal n'est plus bloqué
!!!!!
Maintenant, tout fonctionne comme il se doit, mais produire des travailleurs sans raison n'est toujours pas une bonne pratique; créer des fils n'est pas un plaisir bon marché. Assurez-vous de créer un pool de threads avant cela.
Conclusion
Node.js est une technologie puissante qui doit être explorée autant que possible.
Ma recommandation personnelle - soyez toujours curieux! Si vous savez comment quelque chose fonctionne de l'intérieur, vous pouvez travailler plus efficacement avec.
C'est tout pour les gars d'aujourd'hui. J'espère que ce message vous a été utile et que vous avez appris quelque chose de nouveau sur Node.js.
Merci d'avoir lu et Ă bientĂ´t dans les prochains articles.
.