Node.js läuft standardmäßig auf einem einzigen CPU-Kern. Dies bedeutet, dass die Anwendung nur einen Bruchteil der gesamten Rechenleistung moderner Mehrkernprozessoren nutzen kann. Hier kommt das Clustering ins Spiel. Clustering ist eine bewährte Methode zur Verbesserung der Leistung und Verfügbarkeit von Anwendungen. Ein Cluster ist eine Gruppe von Computern oder Prozessen, die zusammenarbeiten, um eine gemeinsame Aufgabe zu erfüllen. Es gibt zwei Haupttypen von Clustern: Hardware-Cluster und Software-Cluster. In diesem Beitrag werden wir uns darauf konzentrieren, wie man Node.js-Anwendungen mit Hilfe von PM2, einem beliebten Prozessmanager, softwareseitig clustern kann.
Zum Kapitel springen Software-Cluster
Im Gegensatz zum Hardware-Cluster, bei dem mehrere Rechner im Verbund zusammenarbeiten besteht ein Software-Cluster aus mehreren Software-Prozessen, die auf einer Maschinen laufen. Allgemeine Vorteile eines Software-Clusters sind:
Einfachheit
Keine zusätzliche Hardware erforderlich, da mehrere Prozesse auf einer Maschine laufen können.
Ressourcenausnutzung
Optimale Nutzung der verfügbaren CPU-Kerne.
Skalierbarkeit
Anwendungen können leicht auf mehrere Instanzen skaliert werden.
Im Bezug auf unsere Node.JS-Anwendung bedeutet dies:
- Leistungssteigerung:
Nutzung mehrerer oder aller CPU-Kerne zur parallelen Verarbeitung von Anfragen. - Lastverteilung:
Gleichmäßige Verteilung der Anfragen auf die verschiedene Prozesse. - Höhere Verfügbarkeit:
Fortlaufende Verarbeitung von Anfragen bei Ausfall eines Prozesses. - Neustart ohne Ausfall:
Durch die Verwendung von mindestens zwei Kernen kann deine Anwendung mithilfe eines PM2-Software-Clusters ohne Ausfall neugestartet werden. Stichwort: Zero-Downtime.
Zum Kapitel springen Was ist PM2?
PM2 ist ein Prozessmanager für Node.js-Anwendungen, der Funktionen wie Prozessüberwachung, Lastverteilung und automatische Neustarts bei Fehlern bietet. PM2 kann sowohl als Daemon als auch als Cluster-Manager fungieren. Neben dem Cluster-Betrieb wird PM2 auch verwendet, um Anwendungen im Hintergrund zu betreiben, Logfiles zu verwalten und die Anwendung im Falle eines Absturzes oder eines Server-Reboots automatisch neu zu starten.
Wenn du PM2 noch nicht installiert ist, solltest du dies als erstes mithilfe des folgenden Befehls nacholen:
npm install pm2 -g
Nun kannst du PM2 bereits verwenden, um deine Node-Anwendungen zu verwalten. Damit jeder das Beispiel einfach nachmachen und auch nachvollziehen kann, habe ich hier eine main.js
-Datei vorbereitet. Diese werden wir als Ausgangslage verwenden um die verschiedenen Szenarien des Clustings zu erkunden.
const fs = require('fs');
const path = require('path');
const randomString = (Math.random() + 1).toString(36).substring(2);
const fileName = `main_process_file_${randomString}.txt`;
const filePath = path.join(__dirname, fileName);
fs.writeFile(filePath, 'Diese Datei wird in jedem Prozess erstellt', (err) => {
if (err) throw err;
console.log(`Datei ${fileName} wurde erstellt`);
});
Anstatt deine Anwendung mit node main.js
auszuführen kannst du PM2 verwenden um die Anwendung im Hintergrund zu starten:
pm2 start main.js
[PM2] Starting /Users/cooper.bin/Downloads/pm2-cluster-tutorial/main.js in fork_mode (1 instance)
[PM2] Done.
┌────┬─────────┬─────────────┬─────────┬─────────┬──────────┬────────┬──────┬───────────┬──────────┬──────────┬──────────┬──────────┐
│ id │ name │ namespace │ version │ mode │ pid │ uptime │ ↺ │ status │ cpu │ mem │ user │ watching │
├────┼─────────┼─────────────┼─────────┼─────────┼──────────┼────────┼──────┼───────────┼──────────┼──────────┼──────────┼──────────┤
│ 0 │ main │ default │ N/A │ fork │ 14553 │ 0s │ 0 │ online │ 0% │ 1.4mb │ coo… │ disabled │
└────┴─────────┴─────────────┴─────────┴─────────┴──────────┴────────┴──────┴───────────┴──────────┴──────────┴──────────┴──────────┘
Anhand der erstellen Datei im Verzeichnis ist ersichtlich, dass die Anwendung erfolgreich gestartet und die Datei angelegt wurde.
.
├── main.js
└── main_process_file_n6hn8gpd6jg.txt
Du kannst dir die Liste der aktuellen Prozesse jederzeit anzeigen lassen indem du den Befehl pm2 list
verwendest.
Zum Stoppen oder gar löschen der Prozesse kannst du einen der beiden Befehle verwenden:
pm2 stop 0
pm2 delete 0
Neben der id
kannst du auch direkt mit dem Namen des Prozesses arbeiten:
pm2 stop main
pm2 delete main
Diese wenigen Schritte reichen bereits aus, um deine Anwendung im Single-Core und Hintergrund laufen zu lassen. Kommen wir nun aber direkt zu dem Betrieb im Software-Cluster mit PM2.
Zum Kapitel springen Node.JS-Cluster mit PM2
Um eine Node.js-Anwendung im Cluster-Modus zu betreiben, kann eine Konfigurationsdatei verwendet werden. Diese Konfigurationsdatei kommt direkt in das Projektverzeichnis der Node-Anwendung und kann beliebig benannt werden. Ich nenne meine Konfiguration ganz simple pm2-config
.
.
├── main.js
└── pm2-config.json
Für einen einfachen Cluster könnte die Konfigurationsdatei wie folgt aussehen:
[
{
"name": "app_main",
"script": "./main.js",
"exec_mode": "cluster",
"instances": "1"
},
{
"name": "app_sub",
"script": "./main.js",
"exec_mode": "cluster",
"instances": "1"
}
]
Diese Konfiguration startet zwei Instanzen der main.js
auf zwei verschiedenen Kernen. Um den Cluster zu starten, kann direkt die Konfigurationsdatei über PM2 angesprochen werden:
pm2 start pm2-config.json
Gleiches gilt dann auch für die beiden Befehle zum Stoppen oder gar Löschen der Prozesse:
pm2 stop pm2-config.json
pm2 delete pm2-config.json
Diese Konfigurationsdatei startet zwei Instanzen der main.js
Anwendung im Cluster-Modus.
PM2][WARN] Applications app_main, app_sub not running, starting...
[PM2] App [app_sub] launched (1 instances)
[PM2] App [app_main] launched (1 instances)
┌────┬─────────────┬─────────────┬─────────┬─────────┬──────────┬────────┬──────┬───────────┬──────────┬──────────┬──────────┬──────────┐
│ id │ name │ namespace │ version │ mode │ pid │ uptime │ ↺ │ status │ cpu │ mem │ user │ watching │
├────┼─────────────┼─────────────┼─────────┼─────────┼──────────┼────────┼──────┼───────────┼──────────┼──────────┼──────────┼──────────┤
│ 0 │ app_main │ default │ N/A │ cluster │ 17034 │ 0s │ 0 │ online │ 0% │ 35.0mb │ coo… │ disabled │
│ 1 │ app_sub │ default │ N/A │ cluster │ 17035 │ 0s │ 0 │ online │ 0% │ 34.8mb │ coo… │ disabled │
└────┴─────────────┴─────────────┴─────────┴─────────┴──────────┴────────┴──────┴───────────┴──────────┴──────────┴──────────┴──────────┘
Ein aufmerksames Auge wird feststellen, dass nun zwei Dateien erstellt werden. Das zeigt, dass unsere Anwendung erfolgreich im Cluster gestartet wurde.
.
├── main.js
├── main_process_file_5440o168q2.txt
├── main_process_file_m13nezg8vg.txt
└── pm2-config.json
Zum Kapitel springen Bemerklung zur PM2-Dokumentation
In der PM2-Dokumentation steht:
"The cluster mode allows networked Node.js applications (http(s)/tcp/udp server) to be scaled across all CPUs available, without any code modifications."
Dieses Zitat bezieht sich hauptsächlich auf Netzwerkanwendungen. Das bedeutet, dass Webserver, die HTTP, HTTPS, TCP oder UDP verwenden, im Cluster-Modus ohne Anpassungen betrieben werden können. Der Vorteil hierbei ist, dass keine Änderungen am Code nötig sind, um Ports oder Netzwerkeinstellungen zu verwalten.
Produktempfehlungen und -suche in Verbindung mit dem Amazon Partnerprogramm:
¹ Angaben ohne Gewähr. Bei einem Kauf über den Link erhalten wir eine Provision.
Jedoch ist hier Vorsicht geboten, denn dies gilt nicht für alle Arten von Node.js-Anwendungen. Bei Anwendungen, die Cronjobs, Intervalle oder andere zeitgesteuerte Aufgaben enthalten, kann das Clustering dazu führen, dass diese Tasks mehrfach ausgeführt werden – einmal pro Instanz des Clusters. Der Beispielcode zum Erstellen der Dateien zeigt das Problem. Dies kann unerwünschte Effekte haben, wenn diese Tasks nur einmal ausgeführt werden sollen.
Zum Kapitel springen Erkennung für den Main- und Subprozess
Um zu unterscheiden, ob ein Code im Main-Prozess oder einem Sub-Prozess läuft, können Umgebungsvariablen und einfache Überprüfungen verwendet werden. Diese Überprüfung hängt direkt mit der Konfigurationsdatei zusammen.
In der pm2-config.json
Datei werden die Namen der Prozesse definiert, in unserem Beispiel app_main
und app_sub
. Diese Namen werden als Umgebungsvariablen process.env.name
gesetzt, wenn die Prozesse durch PM2 gestartet werden. Mit dieser Umgebungsvariable können wir im Code überprüfen, welcher Prozess gerade ausgeführt wird.
Hier ist ein vollständiges Beispiel mit Änderungen in der main.js
, das zeigt, wie diese Unterscheidung umgesetzt werden könnte:
const fs = require('fs');
const path = require('path');
const appName = 'app';
// Bestimmen des Main- und Subprozess-Namens; PM2 übergibt Umgebungsvariablen beim Start
// diese können über `process.env.name` ausgelesen werden; vgl.: `pm2-config.json`
// Wenn kein `process.env.name` verfügbar ist (also nicht mit PM2 gestartet), wird einfach der Main-Prozessname verwendet
const mainProcessName = `${appName}_main`;
const currentProcessName = process.env.name || mainProcessName;
// Funktion zur Überprüfung, ob es sich um den Main-Prozess handelt
function isMainProcess() {
return currentProcessName === mainProcessName;
}
// Logik für den Main-Prozess
if ( isMainProcess() ) {
const randomString = (Math.random() + 1).toString(36).substring(2);
const fileName = `main_process_file_${randomString}.txt`;
const filePath = path.join(__dirname, fileName);
fs.writeFile(filePath, 'Diese Datei wird nur im Main-Prozess erstellt', (err) => {
if (err) throw err;
console.log(`Datei ${fileName} wurde erstellt`);
});
} else {
console.log('Dies ist ein Sub-Prozess. Keine Datei anlegen.');
}
Zum Kapitel springen Erklärung der Code-Snippets
- appName:
Der Name der Anwendung, hierapp
. Korrespondierend zurpm2-config.json
- mainProcessName:
Der Name des Hauptprozesses, der in der PM2-Konfigurationsdatei definiert ist:app_main
- currentProcessName:
Der aktuelle Prozessname, der entweder aus der Umgebungsvariableprocess.env.name
stammt, oder standardmäßig gleich demmainProcessName
ist, falls nicht verfügbar. - isMainProcess:
Eine Funktion, die überprüft, ob der aktuelle Prozess der Hauptprozess ist.
Wenn PM2 den Cluster startet, setzt es die process.env.name
Variable basierend auf der Konfigurationsdatei. Dadurch kann der Code feststellen, ob er im Hauptprozess oder einem Subprozess läuft, und entsprechend handeln. In diesem Fall wird die Datei dann eben nur im Hauptprozess erstellt.
.
├── main.js
├── main_process_file_rbml5edm8v.txt
└── pm2-config.json
Einer Ausdehnung auf alle Prozessorkerne steht mit damit nichts mehr im Wege. Wir können mit "instances": "-1"
alle Kerne - 1 verwenden. So läuft der Hauptprozess auf einem Kern und die Sub-Prozesse nutzen dann alle restlich verfügbaren Kerne.
[
{
"name": "app_main",
"script": "./main.js",
"exec_mode": "cluster",
"instances": "1"
},
{
"name": "app_sub",
"script": "./main.js",
"exec_mode": "cluster",
"instances": "-1"
}
]
[PM2][WARN] Applications app_main, app_sub not running, starting...
[PM2] App [app_main] launched (1 instances)
[PM2] App [app_sub] launched (11 instances)
┌────┬─────────────┬─────────────┬─────────┬─────────┬──────────┬────────┬──────┬───────────┬──────────┬──────────┬──────────┬──────────┐
│ id │ name │ namespace │ version │ mode │ pid │ uptime │ ↺ │ status │ cpu │ mem │ user │ watching │
├────┼─────────────┼─────────────┼─────────┼─────────┼──────────┼────────┼──────┼───────────┼──────────┼──────────┼──────────┼──────────┤
│ 0 │ app_main │ default │ N/A │ cluster │ 18923 │ 0s │ 0 │ online │ 12% │ 58.2mb │ coo… │ disabled │
│ 1 │ app_sub │ default │ N/A │ cluster │ 18924 │ 0s │ 0 │ online │ 11% │ 57.5mb │ coo… │ disabled │
│ 2 │ app_sub │ default │ N/A │ cluster │ 18925 │ 0s │ 0 │ online │ 11% │ 56.9mb │ coo… │ disabled │
│ 3 │ app_sub │ default │ N/A │ cluster │ 18926 │ 0s │ 0 │ online │ 11% │ 57.3mb │ coo… │ disabled │
│ 4 │ app_sub │ default │ N/A │ cluster │ 18927 │ 0s │ 0 │ online │ 11% │ 57.4mb │ coo… │ disabled │
│ 5 │ app_sub │ default │ N/A │ cluster │ 18928 │ 0s │ 0 │ online │ 11% │ 57.4mb │ coo… │ disabled │
│ 6 │ app_sub │ default │ N/A │ cluster │ 18929 │ 0s │ 0 │ online │ 11% │ 57.2mb │ coo… │ disabled │
│ 7 │ app_sub │ default │ N/A │ cluster │ 18930 │ 0s │ 0 │ online │ 11% │ 57.8mb │ coo… │ disabled │
│ 8 │ app_sub │ default │ N/A │ cluster │ 18931 │ 0s │ 0 │ online │ 11% │ 57.4mb │ coo… │ disabled │
│ 9 │ app_sub │ default │ N/A │ cluster │ 18932 │ 0s │ 0 │ online │ 0% │ 57.3mb │ coo… │ disabled │
│ 10 │ app_sub │ default │ N/A │ cluster │ 18933 │ 0s │ 0 │ online │ 0% │ 51.9mb │ coo… │ disabled │
│ 11 │ app_sub │ default │ N/A │ cluster │ 18934 │ 0s │ 0 │ online │ 0% │ 34.9mb │ coo… │ disabled │
└────┴─────────────┴─────────────┴─────────┴─────────┴──────────┴────────┴──────┴───────────┴──────────┴──────────┴──────────┴──────────┘
Dabei werden durch die Logik in unserer main.js
gewisse Aufgaben weiterhin nur auf dem Main-Prozess ausgeführt - wir erhalten weiterhin nur eine Datei auch wenn unsere Anwendung nun 12 Kerne nutzt.
.
├── main.js
├── main_process_file_cawg3j1sjmf.txt
└── pm2-config.json
Zum Kapitel springen Schlusswort
Das Clustering von Node.js-Anwendungen mit PM2 bietet eine einfache Möglichkeit, die Leistung und Verfügbarkeit zu verbessern. Durch die Nutzung aller verfügbaren CPU-Kerne und die Verteilung der Last auf mehrere Prozesse wird die Anwendung nicht nur schneller, sondern auch ausfallsicherer. Die Unterscheidung zwischen Main- und Sub-Prozessen ermöglicht es, bestimmte Aufgaben gezielt und effizient auszuführen. PM2 bietet dabei eine benutzerfreundliche und leistungsstarke Lösung, um Node.js-Anwendungen zu managen und zu überwachen.