Wie viele Anfragen kann ein Node.js-Server wirklich verarbeiten?
Wie viele Anfragen kann ein Node.js-Server wirklich verarbeiten? Express-Lasttests auf Heroku und AWS im Vergleich zu Django, eine Schätzung der Kapazität von 40.000 Anfragen pro Sekunde sowie Faktoren, die APIs verlangsamen.
Quellen: How Many Requests Can a Real-World Node.js Server-Side App Handle?
Jeder, der neu mit Node.js arbeitet, stellt irgendwann dieselbe praktische Frage: Wie viele Anfragen kann eine Produktiv-Node.js-Anwendung verarbeiten, und wie viele Serverinstanzen sind für einen bestimmten Trafficbedarf erforderlich? Die Antwort hängt von weitaus mehr Faktoren ab als nur vom Framework.
Ein Ziel, das es zu erreichen gilt
Laut Angaben dient die Design-Plattform PicsArt mit einem hauptsächlich aus Node.js bestehenden Backend etwa 40.000 Anfragen pro Sekunde ab.
Diese Zahl ist enorm und hängt von zahlreichen Faktoren ab, die über den Backend-Server hinausgehen. Für diese Übung werden diese anderen Faktoren beiseitegelassen, sodass nur das Backend-Hardware und -Software sowie insbesondere Node.js berücksichtigt werden.
Was synthetische Benchmarks zeigen
Das Team hinter Fastify veröffentlichte einen Benchmark, bei dem Express etwa 15.000 Anfragen pro Sekunde verarbeiten kann, während das reine Node HTTP-Modul etwa 70.000 erreicht.
Ein 2015er MacBook Pro 15“ zeigte fast identische Werte. Selbst 15.000 Anfragen pro Sekunde mit Express sind hervorragend, doch solche Benchmarks laufen gegen einen winzigen Handler, der bei weitem kein echtes API ist. Was ein API „realwelttauglich“ macht, sind die Daten, ihre Übertragung sowie die Wechselwirkung der Komponenten – daher muss ein realistischer Handler mit einer Datenbank oder anderen Diensten kommunizieren.
Der Unterschied zwischen dem Roh-Code und Express ist ohnehin nicht besonders wichtig. Etablierte Frameworks wie Express sind in der Regel die bessere Wahl, denn in der Praxis wird keine einzelne Instanz 100.000 Anfragen pro Sekunde bewältigen können. Deshalb wurde für diese Tests Express installiert, wobei ein kleiner Handler verschiedene Operationen ausführt.
Der Test-Handler
Der Handler führt sechs Schritte aus:
- Einen neuen Benutzer in die Datenbank einfügen.
- 20 Benutzer aus der Datenbank abrufen.
- Die Liste der 20 Benutzer serialisieren und wieder parsen.
- Übersetzen Sie diese 20 Benutzerobjekte rekursiv in camelCase.
- Sortieren Sie das Array.
- Führen Sie leichte Berechnungen sowie Ausgabeoperationen durch, wie z. B.
Date.nowundconsole.log, die ebenfalls Rechenleistung verbrauchen.
Die Antwort wog etwa 12 KB. Jedes Benutzerobjekt hatte eine Größe von 609 Bytes:
Ergebnis 1: Node.js auf Heroku mit AWS RDS
Die Anwendung wurde auf Heroku auf Hardware mit einem Monatspreis von 25 Dollar bereitgestellt, und die Anfragen wurden mit der höchstmöglichen Rate gesendet, ohne die durchschnittliche Latenz zu erhöhen. Die Instanz verarbeitete 31.000 Anfragen innerhalb von 60 Sekunden, was einem Durchschnitt von 515 Anfragen pro Sekunde entspricht.
Ergebnis 2: Django auf Heroku mit AWS RDS
Um zu sehen, was eine nicht-einfachthreadige Technologie leisten würde, wurde unter identischen Bedingungen in Django ein äquivalenter Handler entwickelt und auf demselben Server bereitgestellt. Er erzielte etwas schlechtere Ergebnisse als Node.js: 23.000 Anfragen in 60 Sekunden, also 389 Anfragen pro Sekunde.
Ergebnis 3: drei Node.js-Instanzen auf AWS Elastic Beanstalk
Die dritte Konfiguration simulierte eine reale Produktivumgebung auf AWS. Sowohl die Datenbank als auch der Backend-Server befanden sich in Frankfurt (eu-central-1), der nächstgelegenen Region, etwa 3.000 km vom Tester entfernt. Drei Node.js-Instanzen standen hinter dem AWS-Lastverteilungssystem. Sie bewältigten 19.000 Anfragen in 10 Sekunden, was 1.858 Anfragen pro Sekunde entspricht. Ein kontinuierlicher Lasttest führte anschließend innerhalb von 13 Minuten zu 1,5 Millionen erfolgreichen Anfragen.
Skalierung auf 40.000 Anfragen pro Sekunde
Ein Instanz behandelt etwa 600 Anfragen pro Sekunde – wie viele wären dann für 40.000 Anfragen nötig? Nehmen wir an, dass ein durchschnittlicher Endpunkt auf einer großen Plattform doppelt so ressourcenintensiv ist wie der Test-Handler. Das ergibt 40.000 / (600 / 2) = 133 Instanzen weltweit für eine globale Anwendung im „Unicorn“-Maßstab. Das ist keine große Herausforderung.
Intensivere Endpunkte könnten bis zu dreimal mehr Ressourcen erfordern, doch Caching und ereignisbasierte Kommunikation können den Durchsatz wieder ausgleichen – daher ist eine Verdopplung eine angemessene Mittelwertschätzung.
Was eine API verlangsamt
Unzählige Faktoren können die Geschwindigkeit einer API verringern. Hier sind die leistungsbezogenen Faktoren, die in den meisten serverseitigen Webanwendungen vorkommen.
Datenmenge
Das in der Datenbank gespeicherte oder daraus abgerufene Volumen ist wichtig, insbesondere bei relationellen Datenbanken, wo große Datenmengen oft zu komplexen Abfragen oder zu aufwendigen Prozessen zwischen dem DBMS und seinem Festplattenspeicher führen. In jedem Fall bedeutet mehr Speicherplatz längere Übertragungszeiten. Das Speichern von Dateien in der Datenbank ist ein schlechtes Muster – objektorientiertes Speichern eignet sich besser dafür. Idealerweise bleibt eine Datenspur bei einigen Dutzend Kilobyte, in der Regel bei 1 bis 20 KB.
Entfernung zur Datenbank
Eine größere Entfernung zwischen dem Backend und der Datenbank bedeutet längere Übertragungszeiten für Anfragen und Ergebnisse. Nehmen wir zwei Backends – eines in Virginia und eines in Frankfurt – mit einer einzigen Datenbank in Virginia. Die Anfrage eines US-Benutzers erreicht das US-Backend in etwa 10 bis 40 ms, und die Übertragung vom Backend zur Datenbank dauert 1 bis 5 ms. Das ist in Ordnung. Ein deutscher Benutzer erreicht das Frankfurt-Backend in 10 bis 20 ms, doch die Anfrage nach Virginia benötigt 120 bis 130 ms. Eine Anwendung für eine einzige Region sollte daher Daten und Backend-Logik in der Nähe voneinander halten, während eine globale Anwendung auf komplexe ingenieurtechnische Lösungen angewiesen ist, um weiterhin schnell zu arbeiten.
Datenbank-Hardware
Auch die Hardware hat einen großen Einfluss. Eine kleine Anwendung sollte bessere Hardware kaufen, um ihrem Datenverkehr gerecht zu werden; andernfalls ist wiederum komplexe Ingenieurarbeit erforderlich.
Datenbank-Konfiguration
Datenbanken werden mit Standardeinstellungen ausgeliefert, sodass sie kein weiteres Problem bei der Verwaltung darstellen, doch in großem Maßstab erfordern sie Aufmerksamkeit. Die maximale Anzahl der Verbindungen ist eine Einstellung, auf die man achten muss. Sie kann die Anwendung verlangsamen oder den Datenbankserver überlasten, und Client-Bibliotheken wie ORMs ändern sie oft. Der Verbindungszeitraum ist ein weiterer wichtiger Faktor.
Anzahl der Datenbankaufrufe
Eine echte API kann Dutzende separater Datenbankaufrufe benötigen, um eine Aufgabe abzuschließen, und sie kann auch auf andere Dienste zugreifen, die jeweils wiederum Dutzende von Aufrufen tätigen. Ein bemerkenswertes Beispiel war eine API eines Bankmonolithen mit einer Reaktionszeit von 9 Sekunden in der Cloud-Entwicklungsumgebung. Ihr Handler führte 300 Zeilen aus, wobei etwa 30 Datenbankaufrufe und rund 10 interne Dienstanfragen erfolgten. Dies tritt häufig bei einem „Code-zuerst“-Ansatz und nicht bei einem „Design-zuerst“-Ansatz auf.
Die Lösungen hängen von der Geschäftslogik ab. Warteschlangen und andere asynchrone Techniken können einige Datenbankaufgaben in den Hintergrund verlegen, was eine Aufteilung großer Dienste in kleinere erfordert. Auch die Kombination von Abfragen kann die Anzahl der Aufrufe verringern.
Hooks
Beliebte ODMs und ORMs bieten Hooks oder Middleware an, die sich gut zum Handhaben bestimmter Logiken eignen. Bei Massenoperationen können sie jedoch zu einem versteckten Engpass werden, da auch Plugins wie Konverter für camelCase oder pascal_case Rechenleistung und Zeit in Anspruch nehmen. Hooks helfen zwar, doch was darin geschrieben ist und wo, verdient eine weitere Überlegung.
Rechenintensive Aufgaben
Rechenintensive Aufgaben verlangsamen den Server und lassen den Endbenutzer warten. In Node.js sind viele davon auf den Threadpool angewiesen. Das blockiert zwar die asynchrone Eingabe/Ausgabe, verlangsamt die API dennoch. Beispielsweise muss beim Klicken zur Registrierung auf den Threadpool gewartet werden, um ein Passwort zu hashen, was mehr als 100 Millisekunden dauert.
Engpässe im Event-Loop
Node.js führt Code im Event-Loop auf einem einzigen Thread aus, wodurch die Leistung des Codes die gesamte Anwendung beeinflusst. JSON.parse und JSON.stringify können APIs verlangsamen, und verschachtelte Schleifen sowie rekursive Funktionen sind problematisch. Wenn man die Funktionsweise von Node.js ignoriert, kann die Verfügbarkeit der Anwendung je nach Datenmenge und Traffic beeinträchtigt werden.
Schlechtes Infrastrukturdesign
Große Infrastrukturen weisen nicht nur mehr Probleme auf als Monolithen, sondern auch andere Arten von Problemen, die oft komplexe architektonische Lösungen erfordern. Bei vielen Komponenten beeinträchtigt eine schlechte Architektur stark die Geschwindigkeit der API und macht das System ressourcenschwach, wodurch man mehr bezahlt für etwas, das eigentlich günstiger sein könnte. Bei der Entwicklung komplexer Anwendungen sollte man darüber nachdenken, welche Probleme eine Technologie löst, und die Alternativen kennen. Man sollte berücksichtigen, wie sich die Anforderungen in zwei oder sechs Monaten ändern könnten, und jede Entscheidung anhand dieser Annahmen testen. Zudem sollten SLAs sowie nicht-funktionale Anforderungen berücksichtigt werden, einschließlich der Frage, was passiert, wenn der Datenverkehr zunimmt.
Komponentenskalierung und Einrichtung
Komponenten, die zur Infrastruktur hinzugefügt werden, können die gesamte Plattform verlangsamen. Nehmen wir RabbitMQ – es verfügt wie jede Technologie über zahlreiche Einstellungen. Die Standardwerte eignen sich zunächst gut, doch durch Recherche und genaue Betrachtung einer bestimmten Komponente sowie eine Überprüfung der Einrichtung oder eine Anpassung der Konfiguration kann die Infrastruktur erheblich verbessert werden. Die Komponenten müssen außerdem mit der Nutzung skalieren, damit sie nicht zu Engpässen werden.