Wie verarbeitet Node.js im Allgemeinen 10.000 gleichzeitige Anforderungen?


394

Ich verstehe, dass Node.js einen Single-Thread und eine Ereignisschleife verwendet, um Anforderungen zu verarbeiten, die jeweils nur eine verarbeiten (was nicht blockiert). Aber wie funktioniert das? Sagen wir 10.000 gleichzeitige Anfragen. Die Ereignisschleife verarbeitet alle Anforderungen? Würde das nicht zu lange dauern?

Ich kann (noch) nicht verstehen, wie es schneller sein kann als ein Multithread-Webserver. Ich verstehe, dass Multithread-Webserver Ressourcen (Speicher, CPU) kostenintensiver sind, aber wäre es nicht immer noch schneller? Ich liege wahrscheinlich falsch; Bitte erläutern Sie, wie dieser Single-Thread bei vielen Anfragen schneller ist und was er normalerweise (auf hoher Ebene) tut, wenn er viele Anfragen wie 10.000 bearbeitet.

Und wird dieser Einzelthread mit dieser großen Menge gut skalieren? Bitte denken Sie daran, dass ich gerade erst anfange, Node.js zu lernen.


5
Weil die meiste Arbeit (Verschieben von Daten) nicht die CPU betrifft.
OrangeDog

5
Beachten Sie auch, dass nur weil nur ein Thread Javascript ausführt, dies nicht bedeutet, dass nicht viele andere Threads arbeiten.
OrangeDog

Diese Frage ist entweder zu weit gefasst oder ein Duplikat verschiedener anderer Fragen.
OrangeDog


Zusammen mit Single Threading führt Node.js eine Funktion aus, die als "nicht blockierende E / A" bezeichnet wird. Hier wird die ganze Magie getan
Anand N

Antworten:


762

Wenn Sie diese Frage stellen müssen, sind Sie wahrscheinlich nicht mit den Funktionen der meisten Webanwendungen / -dienste vertraut. Sie denken wahrscheinlich, dass alle Software dies tun:

user do an action
       
       v
 application start processing action
   └──> loop ...
          └──> busy processing
 end loop
   └──> send result to user

Auf diese Weise funktionieren jedoch keine Webanwendungen oder Anwendungen mit einer Datenbank als Back-End. Web-Apps tun dies:

user do an action
       
       v
 application start processing action
   └──> make database request
          └──> do nothing until request completes
 request complete
   └──> send result to user

In diesem Szenario verbringt die Software den größten Teil ihrer Laufzeit mit 0% CPU-Zeit und wartet auf die Rückkehr der Datenbank.

Multithread-Netzwerk-App:

Multithread-Netzwerk-Apps bewältigen die oben genannte Arbeitslast wie folgt:

request ──> spawn thread
              └──> wait for database request
                     └──> answer request
request ──> spawn thread
              └──> wait for database request
                     └──> answer request
request ──> spawn thread
              └──> wait for database request
                     └──> answer request

Daher verbringt der Thread die meiste Zeit mit 0% CPU und wartet darauf, dass die Datenbank Daten zurückgibt. Dabei mussten sie den für einen Thread erforderlichen Speicher zuweisen, der einen vollständig separaten Programmstapel für jeden Thread usw. enthält. Außerdem müssten sie einen Thread starten, der zwar nicht so teuer ist wie das Starten eines vollständigen Prozesses, aber immer noch nicht genau billig.

Singlethreaded-Ereignisschleife

Da wir die meiste Zeit mit 0% CPU verbringen, können Sie Code ausführen, wenn wir keine CPU verwenden. Auf diese Weise erhält jede Anforderung immer noch die gleiche CPU-Zeit wie Multithread-Anwendungen, wir müssen jedoch keinen Thread starten. Also machen wir das:

request ──> make database request
request ──> make database request
request ──> make database request
database request complete ──> send response
database request complete ──> send response
database request complete ──> send response

In der Praxis geben beide Ansätze Daten mit ungefähr der gleichen Latenz zurück, da die Datenbankantwortzeit die Verarbeitung dominiert.

Der Hauptvorteil hier ist, dass wir keinen neuen Thread erzeugen müssen, so dass wir nicht viel Malloc machen müssen, was uns verlangsamen würde.

Magisches, unsichtbares Einfädeln

Das scheinbar Rätselhafte ist, wie beide oben genannten Ansätze es schaffen, die Arbeitslast "parallel" auszuführen. Die Antwort ist, dass die Datenbank mit einem Thread versehen ist. Unsere Single-Threaded-App nutzt also tatsächlich das Multithread-Verhalten eines anderen Prozesses: der Datenbank.

Wo Singlethreaded-Ansatz fehlschlägt

Eine Singlethread-App schlägt fehl, wenn Sie viele CPU-Berechnungen durchführen müssen, bevor Sie die Daten zurückgeben. Nun, ich meine nicht eine for-Schleife, die das Datenbankergebnis verarbeitet. Das ist immer noch meistens O (n). Was ich meine, sind Dinge wie Fourier-Transformation (z. B. MP3-Codierung), Raytracing (3D-Rendering) usw.

Eine weitere Gefahr von Singlethread-Apps besteht darin, dass nur ein einziger CPU-Kern verwendet wird. Wenn Sie also einen Quad-Core-Server haben (heutzutage nicht ungewöhnlich), verwenden Sie die anderen 3 Kerne nicht.

Wo Multithread-Ansatz fehlschlägt

Eine Multithread-App schlägt fehl, wenn Sie pro Thread viel RAM zuweisen müssen. Erstens bedeutet die RAM-Auslastung selbst, dass Sie nicht so viele Anforderungen bearbeiten können wie eine Singlethread-App. Schlimmer noch, Malloc ist langsam. Das Zuweisen vieler, vieler Objekte (wie es bei modernen Webframeworks üblich ist) bedeutet, dass wir möglicherweise langsamer sind als Singlethread-Apps. Hier gewinnt normalerweise node.js.

Ein Anwendungsfall, der Multithreading verschlimmert, ist, wenn Sie eine andere Skriptsprache in Ihrem Thread ausführen müssen. Normalerweise müssen Sie zuerst die gesamte Laufzeit für diese Sprache mallocieren, dann müssen Sie die von Ihrem Skript verwendeten Variablen mallocieren.

Wenn Sie also Netzwerk-Apps in C oder Go oder Java schreiben, ist der Aufwand für das Threading normalerweise nicht allzu hoch. Wenn Sie einen C-Webserver für PHP oder Ruby schreiben, ist es sehr einfach, einen schnelleren Server in Javascript oder Ruby oder Python zu schreiben.

Hybrider Ansatz

Einige Webserver verwenden einen hybriden Ansatz. Nginx und Apache2 implementieren beispielsweise ihren Netzwerkverarbeitungscode als Thread-Pool von Ereignisschleifen. Jeder Thread führt eine Ereignisschleife aus, in der Anforderungen gleichzeitig mit einem Thread verarbeitet werden. Die Anforderungen werden jedoch auf mehrere Threads verteilt.

Einige Single-Threaded-Architekturen verwenden ebenfalls einen Hybridansatz. Anstatt mehrere Threads von einem einzigen Prozess aus zu starten, können Sie mehrere Anwendungen starten, z. B. 4 node.js-Server auf einem Quad-Core-Computer. Anschließend verwenden Sie einen Load Balancer, um die Arbeitslast auf die Prozesse zu verteilen.

Tatsächlich sind die beiden Ansätze technisch identische Spiegelbilder voneinander.


105
Dies ist bei weitem die beste Erklärung für den Knoten, den ich bisher gelesen habe. Diese "Single-Threaded-App nutzt tatsächlich das Multithread-Verhalten eines anderen Prozesses: der Datenbank."
Hat

Was ist, wenn der Client mehrere Anforderungen im Knoten stellt, z. B. einen Namen abruft und ihn ändert, und sagt, dass diese Vorgänge an den Server gesendet werden, um von vielen Clients sehr schnell verarbeitet zu werden? Wie könnte ich mit einem solchen Szenario umgehen?
Remario

3
@CaspainCaldion Es kommt darauf an, was Sie unter sehr schnell und vielen Kunden verstehen. Wie es ist, kann node.js mehr als 1000 Anforderungen pro Sekunde verarbeiten und die Geschwindigkeit ist nur auf die Geschwindigkeit Ihrer Netzwerkkarte beschränkt. Beachten Sie, dass 1000 Anforderungen pro Sekunde nicht gleichzeitig mit Clients verbunden sind. Es kann die 10000 gleichzeitigen Clients problemlos verarbeiten. Der eigentliche Engpass ist die Netzwerkkarte.
Slebetman

1
@slebetman, beste Erklärung überhaupt. Eine Sache ist jedoch, wenn ich einen Algorithmus für maschinelles Lernen habe, der einige Informationen verarbeitet und entsprechende Ergebnisse liefert, sollte ich einen Multithread-Ansatz oder einen Single-Thread verwenden
Ganesh Karewad,

5
@ GaneshKarewad-Algorithmen verwenden CPU, Dienste (Datenbank, REST-API usw.) verwenden E / A. Wenn die KI ein in js geschriebener Algorithmus ist, sollten Sie ihn in einem anderen Thread oder Prozess ausführen. Wenn die KI ein Dienst ist, der auf einem anderen Computer ausgeführt wird (z. B. Amazon-, Google- oder IBM KI-Dienste), verwenden Sie eine einzelne Thread-Architektur.
Slebetman

46

Was Sie zu denken scheinen, ist, dass der größte Teil der Verarbeitung in der Knotenereignisschleife abgewickelt wird. Der Knoten führt die E / A-Arbeit tatsächlich zu Threads. E / A-Vorgänge dauern in der Regel um Größenordnungen länger als CPU-Vorgänge. Warum muss die CPU darauf warten? Außerdem kann das Betriebssystem E / A-Aufgaben bereits sehr gut ausführen. Da Node nicht wartet, wird eine viel höhere CPU-Auslastung erzielt.

Stellen Sie sich NodeJS analog als einen Kellner vor, der die Kundenbestellungen entgegennimmt, während die I / O-Köche sie in der Küche zubereiten. Andere Systeme haben mehrere Köche, die eine Kundenbestellung entgegennehmen, das Essen zubereiten, den Tisch abräumen und sich erst dann um den nächsten Kunden kümmern.


5
Danke für die Restaurant-Analogie! Ich finde Analogien und Beispiele aus der Praxis so viel einfacher zu lernen.
LaVache

13

Ich verstehe, dass Node.js einen Single-Thread und eine Ereignisschleife verwendet, um Anforderungen zu verarbeiten, die jeweils nur eine verarbeiten (was nicht blockiert).

Ich könnte falsch verstehen, was Sie hier gesagt haben, aber "eins nach dem anderen" klingt so, als würden Sie die ereignisbasierte Architektur möglicherweise nicht vollständig verstehen.

In einer "konventionellen" (nicht ereignisgesteuerten) Anwendungsarchitektur verbringt der Prozess viel Zeit damit, herumzusitzen und darauf zu warten, dass etwas passiert. In einer ereignisbasierten Architektur wie Node.js wartet der Prozess nicht nur, er kann auch mit anderen Arbeiten fortfahren.

Beispiel: Sie erhalten eine Verbindung von einem Client, akzeptieren diese, lesen die Anforderungsheader (im Fall von http) und beginnen dann, auf die Anforderung zu reagieren. Wenn Sie den Anforderungshauptteil lesen, senden Sie im Allgemeinen einige Daten an den Client zurück (dies ist eine absichtliche Vereinfachung des Verfahrens, um den Punkt zu demonstrieren).

In jeder dieser Phasen wird die meiste Zeit damit verbracht, darauf zu warten, dass einige Daten vom anderen Ende ankommen - die tatsächliche Zeit, die für die Verarbeitung im JS-Hauptthread aufgewendet wird, ist normalerweise recht gering.

Wenn sich der Status eines E / A-Objekts (z. B. einer Netzwerkverbindung) so ändert, dass es verarbeitet werden muss (z. B. wenn Daten auf einem Socket empfangen werden, ein Socket beschreibbar wird usw.), wird der JS-Hauptthread von Node.js mit einer Liste geweckt von Artikeln, die verarbeitet werden müssen.

Es findet die relevante Datenstruktur und gibt ein Ereignis in dieser Struktur aus, das dazu führt, dass Rückrufe ausgeführt werden, die eingehenden Daten verarbeiten oder weitere Daten in einen Socket schreiben usw. Sobald alle E / A-Objekte verarbeitet werden müssen verarbeitet, wartet der Haupt-JS-Thread von Node.js erneut, bis mitgeteilt wird, dass weitere Daten verfügbar sind (oder ein anderer Vorgang abgeschlossen wurde oder eine Zeitüberschreitung aufgetreten ist).

Beim nächsten Aufwecken kann es durchaus daran liegen, dass ein anderes E / A-Objekt verarbeitet werden muss - beispielsweise eine andere Netzwerkverbindung. Jedes Mal werden die relevanten Rückrufe ausgeführt und dann geht es wieder in den Ruhezustand und wartet darauf, dass etwas anderes passiert.

Der wichtige Punkt ist, dass die Verarbeitung verschiedener Anforderungen verschachtelt ist, eine Anforderung nicht von Anfang bis Ende verarbeitet wird und dann zur nächsten übergeht.

Meiner Meinung nach besteht der Hauptvorteil darin, dass eine langsame Anfrage (z. B. Sie versuchen, 1 MB Antwortdaten über eine 2G-Datenverbindung an ein Mobiltelefon zu senden, oder Sie führen eine wirklich langsame Datenbankabfrage durch) gewonnen hat. ' t schnellere blockieren.

In einem herkömmlichen Multithread-Webserver haben Sie normalerweise einen Thread für jede verarbeitete Anforderung, und er verarbeitet NUR diese Anforderung, bis sie abgeschlossen ist. Was passiert, wenn Sie viele langsame Anfragen haben? Am Ende hängen viele Ihrer Threads an der Verarbeitung dieser Anforderungen, und andere Anforderungen (bei denen es sich möglicherweise um sehr einfache Anforderungen handelt, die sehr schnell bearbeitet werden können) werden in die Warteschlange gestellt.

Abgesehen von Node.js gibt es viele andere ereignisbasierte Systeme, die im Vergleich zum herkömmlichen Modell ähnliche Vor- und Nachteile haben.

Ich würde nicht behaupten, dass ereignisbasierte Systeme in jeder Situation oder mit jeder Arbeitslast schneller sind - sie funktionieren in der Regel gut für E / A-gebundene Workloads, nicht so gut für CPU-gebundene.


12

Verarbeitungsschritte für Single-Threaded-Ereignisschleifenmodelle:

  • Clients Anfrage an Webserver senden.

  • Node JS Web Server verwaltet intern einen begrenzten Thread-Pool, um Dienste für die Clientanforderungen bereitzustellen.

  • Node JS Web Server empfängt diese Anforderungen und stellt sie in eine Warteschlange. Es ist als "Ereigniswarteschlange" bekannt.

  • Der Knoten JS Web Server verfügt intern über eine Komponente, die als "Ereignisschleife" bezeichnet wird. Dieser Name hat den Namen, dass er eine unbestimmte Schleife verwendet, um Anforderungen zu empfangen und zu verarbeiten.

  • Die Ereignisschleife verwendet nur einen einzelnen Thread. Es ist das Herzstück des Node JS Platform Processing Model.

  • Die Ereignisschleife überprüft, ob eine Clientanforderung in die Ereigniswarteschlange gestellt wurde. Wenn nicht, warten Sie auf unbestimmte Zeit auf eingehende Anfragen.

  • Wenn ja, holen Sie sich eine Clientanforderung aus der Ereigniswarteschlange

    1. Startet die Verarbeitung dieser Clientanforderung
    2. Wenn für diese Clientanforderung keine blockierenden E / A-Vorgänge erforderlich sind, verarbeiten Sie alles, bereiten Sie die Antwort vor und senden Sie sie an den Client zurück.
    3. Wenn für diese Clientanforderung einige blockierende E / A-Vorgänge erforderlich sind, z. B. die Interaktion mit Datenbank, Dateisystem und externen Diensten, wird ein anderer Ansatz verfolgt
  • Überprüft die Verfügbarkeit von Threads aus dem internen Thread-Pool
  • Nimmt einen Thread auf und weist diese Client-Anforderung diesem Thread zu.
  • Dieser Thread ist dafür verantwortlich, diese Anforderung entgegenzunehmen, zu verarbeiten, blockierende E / A-Vorgänge auszuführen, die Antwort vorzubereiten und an die Ereignisschleife zurückzusenden

    Sehr schön erklärt von @Rambabu Posa für weitere Erklärungen werfen Sie diesen Link


Das Diagramm in diesem Blog-Beitrag scheint falsch zu sein. Was sie in diesem Artikel erwähnt haben, ist nicht ganz korrekt.
rranj

11

Hinzufügen zur Antwort von slebetman: Wenn Sie sagen Node.JS, dass 10.000 gleichzeitige Anforderungen verarbeitet werden können, handelt es sich im Wesentlichen um nicht blockierende Anforderungen, dh diese Anforderungen beziehen sich hauptsächlich auf Datenbankabfragen.

Intern wird event loopvon Node.JSa behandelt thread pool, wobei jeder Thread eine behandelt non-blocking requestund die Ereignisschleife weiterhin weitere Anforderungen abhört, nachdem die Arbeit an einen der Threads von delegiert wurde thread pool. Wenn einer der Threads die Arbeit abgeschlossen hat, sendet er ein Signal an den, event loopdass er beendet ist callback.Event loopVerarbeiten Sie dann diesen Rückruf und senden Sie die Antwort zurück.

Lesen Sie mehr darüber nextTick, wie die Ereignisschleife intern funktioniert, da Sie NodeJS noch nicht kennen . Lesen Sie Blogs auf http://javascriptissexy.com , sie waren wirklich hilfreich für mich, als ich mit JavaScript / NodeJS anfing.


2

Hinzufügen zu Slebetman Antwort für mehr Klarheit darüber, was beim Ausführen des Codes passiert.

Der interne Thread-Pool in nodeJs hat standardmäßig nur 4 Threads. und es ist nicht so, dass die gesamte Anforderung an einen neuen Thread aus dem Thread-Pool angehängt wird. Die gesamte Ausführung der Anforderung erfolgt wie bei jeder normalen Anforderung (ohne Blockierungsaufgabe), nur dann, wenn eine Anforderung lange ausgeführt wird oder eine schwere Operation wie db ausgeführt wird Aufruf, eine Dateivorgang oder eine http-Anforderung Die Aufgabe wird in den internen Thread-Pool eingereiht, der von libuv bereitgestellt wird. Und da nodeJs standardmäßig 4 Threads im internen Thread-Pool bereitstellt, wartet jede fünfte oder nächste gleichzeitige Anforderung, bis ein Thread frei ist, und sobald diese Vorgänge beendet sind, wird der Rückruf in die Rückrufwarteschlange verschoben. und wird von der Ereignisschleife aufgenommen und sendet die Antwort zurück.

Jetzt kommt eine weitere Information, dass es nicht nur eine einzelne Rückrufwarteschlange gibt, sondern viele Warteschlangen.

  1. NextTick-Warteschlange
  2. Micro Task Queue
  3. Timer-Warteschlange
  4. E / A-Rückrufwarteschlange (Anfragen, Dateioperationen, Datenbankoperationen)
  5. E / A-Abfragewarteschlange
  6. Überprüfen Sie die Phasenwarteschlange oder SetImmediate
  7. Warteschlange für Handler schließen

Immer wenn eine Anfrage eingeht, wird der Code in dieser Reihenfolge von Rückrufen in der Warteschlange ausgeführt.

Es ist nicht so, als ob eine Blockierungsanforderung an einen neuen Thread angehängt wird. Standardmäßig gibt es nur 4 Threads. Dort findet also eine weitere Warteschlange statt.

Immer wenn in einem Code ein Blockierungsprozess wie das Lesen von Dateien auftritt, wird eine Funktion aufgerufen, die Thread aus dem Thread-Pool verwendet. Sobald der Vorgang abgeschlossen ist, wird der Rückruf an die jeweilige Warteschlange übergeben und in der angegebenen Reihenfolge ausgeführt.

Alles wird basierend auf der Art des Rückrufs in die Warteschlange gestellt und in der oben genannten Reihenfolge verarbeitet.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.