Alles was Sie über Node.js wissen müssen

Hallo Habr! Ich präsentiere Ihnen die Übersetzung des Artikels "Alles, was Sie über Node.js wissen müssen" von Jorge Ramón.



Heutzutage ist die Node.js-Plattform eine der beliebtesten Plattformen zum Erstellen effizienter und skalierbarer REST-APIs. Es eignet sich auch zum Erstellen hybrider mobiler Anwendungen, Desktop-Programme und sogar für IoT.


Ich arbeite seit über 6 Jahren mit der Node.js-Plattform und ich liebe sie wirklich. Dieser Beitrag versucht hauptsächlich, eine Anleitung zu geben, wie Node.js tatsächlich funktioniert.


Lass uns anfangen !!


Was wird diskutiert:




Welt vor Node.js.


Multithread-Server


Webanwendungen, die gemäß der Client / Server-Architektur geschrieben wurden, funktionieren wie folgt: Der Client fordert die erforderliche Ressource vom Server an und der Server sendet die Ressource als Antwort. In diesem Schema antwortet der Server auf die Anforderung und beendet die Verbindung.


Dieses Modell ist effektiv, da jede Anforderung an den Server Ressourcen (Speicher, Prozessorzeit usw.) verbraucht. Um jede nachfolgende Anforderung vom Client zu verarbeiten, muss der Server die Verarbeitung der vorherigen Anforderung abschließen.


Bedeutet dies, dass der Server jeweils nur eine Anforderung verarbeiten kann? Nicht wirklich! Wenn der Server eine neue Anforderung empfängt, erstellt er einen separaten Thread für die Verarbeitung.


Der Fluss ist in einfachen Worten die Zeit und die Ressourcen, die die CPU für die Ausführung eines kleinen Befehlsblocks bereitstellt. Trotzdem kann der Server mehrere Anforderungen gleichzeitig verarbeiten, jedoch nur eine pro Thread. Ein solches Modell wird auch als Thread-per-Request-Modell bezeichnet .



Um N Anforderungen zu verarbeiten, benötigt der Server N Threads. Wenn der Server N + 1-Anforderungen empfängt, muss er warten, bis einer der Threads verfügbar wird.


In der obigen Abbildung kann der Server bis zu 4 Anforderungen (Threads) gleichzeitig verarbeiten. Wenn er die nächsten 3 Anforderungen empfängt, müssen diese Anforderungen warten, bis einer dieser 4 Threads verfügbar wird.


Eine Möglichkeit, die Einschränkungen zu beseitigen, besteht darin, dem Server weitere Ressourcen (Speicher, Prozessorkerne usw.) hinzuzufügen. Dies ist jedoch nicht die beste Lösung.



Und vergessen Sie natürlich nicht die technologischen Einschränkungen.


Ein- / Ausgabe blockieren


Die begrenzte Anzahl von Threads auf dem Server ist nicht das einzige Problem. Vielleicht haben Sie sich gefragt, warum ein einzelner Thread nicht mehrere Anforderungen gleichzeitig verarbeiten kann? Alles aufgrund des Blockierens von E / A-Vorgängen .



Angenommen, Sie entwickeln einen Online-Shop und benötigen eine Seite, auf der der Benutzer eine Liste aller Produkte anzeigen kann.


Der Benutzer klopft an http://yourstore.com/products und der Server rendert als Antwort eine HTML-Datei mit allen Produkten aus der Datenbank. Gar nicht kompliziert, oder?


Aber was passiert hinter den Kulissen?


  • Wenn ein Benutzer an /products klopft /products bestimmte Methode oder Funktion ausgeführt werden, um die Anforderung zu verarbeiten. Ein kleiner Code (Ihr oder Ihr Framework) analysiert die Anforderungs-URL und sucht nach einer geeigneten Methode oder Funktion. Der Stream läuft .
  • Nun wird die gewünschte Methode oder Funktion ausgeführt, wie im ersten Absatz funktioniert der Thread.
  • Da Sie ein guter Entwickler sind, speichern Sie alle Systemprotokolle in einer Datei. Um sicherzustellen, dass der Router die gewünschte Methode / Funktion ausführt, protokollieren Sie natürlich auch die Zeile „Methode X wird ausgeführt !!“, aber alle diese blockieren Vorgänge Eingabe- / Ausgabestream wartet .
  • Alle Protokolle werden gespeichert und die folgenden Funktionszeilen ausgeführt. Der Thread funktioniert wieder .
  • Zeit, auf die Datenbank zuzugreifen und alle Produkte SELECT * FROM products - eine einfache Abfrage wie SELECT * FROM products erledigt ihre Aufgabe, aber wissen Sie was? Ja, dies ist eine blockierende E / A-Operation. Der Stream wartet .
  • Sie haben ein Array oder eine Liste aller Produkte erhalten, stellen jedoch sicher, dass Sie dies alles zugesagt haben. Der Stream wartet .
  • Jetzt haben Sie alle Produkte und es ist Zeit, die Vorlage für die zukünftige Seite zu rendern, aber vorher müssen Sie sie lesen. Der Stream wartet .
  • Die Rendering-Engine erledigt ihre Aufgabe und sendet eine Antwort an den Client. Der Thread funktioniert wieder .
  • Der Fluss ist frei wie ein Vogel am Himmel.

Wie langsam sind E / A-Vorgänge? Nun, es kommt auf das Spezifische an. Schauen wir uns die Tabelle an:


BedienungCPU-Zyklen
CPU-Register3 Maßnahmen
L1-Cache8 Maßnahmen
L2-Cache12 Maßnahmen
RAM150 Maßnahmen
Festplatte30.000.000 Maßnahmen
Netzwerk250.000.000 Maßnahmen

Netzwerk- und Festplattenlesevorgänge sind zu langsam. Stellen Sie sich vor, wie viele Anforderungen oder Aufrufe an externe APIs Ihr System während dieser Zeit verarbeiten könnte.


Zusammenfassend lässt sich sagen, dass E / A-Vorgänge den Thread warten lassen und Ressourcen verschwenden.




Problem C10K


Das Problem


C10k (dt. C10k; 10k Verbindungen - 10 Tausend Verbindungsproblem)


In den frühen 2000er Jahren waren Server- und Client-Computer langsam. Das Problem trat auf, wenn 10.000 Clientverbindungen zu demselben Computer parallel verarbeitet wurden.


Aber warum konnte das herkömmliche Thread-per-Request-Modell (Thread auf Anfrage) dieses Problem nicht lösen? Nun, lassen Sie uns ein wenig Mathe verwenden.


Die native Implementierung von Threads weist mehr als 1 MB Arbeitsspeicher pro Stream zu, so dass für 10.000 Threads 10 GB RAM erforderlich sind, und dies gilt nur für den Stream-Stack. Ja, und vergessen Sie nicht, wir sind in den frühen 2000ern !!



Heutzutage arbeiten Server- und Clientcomputer schneller und effizienter, und fast jede Programmiersprache oder jedes Framework kann dieses Problem bewältigen. Tatsächlich ist das Problem jedoch nicht gelöst. Bei 10 Millionen Clientverbindungen zu einem Computer tritt das Problem erneut auf (jetzt ist es das C10M-Problem ).


JavaScript-Rettung?


Vorsicht Spoiler !!!
Node.js löst tatsächlich das C10K-Problem ... aber wie ?!


Serverseitiges JavaScript war in den frühen 2000er Jahren nichts Neues und Ungewöhnliches. Zu dieser Zeit gab es bereits Implementierungen auf der JVM (Java Virtual Machine) - RingoJS und AppEngineJS, die am Thread-per-Request-Modell arbeiteten.


Aber wenn sie das Problem nicht lösen könnten, wie könnte dann Node.js ?! Alles nur, weil JavaScript Single-Threaded ist .




Node.js und die Ereignisschleife


Node.js


Node.js ist eine Serverplattform, die auf der Google Chrome Engine - V8 ausgeführt wird und JavaScript-Code in Maschinencode kompilieren kann.


Node.js verwendet ein ereignisgesteuertes Modell und eine nicht blockierende E / A- Architektur, wodurch es leicht und effizient ist. Dies ist weder ein Framework noch eine Bibliothek, sondern eine JavaScript-Laufzeit.


Schreiben wir ein kleines Beispiel:


 // 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); 

Nicht blockierende E / A.


Node.js verwendet nicht blockierende Eingabe- / Ausgabeoperationen. Was bedeutet das:


  • Der Hauptthread wird nicht durch E / A-Vorgänge blockiert.
  • Der Server wird weiterhin Anforderungen bearbeiten.
  • Wir müssen mit asynchronem Code arbeiten .

Schreiben wir ein Beispiel, in dem der Server eine HTML-Seite als Antwort auf eine Anfrage an /home und für alle anderen Anfragen sendet - 'Hello World'. Um eine HTML-Seite zu senden, müssen Sie sie zuerst aus einer Datei lesen.


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); 

Wenn die angeforderte URL /home lautet, wird das native fs Modul zum Lesen der Datei home.html verwendet.


Funktionen, die als Argumente in http.createServer und fs.readFile fallen, sind Rückrufe . Diese Funktionen werden zu einem späteren Zeitpunkt ausgeführt (die erste, sobald der Server die Anforderung empfängt, und die zweite, wenn die Datei von der Festplatte gelesen und in den Puffer gestellt wird).


Während die Datei von der Festplatte gelesen wird, kann Node.js andere Anforderungen verarbeiten und sogar die Datei erneut lesen und das alles in einem Stream ... aber wie ?!


Ereignisschleife


Die Ereignisschleife ist die Magie, die in Node.js geschieht. Dies ist buchstäblich eine Endlosschleife und tatsächlich ein Thread.



Libuv ist eine C-Bibliothek, die dieses Muster implementiert und Teil des Kernels Node.js. ist Mehr über libuv erfahren Sie hier .


Ein Ereigniszyklus besteht aus 6 Phasen. Jede Ausführung aller 6 Phasen wird als Tick ​​bezeichnet .



  • Zeitgeber : In dieser Phase werden Rückrufe ausgeführt, die von den setTimeout() und setInterval() wurden.
  • ausstehende Rückrufe : Fast alle Rückrufe werden ausgeführt, mit Ausnahme von close , Zeitgebern und setImmediate() .
  • im Leerlauf, vorbereiten : nur für interne Zwecke verwendet;
  • Umfrage : Verantwortlich für den Empfang neuer E / A-Ereignisse. Node.js kann an dieser Stelle blockieren.
  • check : Rückrufe, die durch die setImmediate() -Methode verursacht werden, werden zu diesem Zeitpunkt ausgeführt.
  • Rückrufe schließen : zum Beispiel socket.on('close', ...) ;

Nun, es gibt nur einen Thread, und dieser Thread ist eine Ereignisschleife, aber wer führt dann alle E / A aus?


beachten Sie !!!
Wenn eine Ereignisschleife eine E / A-Operation ausführen muss, verwendet sie den Betriebssystem-Thread aus dem Thread-Pool. Wenn die Aufgabe abgeschlossen ist, wird der Rückruf während der Phase ausstehender Rückrufe in die Warteschlange gestellt.



Ist das nicht cool?




Das Problem der CPU-intensiven Aufgaben


Node.js scheint perfekt zu sein! Sie können erstellen, was Sie wollen.


Schreiben wir eine API zur Berechnung von Primzahlen.


Eine Primzahl ist eine ganzzahlige (natürliche) Zahl, die größer als eins ist und nur durch 1 und durch sich selbst teilbar ist.



Bei einer gegebenen Zahl N sollte die API die ersten N Primzahlen in der Liste (oder im Array) berechnen und zurückgeben.


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 ist die Implementierung der erforderlichen Berechnungen: Die Funktion isPrime prüft, ob die Zahl prime ist, und nthPrime gibt N solcher Zahlen zurück.


Die Datei index.js ist für die Erstellung des Servers verantwortlich und verwendet das Modul prime.js , um jede Anforderung für /primes . Die Nummer N wird durch die Abfragezeichenfolge in der URL geworfen.


Um die ersten 20 Primzahlen zu erhalten, müssen wir eine Anfrage an http://localhost:8080/primes?n=20 .


Angenommen, wir haben 3 Clients, die an uns klopfen und versuchen, auf unsere nicht blockierende E / A-API zuzugreifen:


  • Die erste Abfrage 5 Primzahlen pro Sekunde.
  • Der zweite fragt nach 1000 Primzahlen pro Sekunde
  • Der dritte fordert 10.000.000.000 Primzahlen, aber ...


Wenn der dritte Client eine Anforderung sendet, wird der Hauptthread blockiert, und dies ist das Hauptsymptom für das Problem CPU-intensiver Aufgaben . Wenn der Hauptthread gerade eine „schwere“ Aufgabe ausführt, kann er nicht mehr auf andere Aufgaben zugreifen.


Aber was ist mit libuv? Wenn Sie sich erinnern, hilft diese Bibliothek Node.js bei der Ausführung von Eingabe- / Ausgabeoperationen mit OS-Threads, ohne den Hauptthread zu blockieren. Sie haben absolut Recht. Dies ist die Lösung für unser Problem. Damit dies jedoch möglich ist, muss unser Modul in der Sprache geschrieben sein C ++, damit libuv damit arbeiten kann.


Glücklicherweise wurde ab Version 10.5 das native Worker Threads- Modul zu Node.js hinzugefügt.



Arbeiter und ihre Ströme


Wie die Dokumentation sagt:


Mitarbeiter sind nützlich, um CPU-intensive JavaScript-Vorgänge auszuführen. Verwenden Sie sie nicht für Eingabe- / Ausgabeoperationen. Die bereits in Node.js integrierten Mechanismen bewältigen solche Aufgaben effizienter als der Worker-Thread.

Code korrigieren


Es ist Zeit, unseren Code neu zu schreiben:


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); 

In der index-workerthreads.js jede Anforderung an /primes eine Instanz der Worker Klasse (aus dem nativen Modul worker_threads ), um die primes-workerthreads.js hochzuladen und in den Worker-Thread auszuführen. Wenn die Liste der Primzahlen berechnet und bereit ist, wird das message ausgelöst. Das Ergebnis fällt in den Hauptstrom, da der Mitarbeiter keine Arbeit mehr hat. Er löst auch das exit Ereignis aus, sodass der Hauptstrom Daten an den Client senden kann.


primes-workerthreads.js sich etwas geändert. Es importiert workerData (dies ist eine Kopie der vom Hauptthread übergebenen Parameter) und parentPort über die das Ergebnis der Arbeit des Arbeiters an den Hauptthread zurückgegeben wird.


Versuchen wir nun noch einmal unser Beispiel und sehen, was passiert:



Der Haupt-Thread ist nicht mehr blockiert !!!!!



Jetzt funktioniert alles so, wie es sollte, aber es ist immer noch keine gute Praxis, Arbeiter ohne Grund zu produzieren. Das Erstellen von Fäden ist kein billiges Vergnügen. Stellen Sie sicher, dass Sie zuvor einen Thread-Pool erstellen.


Fazit


Node.js ist eine leistungsstarke Technologie, die nach Möglichkeit untersucht werden sollte.
Meine persönliche Empfehlung - immer neugierig sein! Wenn Sie von innen wissen, wie etwas funktioniert, können Sie effizienter damit arbeiten.


Das ist alles für heute Jungs. Ich hoffe, dieser Beitrag war nützlich für Sie und Sie haben etwas Neues über Node.js gelernt.


Vielen Dank fürs Lesen und bis in die nächsten Beiträge. .

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


All Articles