Node.JS Software Cluster mit PM2 - Bessere Performance durch Multicore

von cooper.bin
veröffentlicht am 27.07.2024 aktualisiert am 27.07.2024

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.

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, hier app. Korrespondierend zur pm2-config.json
  • mainProcessName:
    Der Name des Hauptprozesses, der in der PM2-Konfigurationsdatei definiert ist: app_main
  • currentProcessName:
    Der aktuelle Prozessname, der entweder aus der Umgebungsvariable process.env.name stammt, oder standardmäßig gleich dem mainProcessName 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.


cooper.bin Avatar

cooper.bin

Unterstütze mich und meine Arbeit, so kann ich weiter meiner Leidenschaft nachgehen. Ich lege viel Wert auf Qualität und stecke daher sehr viel Zeit in meine Beiträge. Wenn sie dir gefallen kannst du dir gerne auch meine anderen Artikel anschauen.

Mit PayPal unterstützen

Ich bin auf dem makesmart Discord-Server aktiv. Dort bin ich auch relativ gut erreichbar.

Teile diesen Beitrag



Diese Artikel könnten dich auch interessieren

NeDB: Eine MongoDB-ähnliche dateibasierte Datenbank für Javascript

Mit NeDB erhältst du eine dateibasierte und lokale NoSQL-Datenbank. Ihre Ähnlichkeit mit MongoDB eröffnet zudem flexible Abfragemöglichkeiten für dein Node.js-Projekt.

cooper.bin am 30.03.2024

iCloud: Fotos und Videos anfordern, sortieren und Alben wiederherstellen

Erstelle ein Backup deiner iCloud-Fotos und -Videos, sortiere sie mit dem Tool Local iCloud Media Sort und stelle dabei alle Alben wieder her. Geeignet für jedes System.

cooper.bin am 11.04.2024

Discord WebHook Tutorial - Nachrichten automatisiert versenden

Sende und bearbeite einfach und automatisiert Nachrichten mit Discord Webhooks. Perfekt für die Einbindung von Benachrichtigungen und Drittanbieterdiensten.

cooper.bin am 05.04.2024

TLS-Sicherheit für MongoDB: Umfassende Anleitung zur Verschlüsselung

Lerne, wie du MongoDB mit TLS aktiv und effektiv sicherst. Dein Guide zur Absicherung und praktischen Umsetzung mit allen notwendigen Schritten.

cooper.bin am 02.04.2024

MongoDB installieren - Die NoSQL Datenbank auf dem eigenen Server

MongoDB kann neben einer managed Lösung auch einfach und schnell in wenigen Schritten selbst installiert werden. In diesem Tutorial erfährst du wie du MongoDB auf deinem Server installierst

cooper.bin am 01.04.2024