Absicherung von Node.js-APIs mit Ratenbegrenzung und Circuit Breakers
Wenhao Wang
Dev Intern · Leapcell

Einleitung
In der Welt moderner Webanwendungen dienen APIs als Rückgrat, verbinden verschiedene Dienste und liefern Daten an Benutzer. Die offene Natur von APIs macht sie jedoch auch potenziellen Schwachstellen und Überlastungen ausgesetzt. Stellen Sie sich ein Szenario vor, in dem eine plötzliche Anfrageflut, sei es durch böswillige Angriffe, fehlerhaften clientseitigen Code oder sogar legitimen, aber hochvolumigen Datenverkehr, Ihre Node.js-API überlastet. Dies kann zu Leistungseinbußen, Service-Nichtverfügbarkeit und letztendlich zu einer schlechten Benutzererfahrung führen. Um diese Herausforderungen zu meistern und widerstandsfähigere Systeme aufzubauen, kommen zwei leistungsstarke Muster zum Einsatz: Ratenbegrenzung und Circuit Breakers. Dieser Artikel untersucht die Bedeutung dieser Mechanismen, vertieft sich in ihre zugrunde liegenden Prinzipien, demonstriert ihre Implementierung in Node.js und erörtert, wie sie Ihre API vor verschiedenen Bedrohungen schützen können.
Kernkonzepte erklärt
Bevor wir uns mit den Implementierungsdetails befassen, lassen Sie uns die Kernkonzepte klären, die unsere Diskussion definieren:
- Ratenbegrenzung: Dies ist ein Mechanismus zur Kontrolle der Anzahl von Anfragen, die ein Benutzer oder Client innerhalb eines definierten Zeitfensters an eine API stellen kann. Sein Hauptziel ist es, Missbrauch zu verhindern, eine faire Ressourcenzuweisung zu gewährleisten und die API vor Überlastung zu schützen. Stellen Sie es sich wie einen Türsteher in einem Club vor, der nur eine bestimmte Anzahl von Leuten gleichzeitig hereinlässt, um Überfüllung zu vermeiden.
- Circuit Breaker: Dieses Muster, das von elektrischen Leistungsschaltern inspiriert ist, verhindert, dass ein System wiederholt versucht, eine Operation auszuführen, die wahrscheinlich fehlschlägt. Anstatt einen fehlerhaften Dienst ständig zu belasten, öffnet sich der Circuit Breaker und leitet den Verkehr für einen bestimmten Zeitraum vom fehlerhaften Bestandteil weg. Nach einer Zeitüberschreitung versucht er, sich zu schließen, und erlaubt eine begrenzte Anzahl von Anfragen, um zu überprüfen, ob sich der Dienst erholt hat. Dies verhindert kaskadierende Fehler und gibt fehlerhaften Diensten Zeit zur Erholung.
Ratenbegrenzung verstehen und implementieren
Ratenbegrenzung ist entscheidend für die API-Stabilität. Ohne sie könnte ein einzelner Client Serverressourcen monopolisieren und alle anderen Benutzer beeinträchtigen. Lassen Sie uns seine Prinzipien und seine Implementierung in Node.js mithilfe beliebter Middleware untersuchen.
Prinzipien der Ratenbegrenzung
Ratenbegrenzung beinhaltet typischerweise die Verfolgung von Anfragen von einer bestimmten Quelle (identifiziert durch IP-Adresse, API-Schlüssel oder Benutzer-ID) und das Blockieren nachfolgender Anfragen, wenn das definierte Limit innerhalb eines Zeitfensters überschritten wird. Gängige Algorithmen sind:
- Fixed Window Counter: Ein einfacher Ansatz, bei dem ein Zähler für ein festes Zeitfenster geführt wird. Alle Anfragen innerhalb dieses Fensters erhöhen den Zähler. Sobald das Fenster abgelaufen ist, wird der Zähler zurückgesetzt.
- Sliding Window Log: Diese Methode speichert ein Protokoll von Zeitstempeln für jede Anfrage. Wenn eine neue Anfrage eintrifft, prüft sie, wie viele Anfragen im Protokoll in das aktuelle Fenster fallen. Dies bietet glattere Limits als feste Fenster.
- Token Bucket: Anfragen verbrauchen "Tokens" aus einem Bucket. Tokens werden mit einer festen Rate aufgefüllt. Wenn der Bucket leer ist, werden Anfragen abgelehnt. Dies ermöglicht burst-artigen Datenverkehr und erzwingt dennoch eine durchschnittliche Rate.
Ratenbegrenzung in Node.js implementieren
Für Node.js ist express-rate-limit
eine weit verbreitete und robuste Middleware. Sie ist einfach in Express-Anwendungen zu integrieren.
Installieren Sie zuerst das Paket:
npm install express-rate-limit
Implementieren Sie es dann in Ihrer Express-Anwendung:
const express = require('express'); const rateLimit = require('express-rate-limit'); const app = express(); const port = 3000; // Auf alle Anfragen anwenden const globalLimiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 Minuten max: 100, // Jede IP auf 100 Anfragen pro windowMs begrenzen message: 'Zu viele Anfragen von dieser IP, bitte versuchen Sie es in 15 Minuten erneut', standardHeaders: true, // Ratenbegrenzungsinformationen in den `RateLimit-*` Headern zurückgeben legacyHeaders: false, // Die `X-RateLimit-*` Header deaktivieren }); // Auf bestimmte Routen anwenden, z. B. auf einen Login-Endpunkt const loginLimiter = rateLimit({ windowMs: 60 * 60 * 1000, // 1 Stunde max: 5, // Jede IP auf 5 Login-Versuche pro Stunde begrenzen message: 'Zu viele Login-Versuche von dieser IP, bitte versuchen Sie es in einer Stunde erneut', handler: (req, res) => { res.status(429).json({ error: 'Zu viele Login-Versuche, bitte versuchen Sie es später erneut.' }); }, standardHeaders: true, legacyHeaders: false, }); // Den globalen Limiter auf alle Routen anwenden app.use(globalLimiter); app.get('/', (req, res) => { res.send('Willkommen auf der Homepage!'); }); app.post('/login', loginLimiter, (req, res) => { // Ihre Login-Logik hier res.send('Login erfolgreich!'); }); app.listen(port, () => { console.log(`Server läuft unter http://localhost:${port}`); });
In diesem Beispiel wendet globalLimiter
eine globale Begrenzung von 100 Anfragen pro IP alle 15 Minuten an. Der loginLimiter
ist restriktiver und erlaubt nur 5 Login-Versuche pro IP pro Stunde, was zeigt, wie Limits basierend auf der Empfindlichkeit bestimmter Endpunkte für bestimmte Endpunkte zugeschnitten werden können.
Anwendungsfälle für Ratenbegrenzung
- DDoS-Schutz: Die Begrenzung der Anzahl von Anfragen von einer einzelnen IP-Adresse kann einfache Denial-of-Service-Angriffe abmildern.
- Schutz vor Brute-Force-Angriffen: Die Einschränkung von Login-Versuchen oder Passwort-Reset-Anfragen hilft, Angreifer daran zu hindern, Anmeldedaten zu erraten.
- Verhinderung von API-Missbrauch: Stellt sicher, dass kein einzelner Client übermäßige Ressourcen verbraucht, und erhält so die Servicequalität für alle Benutzer.
- Kostenkontrolle: Für APIs, bei denen pro Anfrage Kosten anfallen (z. B. Drittanbieterdienste), kann die Ratenbegrenzung helfen, die Nutzung zu steuern.
Circuit Breaker verstehen und implementieren
Während Ratenbegrenzung vor hohem Volumen schützt, schützen Circuit Breaker vor fehlgeschlagenen Abhängigkeiten.
Prinzipien von Circuit Breakers
Ein Circuit Breaker existiert typischerweise in drei Zuständen:
- Closed: Dies ist der Anfangszustand. Anfragen werden normal durchgelassen. Wenn Fehler auftreten, überwacht der Breaker diese. Wenn die Fehlerrate einen Schwellenwert überschreitet, wechselt er in den Open-Zustand.
- Open: In diesem Zustand schlagen alle Anfragen an die geschützte Operation sofort fehl (Fast-Fail). Dies verhindert eine weitere Überlastung des fehlerhaften Dienstes und gibt dem Aufrufer schnell einen Fehler zurück. Nach einer konfigurierbaren Zeitüberschreitung wechselt er in den Half-Open-Zustand.
- Half-Open: Eine begrenzte Anzahl von Testanfragen wird an die geschützte Operation durchgelassen. Wenn diese Anfragen erfolgreich sind, geht der Circuit Breaker davon aus, dass sich der Dienst erholt hat, und wechselt zurück in den Closed-Zustand. Wenn sie fehlschlagen, wechselt er zurück in den Open-Zustand und startet die Zeitüberschreitung neu.
Circuit Breakers in Node.js implementieren
Node.js verfügt über mehrere Bibliotheken zur Implementierung von Circuit Breakern, z. B. opossum
.
Installieren Sie zuerst opossum
:
npm install opossum
Hier ist ein Beispiel dafür, wie es verwendet werden kann, um einen externen API-Aufruf zu schützen:
const CircuitBreaker = require('opossum'); const axios = require('axios'); // Zum Ausführen von HTTP-Anfragen // Optionen für den Circuit Breaker const options = { timeout: 5000, // Wenn unsere Funktion länger als 5 Sekunden dauert, wird ein Fehler ausgelöst errorThresholdPercentage: 50, // Wenn 50 % der Anfragen fehlschlagen, wird der Circuit ausgelöst resetTimeout: 10000, // Nach 10 Sekunden wird der Circuit auf `half-open` gesetzt }; // Definieren Sie die Funktion, die fehlschlagen kann (z. B. ein externer API-Aufruf) async function callExternalService() { console.log('Versuch, externen Dienst aufzurufen...'); try { const response = await axios.get('http://localhost:8080/data'); // Ersetzen Sie dies durch Ihren externen Dienstendpunkt if (response.status !== 200) { throw new Error(`Externer Dienst antwortete mit Status: ${response.status}`); } console.log('Externer Dienstaufruf erfolgreich!'); return response.data; } catch (error) { console.error('Externer Dienstaufruf fehlgeschlagen:', error.message); throw error; // Erneut auslösen, um den Circuit Breaker über den Fehler zu informieren } } // Erstellen Sie einen Circuit Breaker um die Funktion const breaker = new CircuitBreaker(callExternalService, options); // Lauschen Sie auf Circuit Breaker-Ereignisse für Protokollierung und Fehlerbehebung breaker.on('open', () => console.warn('Circuit Breaker OPEN! Externer Dienst ist wahrscheinlich ausgefallen.')); breaker.on('halfOpen', () => console.log('Circuit Breaker HALF-OPEN. Externer Dienst wird geprüft...')); breaker.on('close', () => console.log('Circuit Breaker CLOSED. Externer Dienst wiederhergestellt.')); breaker.on('fallback', (error) => console.error('Circuit Breaker im Fallback-Modus:', error.message)); // Beispielverwendung in einer Express-Route const express = require('express'); const app = express(); const port = 3000; app.get('/protected-data', async (req, res) => { try { const data = await breaker.fire(); res.json(data); } catch (error) { // Wenn der Circuit geöffnet ist, wird dieser Fehler sofort ausgelöst // ODER wenn der zugrunde liegende Dienst fehlschlägt und kein Fallback bereitgestellt wird res.status(503).json({ error: 'Dienst vorübergehend nicht verfügbar. Bitte versuchen Sie es später erneut.' }); } }); // Ein Dummy-externer Dienst zum Testen const mockExternalService = express(); mockExternalService.get('/data', (req, res) => { // Fehler sporadisch simulieren if (Math.random() < 0.6) { // 60% Chance auf Fehler console.log('Mock-externer Dienst schlägt fehl...'); return res.status(500).json({ message: 'Interner Serverfehler vom Mock-Dienst' }); } console.log('Mock-externer Dienst erfolgreich...'); res.json({ message: 'Daten vom externen Dienst' }); }); mockExternalService.listen(8080, () => { console.log('Mock-externer Dienst läuft auf Port 8080'); }); app.listen(port, () => { console.log(`Haupt-API-Server läuft unter http://localhost:${port}`); });
In diesem Beispiel versucht breaker.fire()
, callExternalService
auszuführen. Wenn callExternalService
zu oft fehlschlägt (in diesem Fall 50 %), öffnet sich der Circuit, und nachfolgende Aufrufe von breaker.fire()
lösen sofort einen Fehler aus, wodurch kontinuierliche Aufrufe des fehlerhaften externen Dienstes verhindert werden. Nach resetTimeout
wechselt er in den Half-Open-Zustand und versucht einige Anfragen, um zu sehen, ob der Dienst wieder verfügbar ist.
Sie können auch eine fallback
-Funktion für den Circuit Breaker definieren, die ausgeführt wird, wenn der Circuit geöffnet ist oder wenn die primäre Funktion fehlschlägt und kein anderer Fehlerbehandler vorhanden ist. Dies ermöglicht eine anmutige Funktionalitätsverschlechterung.
// ... (vorheriger Code) ... // Fallback-Funktion hinzufügen breaker.fallback(async () => { console.log('Verwende Fallback-Daten!'); return { message: 'Fallback-Daten: Dienst ist derzeit nicht verfügbar, hier sind jedoch einige zwischengespeicherte Informationen.' }; }); // ... (Rest des Codes) ...
Anwendungsfälle für Circuit Breakers
- Microservices-Architekturen: Unerlässlich, um kaskadierende Fehler in vernetzten Diensten zu verhindern. Wenn ein Microservice ausfällt, reißt er nicht das gesamte System mit.
- Integrationen mit Drittanbieter-APIs: Schützen Sie Ihre Anwendung vor Ausfällen oder Leistungsverschlechterungen externer Dienste, von denen Sie abhängig sind.
- Datenbankverbindungen: Verhindern Sie, dass Ihre Anwendung kontinuierlich Abfragen an eine nicht reagierende Datenbank wiederholt.
- Ressourcenschutz: Gibt fehlerhaften Diensten die Möglichkeit, sich zu erholen, indem Anfragen an sie vorübergehend gestoppt werden.
Fazit
Die Implementierung von Ratenbegrenzung und Circuit Breakern ist nicht nur eine Best Practice, sondern eine grundlegende Voraussetzung für den Aufbau robuster, skalierbarer und widerstandsfähiger Node.js-APIs. Ratenbegrenzung fungiert als Front-Line-Abwehr Ihrer API und stellt faire Nutzung und Überlastungsschutz sicher, während Circuit Breaker entscheidende Widerstandsfähigkeit gegen fehlschlagende Abhängigkeiten bieten, kaskadierende Fehler verhindern und eine anmutige Verschlechterung fördern. Durch die strategische Anwendung dieser Muster können Sie die Stabilität und Zuverlässigkeit Ihrer Anwendungen erheblich verbessern und selbst unter widrigen Bedingungen eine durchweg positive Benutzererfahrung bieten. Der Schutz Ihrer API mit diesen Mustern ist eine Investition in den langfristigen Betriebserfolg.