Die Wahrheit über Skalierung und Lastverteilung mit Node.js

Hast du eine Single-Pages-Application (SPA) mit Node.js im Back-End, aber hast so einen immensen Datenverkehr, dass du nicht weißt wie es weitergeht? Bevor du noch weitere teure und hochperformante Instanzen aufsetzt, stelle sicher, dass du all deine Ressourcen richtig einsetzt.

Grundlagen

Node.js ist eine Plattform, die das Bilden von schnellen und skalierbaren Web-Anwendungen ermöglicht. Bei Node.js werden nicht-blockierende event-gesteuerte Eingaben und Ausgaben (IO) unterstützt. In anderen Worten, Node.js läuft in einem einzigen Thread. Nicht-blockierende Eingabe und Ausgabe bedeutet, dass gleichzeitig auftretende Verbindungen in einer einzigen Ereignisschleife landen.

Daraus folgt, dass Nodej.js  am besten bei Anwendungsfällen, bei denen Daten fast in Echtzeit mittels Websockets übertragen werden, eingesetzt wird. Insbesondere gilt dies, wenn sehr viele Verbindungen gleichzeitig auftreten können. Dieser Blogeintrag beweist, dass node.js hierbei besonders gut abschneidet. Anders sieht es bei Anwendungsfällen aus, bei denen komplizierte Berechnungen stattfinden, wie zum Beispiel beim Errechnen der Fibonacci Reihe. Diese umfassenden Berechnungen blockieren nämlich – sofern sie nicht an andere Prozesse delegiert werden – die Node.js-Anwendung. Das bedeutet, dass während der Berechnung alle Verbindungen aus der Warteschlange nicht bedient werden können. Nun wissen wir für welche Anwendungsfälle Node.js geeignet ist.

Vertikale Skalierung bei Node.js

Bekanntlich kann die Leistung eines Systems durch zwei Wege gesteigert werden: Vertikal oder Horizontal. Lasst uns mit der einfachen Variante, der vertikalen Skalierung, beginnenWie dir vielleicht bewusst ist, ist Node.js leider nicht in der Lage „out-of-the-box“ mehr als einen Prozessor zu nutzen, da kein Multithreading unterstützt wird. Node.js basiert auf Googles JavaScript Engine V8, welche eine hart-programmierte 1.5 Gigabyte Arbeitsreicher Obergrenze hat. Dies macht es sehr schwierig dein 64-Kern Endgerät mit 32 Gigabyte Arbeitsspeicher vernünftig zu nutzen. Zum Glück ist es aber möglich mit Hilfe der Node.js Cluster-Api mehrere untergeordnete (aber nebenläufige) Instanzen zu erzeugen. Dies geschieht entweder direkt über die API von Node.js , oder – wie ich empfehle – über eine Manager-Bibliothek wie Throng. Denn bei Throng handelt es sich um eine einfache Bibliothek, die deinem Code keine unnötige Abstraktion hinzufügt. Somit bist du in der Lage die Ressourcen bestmöglichst auszuschöpfen. Sobald allerdings der Datenverkehr sehr groß wird, wird das Bündeln mehrerer Server oder das Nutzen einer Virtuellen Maschine unumgänglich.

Horizontale Skalierung bei Node.js

Die horizontale Skalierung ist aus architektonischer Sicht viel interessanter. Es wird empfohlen nach dem 3-Schichten-Modell vorzugehen. Eine Schicht wird hierbei zur Lastverteilung genutzt, die mittlere Schicht zum Ausführen von Node.js und es gibt eine Nachrichten-Schicht, die alles verknüpft.

Die Lastverteilungs-Schicht, verteilt die eingehenden Anfragen an alle parallel arbeitenden Systeme. Der Knotenpunkt in der Mitte ist für die Logik deiner Anwendung verantwortlich. Die Nachrichten-Schicht koordiniert alle Knotenpunkte und ermöglicht das Ausführen von Knoten-übergreifenden Berechnungen.

Welche Lastverteilungs-Lösung ist die Richtige für dich?

Es gibt eine Reihe von bekannten Alternativen zu Webservern mit eingebauten Lastverteilungs-Funktionen. Es gibt unter anderem Nginx und apache. Eine weitere Lösung ist HAProxy, dieser ist zwar kein vollständiger Server, ist aber genau für diesen Zweck bestimmt. Nicht zu vergessen ist natürlich die „Do-It-Yourself“-Variante: Das Verwenden einer weiteren Node.js-Anwendung, um das Verteilen der Last zu organisieren. Bei letzterer Variante bietet sich  node-http-proxy sehr gut an. Wenn dein Datenverkehr mit SSL verschlüsselt werden muss, dann beachte dies an dieser Stelle. Denn, wie du es dir vorstellen kannst, ist SSL-Handling unter gewissen Umständen sehr teuer. Im allgemein gilt bei verschlüsseltem Datenverkehr: “Terminate early

Nachrichten-Schicht: die Qual der Wahl

Diese Schicht verbindet und koordiniert alle Node.js-Knotenpunkte. Es gibt hier viele hochperformante Lösungen (etwa RabbitMQ , ZeroMQ oder Redis). Die Besonderheit bei Redis besteht nicht nur darin, dass der Einstig sehr leicht fällt, sondern auch dass der größte Teil der Abfragen in-Memory stattfindet (und die Festplatte nur zum Zwecke der Datenpersistenz verwendet wird).

Selbstverständlich wurden hierbei viele Annahmen getroffen. Dein spezieller Anwendungsfall sieht sicherlich anders aus. Der Vorteil bei der gemeinsamen Verwendung von Node.js und Redis.js liegt allerdings in der Einfachheit der Lösung. Selbst wenn sich die Komplexität deiner Software erhöht, ist der Umfang dieser Lösung gleichbleibend.

Happy Scaling!

Hast du Erfahrung mit dieser Problematik? Was hältst du von der hier vorgeschlagenen Lösung? Lass es mich in den Kommentaren wissen!

Advertisements