Tout ce que vous devez savoir sur Node.js

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:


FonctionnementCycles CPU
Registres CPU3 mesures
Cache L18 mesures
Cache L212 mesures
RAM150 mesures
Disque30 000 000 mesures
Réseau250 000 000 mesures

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:


 // Importing native http module const http = require('http'); // Creating a server instance where every call // the message 'Hello World' is responded to the client const server = http.createServer(function(request, response) { response.write('Hello World'); response.end(); }); // Listening port 8080 server.listen(8080); 

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. .

Source: https://habr.com/ru/post/fr460661/


All Articles