How ich einen Sliding Window Rate Limiter in Redis gebaut habe
Unsere Video-API stürzte jeden Abend um 20:00 Uhr UTC ab.
Es war kein echter Traffic. Ein paar Scraper hatten unseren Trending-Endpoint gefunden und ihn bombardiert. Unsere Datenbankabfragen häuften sich an. Echte Nutzer sahen nur noch Lade-Spinner statt Videos.
Wir brauchten Rate Limiting. Die meisten einfachen Methoden verschlimmerten das Problem sogar.
Das Problem mit Fixed Window Countern: Die meisten fangen mit Fixed Windows an. Man zählt die Anfragen in einem festen Zeitblock. Wenn das Limit 100 pro Minute beträgt, kann ein Nutzer 100 Anfragen um 11:59:59 senden und weitere 100 um 12:00:00. Das sind 200 Anfragen in einer Sekunde. Scraper nutzen diese Grenzen aus, um die Limits zu umgehen.
Die Lösung: Sliding Window. Ein Sliding Window zählt die Anfragen der letzten N Sekunden relativ zum aktuellen Zeitpunkt. Es gibt keine Grenzen, die man ausnutzen könnte.
Wir verwenden je nach Bedarf zwei verschiedene Methoden:
- Sliding Window Log (für Präzision) Für authentifizierte Nutzer verwenden wir Redis Sorted Sets (ZSET). Wir speichern für jede Anfrage einen Zeitstempel.
- Wir nutzen
ZREMRANGEBYSCORE, um alte Einträge zu löschen. - Wir nutzen
ZCARD, um die aktuellen Anfragen zu zählen. - Wir nutzen
ZADD, um neue Anfragen zu erfassen.
Um Race Conditions zu vermeiden, führen wir diese Logik in einem einzigen Lua-Skript aus. Das macht die Operation atomar. Ein Skript erledigt alles in einem einzigen Durchgang zu Redis. So wird verhindert, dass zwei Anfragen gleichzeitig glauben, sie lägen noch unter dem Limit.
- Sliding Window Counter (für Skalierbarkeit) Bei anonymem Traffic haben wir Millionen von Keys. Wir können nicht jeden Zeitstempel speichern. Stattdessen verwenden wir einen Counter, der das aktuelle und das vorherige Fenster gewichtet. Das verbraucht viel weniger Speicher und ist hocheffizient.
Wichtige Erkenntnisse aus der Produktion:
- Nutzt Lua-Skripte. Das stellt sicher, dass Prüfung und Schreibvorgang als eine einzige Aktion ausgeführt werden.
- Verwendet Standard-Header. Sendet immer
X-RateLimit-RemainingundRetry-After. Das hilft gutartigen Clients, sich automatisch zurückzuhalten. - Fail Open. Wenn Redis ausfällt, lasst die Anfrage durch. Eine überlastete API ist besser als eine komplett tote API.
- Identifiziert Clients korrekt. Nutzt API-Keys für Nutzer und gehashte IPs für Gäste.
- Passt die Limits an die Endpoints an. Ein rechenintensiver Search-Endpoint benötigt strengere Limits als ein einfacher List-Endpoint.
Das Ergebnis war unmittelbar spürbar. Die Spitzen am Abend verschwanden. Die Performance unserer Datenbank kehrte zum Normalzustand zurück.
