Node.js-Leistung entschlüsseln mit `perf_hooks` und `AsyncLocalStorage`
Min-jun Kim
Dev Intern · Leapcell

Einleitung
In der schnelllebigen Welt der Webentwicklung wirkt sich die Leistung von Node.js-Anwendungen direkt auf das Benutzererlebnis und den Geschäftserfolg aus. Langsame Reaktionszeiten Speicherlecks oder ineffiziente Codepfade können zu frustrierten Benutzern und Umsatzeinbußen führen. Obwohl verschiedene Tools für die allgemeine Überwachung existieren, erfordert das wirkliche Verständnis des "Warum" hinter Leistungsschwächen oft detaillierte Einblicke in die spezifische Codeausführung und die kontextbezogenen Informationen, die diese Vorgänge umgeben. Hier kommen die integrierten Module perf_hooks
und AsyncLocalStorage
von Node.js ins Spiel, die leistungsstarke, leichtgewichtige Lösungen zum Instrumentieren und Beobachten des Verhaltens Ihrer Anwendung bieten. Dieser Artikel befasst sich damit, wie diese beiden Module zusammen genutzt werden können, um eine tiefe Performance-Transparenz zu bieten und Entwicklern zu helfen, kritische Bereiche ihrer Node.js-Anwendungen zu identifizieren und zu optimieren.
Tiefgehender Einblick in die Leistungsüberwachung
Bevor wir uns mit den praktischen Anwendungen befassen, sollten wir ein klares Verständnis der Kernwerkzeuge festlegen, die wir verwenden werden:
perf_hooks
: Dieses Node.js-Modul stellt eine Implementierung der Web Performance API bereit. Es ermöglicht Ihnen, die Leistung von JavaScript-Code durch Methoden wieperformance.mark()
,performance.measure()
undperformance.now()
zu messen. Es ist äußerst nützlich, um benutzerdefinierte Leistungsmetriken zu erstellen und die Latenz bei bestimmten Vorgängen zu beobachten.AsyncLocalStorage
: Eingeführt in Node.js 12, bietetAsyncLocalStorage
eine Möglichkeit, Daten über asynchrone Operationen hinweg zum selben logischen Anforderungs- oder Ausführungskontext zu speichern und abzurufen. Stellen Sie es sich als Thread-lokalen Speicher für asynchrone Aufrufstapel vor. Dies ist entscheidend für das Verfolgen von Vorgängen und das Anhängen von kontextbezogenen Metadaten (wie einer Anforderungs-ID oder Benutzer-ID) an Leistungsmessungen, selbst wenn Vorgänge über verschiedene asynchrone Rückrufe und Promises verteilt sind.
Die Stärke der Kombination von perf_hooks
und AsyncLocalStorage
liegt in ihrer sich ergänzenden Natur. perf_hooks
sagt Ihnen, wie lange etwas gedauert hat, während AsyncLocalStorage
den Kontext bereitstellt, in dem es so lange gedauert hat. So können Sie Fragen beantworten wie: "Wie lange hat die Ausführung von getUserData
für diese spezifische Anforderung von diesem Benutzer gedauert?" ausschließlich auf der Grundlage interner Anwendungsfunktionalität.
Messung der Funktionsausführung mit perf_hooks
Beginnen wir mit einem einfachen Beispiel für die Verwendung von perf_hooks
zur Messung der Ausführungszeit einer Funktion.
const { performance, PerformanceObserver } = require('perf_hooks'); // Erstellen Sie einen PerformanceObserver, der auf 'measure'-Ereignisse wartet const obs = new PerformanceObserver((items) => { items.getEntries().forEach((entry) => { console.log(`Messung: ${entry.name} - Dauer: ${entry.duration.toFixed(2)}ms`); }); // obs.disconnect(); // Trennen, wenn Sie nur einmal beobachten möchten }); observe({ entryTypes: ['measure'], buffered: true }); function expensiveOperation(iterations) { let sum = 0; for (let i = 0; i < iterations; i++) { sum += Math.sqrt(i); } return sum; } // Markieren Sie den Beginn des Vorgangs performance.mark('startExpensiveOperation'); // Führen Sie die Funktion aus const result = expensiveOperation(10000000); // Markieren Sie das Ende des Vorgangs performance.mark('endExpensiveOperation'); // Messen Sie die Dauer zwischen den beiden Markierungen performance.measure('expensiveOperationDuration', 'startExpensiveOperation', 'endExpensiveOperation'); console.log('Operation abgeschlossen. Ergebnis:', result);
Wenn Sie diesen Code ausführen, protokolliert der PerformanceObserver
die Dauer von expensiveOperationDuration
. Dies ist ein fundamentaler Schritt, um Leistungsschwächen zu verstehen.
Hinzufügen von Kontext mit AsyncLocalStorage
Lassen Sie uns nun AsyncLocalStorage
integrieren, um unseren Leistungsmessungen kontextbezogene Informationen hinzuzufügen. Ein häufiges Szenario ist das Verfolgen einer requestId
über einen gesamten asynchronen Ablauf.
const { AsyncLocalStorage } = require('async_hooks'); const { performance, PerformanceObserver } = require('perf_hooks'); const crypto = require('crypto'); // Zum Generieren von Anforderungs-IDs const asyncLocalStorage = new AsyncLocalStorage(); // PerformanceObserver bleibt gleich const obs = new PerformanceObserver((items) => { items.getEntries().forEach((entry) => { const context = asyncLocalStorage.getStore(); const requestId = context ? context.requestId : 'N/A'; console.log(`[Request ID: ${requestId}] Messung: ${entry.name} - Dauer: ${entry.duration.toFixed(2)}ms`); }); }); observe({ entryTypes: ['measure'], buffered: true }); function simulateDatabaseCall(delay) { return new Promise(resolve => setTimeout(resolve, delay)); } async function processUserRequest(userId) { // Speichern Sie anforderungsbezogene Daten const requestId = crypto.randomUUID(); asyncLocalStorage.enterWith({ requestId, userId }); performance.mark('startProcessUserRequest'); console.log(`[Request ID: ${requestId}] Verarbeite Anforderung für Benutzer: ${userId}`); // Simulieren Sie mehrere asynchrone Schritte performance.mark('startDatabaseRead'); await simulateDatabaseCall(Math.random() * 100); // Simulation des Lesens aus der DB performance.mark('endDatabaseRead'); performance.measure('DatabaseReadDuration', 'startDatabaseRead', 'endDatabaseRead'); performance.mark('startBusinessLogic'); // Einige synchrone oder asynchrone Geschäftslogik await simulateDatabaseCall(Math.random() * 50); // Eine weitere asynchrone Operation // Der Kontext von AsyncLocalStorage ist hier noch verfügbar const currentContext = asyncLocalStorage.getStore(); console.log(`[Request ID: ${currentContext.requestId}] Ausführung der Geschäftslogik.`); performance.mark('endBusinessLogic'); performance.measure('BusinessLogicDuration', 'startBusinessLogic', 'endBusinessLogic'); performance.mark('endProcessUserRequest'); performance.measure('TotalRequestProcessing', 'startProcessUserRequest', 'endProcessUserRequest'); console.log(`[Request ID: ${requestId}] Anforderung verarbeitet.`); } // Simulieren Sie gleichzeitige Anfragen processUserRequest('user-123'); setTimeout(() => processUserRequest('user-456'), 50); setTimeout(() => processUserRequest('user-789'), 100);
In diesem erweiterten Beispiel:
- Initialisieren wir
asyncLocalStorage
. - Innerhalb von
processUserRequest
generieren wir eine eindeutigerequestId
und verwendenasyncLocalStorage.enterWith()
, um sie zusammen mit deruserId
zu speichern. Dieser Kontext ist nun implizit für alle nachfolgenden asynchronen Operationen verfügbar, die innerhalb diesesenterWith
-Blocks gestartet werden. - Der
PerformanceObserver
-Callback ruft nun dierequestId
ausasyncLocalStorage.getStore()
ab, wenn einmeasure
-Ereignis auftritt, und verknüpft die Leistungsmetrik direkt mit der spezifischen Anforderung. - Beachten Sie, wie der über
asyncLocalStorage.getStore()
verfügbare Kontext auch nach mehrerenawait
-Aufrufen beibehalten wird, was seine Fähigkeit zeigt, den Zustand über asynchrone Grenzen hinweg aufrechtzuerhalten.
Dieses Muster ist unglaublich leistungsfähig für das Debuggen, A/B-Tests von Leistung, das Verfolgen von Anforderungen durch Microservices (wenn Sie die requestId
weitergeben) und die Generierung detaillierter Leistungsberichte pro Anforderungstyp oder Benutzersegment.
Anwendungsszenarien
- Verfolgung der API-Endpunkt-Latenz: Messen Sie die für jede eingehende API-Anforderung benötigte Gesamtzeit und verknüpfen Sie sie mit dem Anforderungspfad der Benutzer-ID und anderen relevanten Anforderungsparametern.
- Leistung von Datenbankabfragen: Instrumentieren Sie spezifische Datenbankabfragen oder ORM-Operationen, um langsame Abfragen zu identifizieren und sie mit der Ursprungsanforderung zu verknüpfen.
- Microservice-Interkommunikation: Wenn Ihre Dienste Nachrichten mit einer Anforderungs-ID austauschen, können Sie
AsyncLocalStorage
verwenden, um diese ID bei der Verarbeitung eingehender Nachrichten beizubehalten, um eine End-to-End-Nachverfolgbarkeit der Leistung sicherzustellen. - Überwachung von Hintergrundaufträgen: Verfolgen Sie die Ausführungszeit und die Kontextdaten (z. B. Auftrags-ID, den Benutzer, der den Auftrag initiiert hat) für lang andauernde Hintergrundaufgaben.
- A/B-Tests der Leistung: Indem Sie den Anforderungskontext kennen, können Sie Leistungsmetriken basierend auf verschiedenen Feature-Flags oder Benutzergruppen analysieren, um die Leistungsauswirkungen neuer Features zu bewerten.
Fazit
Die Kombination von perf_hooks
und AsyncLocalStorage
bietet Node.js-Entwicklern ein robustes und natives Toolkit für die detaillierte Leistungsüberwachung. perf_hooks
ermöglicht eine präzise Messung der Codeausführungszeiten, während AsyncLocalStorage
den Kontext intelligent über komplexe asynchrone Abläufe hinweg beibehält. Zusammen ermöglichen sie es Ihnen, über übergeordnete Metriken hinauszugehen und das "Wer" "Was" und "Wann" hinter der Leistung Ihrer Anwendung zu verstehen, was zu gezielteren und effektiveren Optimierungen führt und letztendlich zu schnelleren und zuverlässigeren Node.js-Anwendungen führt. Dieses Duo ist unerlässlich für jeden, der ernsthaft leistungsstarke Node.js-Dienste aufbaut.