Implementierung eines Sliding-Window-Ratenlimiters mit Go und Redis
Grace Collins
Solutions Engineer · Leapcell

Einführung
In der sich ständig weiterentwickelnden Landschaft verteilter Systeme und Microservices ist die effektive Verwaltung des API-Traffics von größter Bedeutung für die Aufrechterhaltung der Systemstabilität, die Verhinderung von Missbrauch und die Gewährleistung einer fairen Ressourcenverteilung. Ungedrosselte Anfragen können Server überlasten, zu Denial-of-Service-Angriffen führen und die Benutzererfahrung beeinträchtigen. Hier kommt die Ratenbegrenzung als kritischer Abwehrmechanismus ins Spiel. Unter den verschiedenen Algorithmen zur Ratenbegrenzung bietet der Sliding-Window-Ansatz eine genauere und reaktionsfähigere Methode zur Steuerung der Anfrageraten im Vergleich zu einfacheren Methoden wie Fixed Windows oder Token Buckets. Dieser Artikel untersucht die Implementierung eines Sliding-Window-Ratenlimiters mit Go, das für seine Nebenläufigkeitsfunktionen und Leistung bekannt ist, und Redis, einem leistungsstarken In-Memory-Datenspeicher, der sich ideal für Hochleistungsanwendungsfälle wie die Ratenbegrenzung eignet. Wir werden uns mit den Mechanismen dieser robusten Technik befassen und praktische Go-Codebeispiele zur Demonstration ihrer Implementierung bereitstellen.
Grundlegende Konzepte verstehen
Bevor wir uns mit der Implementierung befassen, wollen wir ein klares Verständnis der beteiligten Schlüsselkonzepte aufbauen:
- Ratenbegrenzung (Rate Limiting): Ein Kontrollmechanismus zur Begrenzung der Anzahl von Anfragen, die ein Benutzer oder Client innerhalb eines bestimmten Zeitrahmens an einen Server stellen kann. Seine Hauptziele sind die Verhinderung von Ressourcenerschöpfung, der Schutz vor bösartigen Aktivitäten und die Gewährleistung einer fairen Nutzung.
- Sliding-Window-Algorithmus: Dies ist ein Algorithmus zur Ratenbegrenzung, der Anfragen über ein kontinuierliches, sich bewegendes Zeitfenster verfolgt. Im Gegensatz zum Fixed-Window-Algorithmus, der unter einem "Burstiness"-Problem an den Fenstergrenzen leiden kann, bietet das Sliding Window eine reibungslosere und genauere Ratenkontrolle. Typischerweise kombiniert er zwei feste Fenster: das aktuelle Fenster und das vorherige Fenster. Die Anzahl für das aktuelle Fenster wird gewichtet, indem berücksichtigt wird, wie viel von diesem Fenster vergangen ist, und die Anzahl für das vorherige Fenster wird gewichtet, indem berücksichtigt wird, wie viel von diesem Fenster noch relevant ist.
- Go (Golang): Eine statisch typisierte, kompilierte Programmiersprache, die bei Google entwickelt wurde. Ihre Stärken liegen in ihren exzellenten Nebenläufigkeitsprimitiven (Goroutinen und Channels), der Garbage Collection und der robusten Standardbibliothek, was sie zu einer idealen Wahl für die Erstellung von Hochleistungs-Netzwerkdiensten macht.
- Redis: Ein Open-Source-Datenspeicher für Datenstrukturen im Arbeitsspeicher, der als Datenbank, Cache und Message Broker verwendet wird. Seine blitzschnellen Lese-/Schreibgeschwindigkeiten in Kombination mit der Unterstützung verschiedener Datenstrukturen wie sortierter Mengen und Hash-Maps machen ihn außergewöhnlich gut für die Implementierung von Ratenbegrenzern geeignet.
Der Sliding-Window-Ratenbegrenzer erklärt
Die Kernidee eines Sliding-Window-Ratenlimiters ist die Verfolgung von Anfragen anhand ihrer Zeitstempel. Wenn eine Anfrage eingeht, wird ihr Zeitstempel zu einer Datenstruktur hinzugefügt. Um dann festzustellen, ob die Anfrage zulässig ist, zählen wir die Anzahl der Anfragen, deren Zeitstempel in das definierte Zeitfenster fallen (z. B. die letzten 60 Sekunden). Entscheidend ist, dass im Laufe der Zeit ältere Anfragen, die außerhalb des Fensters liegen, automatisch verworfen werden.
Redis Sortierte Mengen (ZSETs) sind hierfür perfekt geeignet. Der Zeitstempel jeder Anfrage kann als Score gespeichert werden und eine eindeutige Kennung (z. B. eine UUID oder einfach der Zeitstempel selbst) als Mitglied fungieren. Dies ermöglicht uns effizient:
- Zeitstempel einer neuen Anfrage hinzufügen:
ZADD key timestamp timestamp - Alte Anfragen entfernen:
ZREMRANGEBYSCORE key -inf (jetzt - fensterDauer) - Aktuelle Anfragen zählen:
ZCARD keyoderZCOUNT key (jetzt - fensterDauer) +inf
Implementierungsdetails mit Go und Redis
Lassen Sie uns die Go- und Redis-Implementierung durchgehen.
Zuerst benötigen wir einen Redis-Client für Go. Das Paket go-redis/redis/v8 ist eine beliebte und robuste Wahl.
package main import ( "context" "fmt" "log" "strconv" "time" "github.com/go-redis/redis/v8" ) // RateLimiterConfig speichert die Konfiguration unseres Ratenlimiters type RateLimiterConfig struct { Limit int // Maximale Anzahl erlaubter Anfragen WindowSize time.Duration // Dauer des Sliding Window RedisClient *redis.Client } // NewRateLimiterConfig erstellt eine neue RateLimiterConfig-Instanz func NewRateLimiterConfig(limit int, windowSize time.Duration, rdb *redis.Client) *RateLimiterConfig { return &RateLimiterConfig{ Limit: limit, WindowSize: windowSize, RedisClient: rdb, } } // Allow prüft, ob eine Anfrage basierend auf dem Sliding-Window-Algorithmus zulässig ist func (rlc *RateLimiterConfig) Allow(ctx context.Context, key string) (bool, error) { now := time.Now().UnixNano() / int64(time.Millisecond) // Aktueller Zeitstempel in Millisekunden // Verwenden Sie eine Redis-Transaktion (MULTI/EXEC) für Atomizität // Dies stellt sicher, dass alle Operationen als eine einzige, atomare Einheit behandelt werden pipe := rlc.RedisClient.Pipeline() // 1. Zeitstempel älter als das Fenster entfernen // ZREMRANGEBYSCORE key -inf (jetzt - fensterGroesseInMillisekunden) // Das `( ` vor dem Score macht ihn exklusiv. Wir wollen Elemente entfernen, die strikt älter sind als der Fensterstart. minScore := now - rlc.WindowSize.Milliseconds() pipe.ZRemRangeByScore(ctx, key, "-inf", strconv.FormatInt(minScore, 10)) // 2. Zeitstempel der aktuellen Anfrage hinzufügen // ZADD key jetzt jetzt // Score und Member sind hier dieselben (Zeitstempel) pipe.ZAdd(ctx, key, &redis.Z{ Score: float64(now), Member: now, }) // 3. Anzahl der Anfragen im aktuellen Fenster zählen // ZCOUNT key (jetzt - fensterGroesseInMillisekunden) +inf // Wir zählen alle Elemente innerhalb des aktuellen Fensters, einschließlich des neu hinzugefügten. countCmd := pipe.ZCount(ctx, key, strconv.FormatInt(minScore, 10), "+inf") // 4. Ablauf für den Key festlegen, um zu verhindern, dass er unendlich wächst // Dies ist wichtig für Keys, die keinen Datenverkehr mehr erhalten. // Wir setzen ihn auf etwas länger als die Fenstergröße, um sicherzustellen, dass die Aufzeichnungen // innerhalb des Fensters immer verfügbar sind, plus ein kleiner Puffer. pipe.Expire(ctx, key, rlc.WindowSize+10*time.Second) // Puffer zur Sicherheit hinzufügen // Alle Befehle in der Pipeline atomar ausführen _, err := pipe.Exec(ctx) if err != nil { return false, fmt.Errorf("redis transaction failed: %w", err) } // Zählen aus dem ausgeführten Befehl abrufen currentRequests, err := countCmd.Result() if err != nil { return false, fmt.Errorf("failed to get request count from redis: %w", err) } return int(currentRequests) <= rlc.Limit, nil } func main() { // Redis-Client initialisieren rdb := redis.NewClient(&redis.Options{ Addr: "localhost:6379", // Ersetzen Sie dies durch Ihre Redis-Adresse Password: "", // Kein Passwort gesetzt DB: 0, // Standard-DB verwenden }) // Redis anpingen, um die Verbindung sicherzustellen ctx := context.Background() _, err := rdb.Ping(ctx).Result() if err != nil { log.Fatalf("Could not connect to Redis: %v", err) } fmt.Println("Connected to Redis!") // Ratenbegrenzer konfigurieren: 5 Anfragen pro 10 Sekunden pro eindeutigem Key limiter := NewRateLimiterConfig(5, 10*time.Second, rdb) // Anfragen für eine bestimmte Benutzer-ID oder einen API-Schlüssel simulieren userID := "user:123" fmt.Printf("Rate limit for %s: %d requests per %s\n", userID, limiter.Limit, limiter.WindowSize) for i := 1; i <= 10; i++ { allowed, err := limiter.Allow(ctx, userID) if err != nil { log.Printf("Error checking rate limit for %s: %v", userID, err) time.Sleep(500 * time.Millisecond) // Bei Fehlern nicht überlasten continue } if allowed { fmt.Printf("Request %d for %s: ALLOWED\n", i, userID) } else { fmt.Printf("Request %d for %s: BLOCKED (Rate Limit Exceeded)\n", i, userID) } time.Sleep(500 * time.Millisecond) // Einige Verzögerungen zwischen den Anfragen simulieren if i == 5 { fmt.Println("\n--- Waiting for window to slide ---") time.Sleep(6 * time.Second) // Warten Sie einige Zeit, um zu sehen, wie Anfragen wieder zulässig werden fmt.Println("--- Continuing requests ---") } } }
Codeerklärung:
RateLimiterConfigStruktur: Diese Struktur speichert dasLimit(maximale Anzahl erlaubter Anfragen) und dieWindowSize(die Dauer des Sliding Window, z. B. 10 Sekunden) und dieRedisClient-Instanz.Allow(ctx context.Context, key string) (bool, error)Methode: Dies ist die Kernlogik.now: Wir erhalten den aktuellen Zeitstempel in Millisekunden, der als Score für unsere Redis Sorted Set-Mitglieder dient.- Redis-Pipeline (Transaktion): Entscheidend ist, dass wir eine Redis
Pipelinefür Atomizität verwenden. Dies bündelt die BefehleZREMRANGEBYSCORE,ZADD,ZCOUNTundEXPIREin einer einzigen Roundtrip-Anfrage an den Redis-Server. Dies garantiert, dass alle diese Operationen sequenziell und aus Sicht von Redis atomar ausgeführt werden, wodurch Race Conditions verhindert werden, bei denen die Zählung zwischen einem Lese- und Schreibvorgang falsch sein könnte. ZRemRangeByScore: Dieser Befehl entfernt alle Mitglieder aus dem sortierten Setkey, deren Scores kleiner oder gleichjetzt - fensterGroessesind. Dies bereinigt effektiv alte Anfragen, die nicht mehr innerhalb unseres aktuellen Sliding Window liegen.ZAdd: Wir fügen denjetzt-Zeitstempel als Score und Mitglied in das sortierte Set ein. Da der Score der Zeitstempel ist, können wir nach Zeit sortieren und filtern.ZCount: Dieser Befehl zählt die Anzahl der Mitglieder im sortierten Setkey, deren Scores im Bereich(jetzt - fensterGroesse)bis+infliegen. Dies gibt uns die Gesamtzahl der Anfragen innerhalb des aktuellen Sliding Window.Expire: Wir legen einen Verfall für den Redis-Key fest. Dies ist eine entscheidende Optimierung für Keys, die möglicherweise keinen Datenverkehr mehr erhalten. Ohne Verfall würden ungenutzte Ratenbegrenzer-Keys unbegrenzt im Redis-Speicher angesammelt werden. Wir setzen ihn auf etwas länger alsWindowSize, um sicherzustellen, dass selbst Anfragen am Ende des vorherigen Fensters lange genug im Set verbleiben, um korrekt gezählt zu werden, wenn das Fenster verschoben wird.Exec: Führt alle Befehle in der Pipeline aus.currentRequests <= rlc.Limit: Schließlich vergleichen wir die gezählten Anfragen mit unserem konfiguriertenLimit, um zu entscheiden, ob die eingehende Anfrage zugelassen oder blockiert werden soll.
Anwendungsszenarien
Ein Sliding-Window-Ratenbegrenzer ist äußerst vielseitig und kann in verschiedenen Szenarien eingesetzt werden:
- API-Gateway-Schutz: Begrenzung der Anzahl von Anfragen pro Client zum Schutz von Backend-Diensten vor Überlastung.
- Benutzerbezogene Drosselung: Verhinderung, dass ein einzelner Benutzer zu viele Anfragen stellt (z. B. zu viele Anmeldeversuche, zu viele Suchanfragen).
- DDOS-Prävention: Als erste Verteidigungslinie gegen volumetrische Angriffe durch Blockieren von IP-Adressen, die eine ungewöhnlich hohe Anzahl von Anfragen stellen.
- Faire Ressourcennutzung: Sicherstellen, dass alle Benutzer einen fairen Anteil an begrenzten Ressourcen erhalten, indem diejenigen priorisiert werden, die die Ratenbegrenzungen einhalten.
- Abrechnung und gestaffelte Dienste: Implementierung unterschiedlicher Ratenbegrenzungen für verschiedene Abonnementstufen (z. B. kostenlose Stufe erhält 100 Anfragen/Minute, Premium erhält 1000 Anfragen/Minute).
Fazit
Die Implementierung eines Sliding-Window-Ratenlimiters mit Go und Redis bietet eine äußerst effektive und effiziente Methode zur Verwaltung des API-Traffics. Durch die Nutzung der sortierten Mengen von Redis und der Nebenläufigkeitsfunktionen von Go können wir ein robustes System aufbauen, das Anfragen über ein kontinuierliches Zeitfenster genau verfolgt und so Ressourcenerschöpfung verhindert und die Systemstabilität gewährleistet. Dieser Ansatz bietet eine überlegene Fairness und Genauigkeit im Vergleich zu einfacheren Methoden und ist somit ein unverzichtbares Werkzeug für die Entwicklung robuster verteilter Anwendungen.

