Erkennen des wahren Engpasses bei einem langsamen Node.js-Endpunkt
Lernen Sie eine systematische Methode, um die Latenz im Backend entlang des Anfragenpfades zu verfolgen – vom Node.js-Code bis hin zu Datenbankabfragen – unter Verwendung von Zeitmessung und EXPLAIN ANALYZE.
Ein Backend-Endpunkt wirkt träge, und die reflexhafte Reaktion ist fast immer dieselbe:
"Node.js ist langsam."
Das war früher auch Ihre Standarderklärung.
Dann, nachdem man Zeit damit verbracht hat, echte Leistungsprobleme aufzuspüren, ergab sich eine andere Erkenntnis:
Der Ort, an dem die Trägheit auftritt, ist nicht automatisch der Ort, der sie verursacht.
Ihre Node.js-Dienst kann genau so laufen, wie es vorgesehen ist, während die eigentliche Verzögerung von der Datenbank, einer Drittanbieter-API, dem Netzwerk oder einer einzigen schlecht optimierten Abfrage stammt, die irgendwo im Anfragenpfad versteckt ist.
Nachfolgend finden Sie die Methode, die immer dann angewandt werden sollte, wenn ein Backend-Endpunkt unangemessen langsam wirkt.
1. Beginnen Sie mit dem eigentlichen Problem
Stellen Sie sich eine Route wie folgt vor:
GET /api/users?email=user@example.com
Die Antwort ist korrekt.
Aber es dauert konstant etwa 2 bis 3 Sekunden.
Die erste Reaktion beinhaltet in der Regel Ideen wie:
- Den Node.js-Code anpassen
- Eine Caching-Schicht einfügen
- Den Server ausbauen
- Weitere Instanzen starten
- Abschnitte des JavaScripts umschreiben
Nichts davon ist bisher durch Beweise gestützt – es handelt sich um Spekulationen.
Was Sie eigentlich zuerst fragen sollten ist:
Wohin geht die Zeit eigentlich?
2. Messen, bevor man etwas ändert
Anstatt direkt zu Codeänderungen überzugehen, messen Sie zunächst jeden einzelnen Schritt.
Zum Beispiel:
console.time("getUsers");
const users = await getUsers();console.timeEnd("getUsers");
Falls das ausgegebene Ergebnis ist:
getUsers: 2720ms
dann sagt Ihnen diese eine Zahl bereits etwas Nützliches.
Das Engpassproblem liegt wahrscheinlich nicht im HTTP-Layer selbst.
Es findet irgendwo innerhalb von getUsers() statt.
Es ist an der Zeit, noch einen Schritt tiefer zu gehen.
3. Die Datenbankabfrage messen
Nehmen wir an, die zugrunde liegende Funktion sieht so aus:
console.time("db-query");
const users = await prisma.user.findMany({
where: {
email: email
}
});console.timeEnd("db-query");
Und die Zeitmessung ergibt Folgendes:
db-query: 2680ms
Das eingrenzt die Suche erheblich.
Node.js ist es nicht, das 2,6 Sekunden zum Verarbeiten der Anfrage benötigt.
Es ist der Aufruf an die Datenbank.
Deshalb kann es sich lohnen, nicht direkt auf Einstellungen auf Anwendungsseite zu setzen – sonst wird Zeit verschwendet, die man nicht wieder zurückgewinnt.
4. Fragen Sie nun PostgreSQL, was es tut
Genau in diesem Moment kommt EXPLAIN ANALYZE ins Spiel.
Zum Beispiel:
EXPLAIN ANALYZE
SELECT *
FROM users
WHERE email = 'user@example.com';
Die Ausgabe könnte etwas in dieser Richtung zeigen:
Seq Scan on users
(actual time=0.025..2720.532 rows=1)
Der entscheidende Punkt, den man beachten muss, ist:
Seq Scan
PostgreSQL durchläuft die gesamte Tabelle Zeile für Zeile, anstatt direkt über einen Index zum entsprechenden Datensatz zu springen.
Sobald die Tabelle groß genug wird, wird diese sequentielle Suche äußerst aufwendig.
5. Die Lösung liegt nicht in „Optimieren von Node.js“
Falls Suchanfragen nach email häufig stattfinden, kann das Hinzufügen eines geeigneten Index die Kosten der Abfrage erheblich senken.
Zum Beispiel:
CREATE INDEX idx_users_email
ON users(email);
Führen Sie anschließend dieselbe Abfrage erneut aus:
EXPLAIN ANALYZE
SELECT *
FROM users
WHERE email = 'user@example.com';
Der Ausführungsplan sollte nun etwas Ähnliches zeigen:
Index Scan using idx_users_email
anstatt:
Seq Scan
Die genaue Millisekundenangabe ist hier eigentlich nicht entscheidend.
Wichtig ist, dass die Ausführungsstrategie auf Grundlage messbarer Daten geändert wurde, nicht aufgrund einer Vermutung.
6. Die wichtigere Lektion
Im Grunde hat das alles nichts mit PostgreSQL zu tun.
Es handelt sich um eine Lektion darüber, wie man systematisch debuggt.
Wenn eine Anfrage Zeit in Anspruch nimmt, widerstehen Sie dem Drang, sofort das Framework zu beschuldigen.
Bilden Sie sich den vollständigen Pfad vor, den eine Anfrage zurücklegt:
Client
↓
HTTP
↓
Node.js
↓
Business Logic
↓
Redis / Database / External API
↓
Response
Jede einzelne Schicht in dieser Kette könnte der eigentliche Engpass sein.
Ihre Aufgabe ist es, genau herauszufinden, welche das ist.
7. Ein einfacher Debugging-Prozess
Immer dann, wenn ein Endpunkt langsam ist, sollte man im Großen und Ganzen dieser Ablauf folgen.
Schritt 1 – Die gesamte Anfrage messen
Request: 2.8
Schritt 2 – Die Anfrage in Komponenten aufteilen
Authentication: 20ms
Business logic: 50ms
Database: 2.6s
Response serialization: 15ms
Sobald Sie diese Aufteilung haben, wird es viel einfacher, den Schuldigen ausfindig zu machen.
Schritt 3 – Den langsamsten Komponenten nachgehen
Falls der Datenbank-Anteil 2,6 Sekunden in Anspruch nimmt:
Verschwenden Sie keine Stunde damit, JavaScript feinzujustieren.
Gehen Sie direkt zur Datenbank.
Schritt 4 – Die Abfrage überprüfen
Betrachten Sie genau:
EXPLAIN ANALYZE
Achten Sie außerdem auf:
- Sequenzielle Durchsuchungen
- Ob tatsächlich Indizes verwendet werden
- Verschachtelte Abfragen
- Sortiervorgänge
- Filterbedingungen
- Wie viele Zeilen durchsucht werden
- Wie viele Zeilen tatsächlich zurückgegeben werden
Schritt 5 – Etwas korrigieren
Mögliche Korrekturen:
- Einen geeigneten Index hinzufügen
- Eine ineffizient strukturierte Abfrage umschreiben
- Eine nicht benötigte verschachtelte Abfrage entfernen
- Ein N+1-Abframuster beseitigen
- Unnötig heruntergeladene Daten reduzieren
Schritt 6 – Erneut messen
Glauben Sie niemals einfach, dass Ihre Korrektur funktioniert hat.
Bestätigen Sie dies durch eine erneute Messung.
8. Vergessen Sie nicht externe APIs
Die Verzögerung liegt nicht immer in der Datenbank.
Betrachten Sie folgendes Szenario:
const user = await getUser();
const payment = await getPaymentDetails();const orders = await getOrders();return {
user,
payment,
orders
};
Falls jeder einzelne Aufruf folgende Zeit in Anspruch nimmt:
getUser() → 100ms
getPaymentDetails() → 900ms
getOrders() → 700ms
dann ist die Leistung des Endpunkts weitaus langsamer, als sie sein müsste.
Und hier lautet die Lösung nicht „Node.js schneller laufen lassen.“
Es könnte einfach darum gehen, die Ausführung unabhängiger Aufrufe zu ändern.
Zum Beispiel:
const [user, payment, orders] = await Promise.all([
getUser(),
getPaymentDetails(),
getOrders()
]);
Auf diese Weise werden Operationen, die voneinander unabhängig sind, gleichzeitig ausgeführt anstatt nacheinander.
Doch es gibt hier eine wichtige Einschränkung:
Verwenden SiePromise.all()nicht, ohne es gründlich durchzudenken.
Wenn Operationen voneinander abhängig sind, Konkurrenzbeschränkungen erforderlich sind oder das Risiko besteht, einen nachgelagerten Dienst zu überlasten, kann die parallele Ausführung aller Aufgaben die Situation tatsächlich verschlimmern anstatt verbessern.
Die richtige Leistungsverbesserung hängt immer von der konkreten Arbeitslast ab, mit der man es zu tun hat.
9. Achten Sie auf N+1-Anfragen
Es gibt noch eine weitere Leistungsfallstrick, der auf den ersten Blick harmlos erscheint.
Nehmen wir folgendes Beispiel:
const users = await getUsers();
for (const user of users) {
user.orders = await getOrders(user.id);
}
Falls Sie mit 100 Benutzern arbeiten, erzeugt dieses Muster heimlich:
1 query → get users
100 queries → get orders
Das führt dazu, dass für einen einzigen API-Aufruf bis zu 101 separate Datenbankabfragen notwendig sind.
Das ist das bekannte N+1-Anfrage-Problem.
Je nach Situation können bessere Strategien sein:
- Die Tabellen direkt zu verknüpfen
- Sich auf die Beziehungs-Ladefunktionen eines ORMs zu verlassen
- Daten in Batches statt einzeln abzurufen
- Eine
WHERE IN-Klausel zu verwenden - Die Struktur der Antwort neu zu überdenken
Wie immer hängt die Wahl des Ansatzes vollständig von der Arbeitslast ab.
10. Erhöhen Sie die Servergröße nicht zu früh
Eine häufige instinktive Reaktion auf eine langsame API ist:
">Lassen Sie uns mehr CPU und RAM einsetzen."
Das kann in manchen Fällen helfen.
Oft verbringen Sie jedoch nur mehr Geld, ohne das eigentliche Problem anzugehen.
Falls eine Datenbankabfrage schlecht geschrieben ist, macht das Aufstocken des Node.js-Servers die Abfrage nicht schneller.
Vor dem Aufrüsten der Hardware fragen Sie sich bitte:
Ist die Anwendung tatsächlich durch CPU oder Speicher eingeschränkt?
Falls nicht, wird das Hinzufügen von Serverkapazität wahrscheinlich das eigentliche Engpassproblem nicht beheben.
11. Die Denkweise beim Debuggen, derer ich mich bemühe
Ein nützliches Denkmodell für die Bewältigung von Langsamkeiten im Backend sieht so aus:
Is the API slow?
↓
Measure it
↓
Which layer is slow?
↓
Measure that layer
↓
Find the actual bottleneck
↓
Make one change
↓
Measure again
Anstatt:
API slow
↓
Optimize Node.js
↓
Add Redis
↓
Increase server
↓
Hope it gets faster
Der zweite Ansatz ist reine Spekulation.
Der erste Ansatz basiert auf echter Ingenieursarbeit.
12. Meine Überprülliste für die Leistung des Backends
Vor jeder Optimierung lohnt es sich, Folgendes zu überprüfen:
- Wie hoch ist die tatsächliche End-to-End-Antwortzeit?
- Ist die Arbeitslast durch den CPU-Beschränkungsfaktor beeinträchtigt?
- Ist die Datenbankabfrage an sich langsam?
- Gibt es irgendwo ein N+1-Muster?
- Sind die richtigen Indizes vorhanden?
- Was zeigt
EXPLAIN ANALYZEauf? - Fügt eine Drittanbieter-API Verzögerungen hinzu?
- Werden unabhängige Aufgaben sequenziell ausgeführt, obwohl das nicht notwendig ist?
- Lädt der Code mehr Daten herunter, als er tatsächlich benötigt?
- wird Redis dort genutzt, wo es sinnvoll ist?
- Ist der Connection Pool richtig konfiguriert?
- Hat die von Ihnen vorgenommene Änderung tatsächlich zu einer Verbesserung der gemessenen Werte geführt?
Letzter Gedanke
Eine der wertvollsten Lektionen bei der Arbeit am Backend lautet:
Leistungsprobleme werden selten durch Raten gelöst.
Eine langsame Node.js-API bedeutet nicht automatisch, dass Node.js selbst schuld ist.
Der eigentliche Übeltäter könnte sein:
PostgreSQL
Redis
External APIs
Network
N+1 queries
Poor indexes
Serialization
Connection pools
Application logic
Es ist nicht die Kernfähigkeit, zu wissen, wie man jede dieser Technologien einzeln optimiert.
Die eigentliche Fähigkeit besteht darin, genau herauszufinden, wo sich das Engpassproblem tatsächlich befindet.
Sobald man genau weiß, wo die Zeit verschwindet, folgt in der Regel die Lösung von selbst.
Messen zuerst. Den Engpass finden. Den Engpass beheben. Wieder messen.
Das ist der Ansatz, der heute bei der Bewältigung von Backend-Leistungproblemen angewandt wird.
Zusätzliche Literatur
- Erstellung einer Fehlerbehandlung von Produktionsqualität in Node.js-Anwendungen — Erfahren Sie, wie Sie Fehler in Node.js klassifizieren, eine benutzerdefinierte Fehlerhierarchie entwerfen, die asynchrone Fehlerbehandlung zentralisieren und Stack-Traces schützen, um die Zuverlässigkeit in der Produktion zu gewährleisten.
- 20 Node.js-Muster, die Ausfälle von Produktionsservern verhindern — Lernen Sie 20 praktische Node.js-Muster – von der Fehlerbehandlung über einen reibungslosen Herunterfahren bis hin zum Connection Pooling –, die Abstürze verhindern, bevor ein Neustart notwendig wird.