Wie kann ich in Node.js mehrere Anfragen gleichzeitig verarbeiten?

Melden
  1. Grundlagen der Parallelität in Node.js
  2. Umgang mit mehreren Anfragen in einem HTTP-Server
  3. Synchrone Operationen vermeiden
  4. Parallele Tasks mit Promise.all
  5. Worker Threads für CPU-Intensive Aufgaben
  6. Lastverteilung und Skalierung
  7. Zusammenfassung

Grundlagen der Parallelität in Node.js

Node.js verwendet ein eventgetriebenes, nicht-blockierendes I/O-Modell, welches es ermöglicht, mehrere Anfragen gleichzeitig zu bedienen, ohne dass eine blockierende Operation den Event-Loop aufhält. Das heißt, wenn eine Anfrage eine Zeitintensive Aufgabe wie Datenbankzugriff oder Netzwerkoperationen ausführt, wartet Node.js nicht darauf, sondern kann andere Anfragen parallel abwickeln.

Das bedeutet allerdings nicht, dass mehrere Operationen wirklich gleichzeitig auf einer CPU ausgeführt werden (außer bei mehreren CPU-Kernen mit Worker Threads), sondern vielmehr, dass Node.js durch das Event-Loop-Konzept schnell zwischen diesen Tasks wechselt, sobald sie auf E/A warten.

Umgang mit mehreren Anfragen in einem HTTP-Server

Wenn du beispielsweise einen einfachen HTTP-Server mit dem Modul http erstellst, kannst du per Default mehrere eingehende Verbindungen gleichzeitig handhaben. Sobald eine Anfrage eintrifft, wird der Callback aufgerufen, der die Anfrage verarbeitet. Solange keine synchronen Blockierungen im Code sind, können weitere Anfragen ebenfalls parallel bearbeitet werden.

const http = require(http);const server = http.createServer((req, res) => { // Simuliere eine asynchrone Aufgabe setTimeout(() => { res.writeHead(200, {Content-Type: text/plain}); res.end(Antwort nach Verzögerung\n); }, 2000);});server.listen(3000, () => { console.log(Server läuft auf http://localhost:3000);});

In diesem Beispiel können, auch wenn eine Anfrage zwei Sekunden schläft, andere Anfragen von Node.js ohne Verzögerung starten und direkt bearbeitet werden. Das liegt daran, dass die Verzögerung mit setTimeout asynchron ist und somit nicht den Event-Loop blockiert.

Synchrone Operationen vermeiden

Eine mögliche Ursache für blockierendes Verhalten ist, wenn synchroner Code verwendet wird, wie z. B. fs.readFileSync oder aufwendige Berechnungen im Haupt-Thread. Solche Operationen halten den Event-Loop an und verhindern, dass andere Anfragen parallel bearbeitet werden können.

Um mehrere Anfragen effizient zu verarbeiten, ist es daher zwingend, synchrone Blockierungen zu vermeiden und stattdessen asynchrone APIs und Promises oder Async/Await zu verwenden. Dies führt dazu, dass Node.js während der Wartezeiten andere Aufgaben übernehmen kann.

Parallele Tasks mit Promise.all

Manchmal müssen innerhalb einer einzelnen Anfrage mehrere asynchrone Operationen gleichzeitig durchgeführt werden, zum Beispiel mehrere Datenbankabfragen. Hierfür bietet sich Promise.all an, um diese parallelen Promises abzuwarten.

async function handleRequest() { const result1 = fetchDataFromDB1(); const result2 = fetchDataFromDB2(); const = await Promise.all( ); return { data1, data2 };}

Der Vorteil dabei liegt darin, dass beide Datenbankanfragen gleichzeitig laufen und die Gesamtdauer somit nicht die Summe der Einzelzeiten, sondern nur die längste dauert. Dies bedeutet eine optimierte Reaktionszeit beim Handling von Anfragen.

Worker Threads für CPU-Intensive Aufgaben

Node.js ist standardmäßig single-threaded im Hinblick auf JavaScript-Ausführung. Wenn eine Anfrage sehr rechenintensive Aufgaben umfasst (z. B. komplexe Berechnungen, Bildverarbeitung), wird der Event-Loop blockiert, und somit auch die Bearbeitung anderer Anfragen verzögert.

In solchen Fällen bietet es sich an, worker_threads zu verwenden. Diese ermöglichen es, JavaScript-Code in separaten Threads auszuführen, wodurch parallele Verarbeitung von CPU-intensiven Aufgaben möglich wird ohne den Haupt-Thread zu blockieren.

Ein einfaches Beispiel sieht so aus:

const { Worker } = require(worker_threads);function runWorker() { return new Promise((resolve, reject) => { const worker = new Worker(./worker-task.js); worker.on(message, resolve); worker.on(error, reject); worker.on(exit, (code) => { if (code !== 0) reject(new Error(`Worker gestoppt mit Exit-Code ${code}`)); }); });}// Beispiel in einem Request-Handlerasync function handleRequest(req, res) { try { const result = await runWorker(); res.end(`Ergebnis vom Worker: ${result}`); } catch (err) { res.end(Fehler beim Verarbeiten); }}

Dadurch kannst du also parallel mehrere arbeitsintensive Tasks auslagern und so den Hauptserver für eingehende Anfragen reaktionsfähig halten.

Lastverteilung und Skalierung

Um noch mehr Anfragen gleichzeitig verarbeiten zu können, kannst du Node.js im sogenannten Cluster-Modus betreiben. Das Modul cluster ermöglicht es, mehrere Node-Prozesse zu starten, die auf mehreren CPU-Kernen verteilt sind und so die Last effektiv teilen. Der Master-Prozess verteilt eingehende Verbindungen auf die Worker-Prozesse.

Das führt dazu, dass dein Server die parallele Verarbeitung zahlreicher Anfragen auch über mehrere Kerne hinweg skalieren kann.

Typischerweise sieht das so aus:

const cluster = require(cluster);const http = require(http);const numCPUs = require(os).cpus().length;if (cluster.isMaster) { console.log(`Master ${process.pid} startet`); for (let i = 0; i < numCPUs; i++) { cluster.fork(); } cluster.on(exit, (worker, code, signal) => { console.log(`Worker ${worker.process.pid} gestorben, starte neuen`); cluster.fork(); });} else { // Worker Prozess - HTTP Server http.createServer((req, res) => { res.writeHead(200); res.end(`Hallo von Worker ${process.pid}\n`); }).listen(8000); console.log(`Worker ${process.pid} läuft`);}

Auf diese Weise kannst du viele gleichzeitige Anfragen viel effizienter verarbeiten als mit einem einzelnen Node-Prozess.

Zusammenfassung

Node.js verarbeitet mehrere Anfragen gleichzeitig dank seiner Event-Loop-Architektur und asynchronem I/O. Um diese Fähigkeit optimal zu nutzen, solltest du synchrone Blockierungen vermeiden und stattdessen asynchrone Muster mit Callbacks, Promises oder Async/Await verwenden. Für CPU-intensive Aufgaben kannst du Worker Threads nutzen, und durch Cluster kannst du mehrere Node-Prozesse auf verschiedenen CPU-Kernen laufen lassen, um die Last zu verteilen.

Diese Kombination aus asynchroner Programmierung, Worker Threads und Clustering ermöglicht es Node.js, sehr viele Anfragen gleichzeitig performant zu verarbeiten.

0

Kommentare