Ramp ruft Dutzende von APIs Dritter auf (Finicity, Teller, Zendesk, Salesforce), die jeweils eigene Kontingente vorgeben, führt periodische Celery-Aufgaben über seine gesamte Nutzerbasis aus und wollte eingehenden Datenverkehr nach authentifiziertem Nutzer statt nach IP-Adresse begrenzen. Alle drei Anforderungen wiesen auf dieselbe fehlende Komponente hin: einen Ratenbegrenzer, der über jeden Worker und jeden Server hinweg präzise ist, nicht nur innerhalb eines einzelnen.

Der bisherige Ansatz von Ramp stützte sich auf die integrierte Ratenbegrenzung von Celery pro Aufgabe. Diese Begrenzung galt pro Worker, nicht global, sodass die Durchsetzung einer echten systemweiten Rate erforderte, dass ein Ingenieur die Worker-Anzahl der Warteschlange und ihr Autoscaling-Verhalten kannte und das gewünschte Limit manuell auf die Worker aufteilte – eine Zahl, die in dem Moment veraltet war, in dem sich die Worker-Anzahl änderte. Unter bestimmten Celery-Prefetch-Konfigurationen konnten ratenbegrenzte Aufgaben sogar einen Worker daran hindern, neue Arbeit aufzunehmen, wodurch ganze Warteschlangen zum Stillstand kamen.

Ramp bewertete die Algorithmen festes Zeitfenster, gleitendes Zeitfenster und Leaky Bucket, bevor man sich für den Generic Cell Rate Algorithm (GCRA) entschied, eine Leaky-Bucket-Variante, die pro Schlüssel nur einen einzigen berechneten Zeitstempel speichern muss statt eines vollständigen Anfrageprotokolls. Man implementierte ihn auf Redis, fügte eine verteilte Sperre hinzu, um eine Lücke durch Race Conditions in den Referenzimplementierungen zu schließen, und verwendete die Serverzeit von Redis statt der Uhr jedes App-Servers, um Abweichungen zwischen den Maschinen zu vermeiden.

Das Ergebnis war ein einziges Ratenbegrenzungs-Framework, das Ramp auf alle drei Anwendungsfälle richten konnte – Aufrufe von APIs Dritter, globale Drosselung von Celery-Aufgaben und eingehende API-Limits pro Nutzer –, ohne dass ein Ingenieur die Worker-Anzahl überhaupt berücksichtigen musste.

Ein globaler GCRA-Ratenbegrenzer mit Redis als Backend

Das Speichern eines einzigen TAT-Werts pro Schlüssel ist weitaus günstiger als das vollständige Anfrageprotokoll eines gleitenden Zeitfensters, sodass der Begrenzer selbst bei Ramps Anfragevolumen schnell bleibt.

Das Lesen der aktuellen Zeit aus Redis selbst statt von dem App-Server, der die Anfrage bearbeitet, beseitigt Uhrzeitabweichungen als Ursache inkonsistenter Durchsetzung.

Eine Redis-Sperre um das Prüfen-und-Setzen schließt die Race Condition, bei der zwei gleichzeitige Anfragen beide einen veralteten TAT lesen und beide genehmigt werden könnten.

Der Aufbau eines allgemeinen Frameworks statt eines einmaligen Begrenzers bedeutete, dass derselbe Code Kontingente Dritter, die Drosselung von Celery-Aufgaben und API-Limits pro Nutzer bedienen konnte, ohne doppelte Logik.

Ein Ratenlimit, das nur pro Prozess durchgesetzt wird, nähert ein globales Limit lediglich an; erst die Zentralisierung des Zählers in einem gemeinsam genutzten, sperrbaren Speicher sorgt dafür, dass die Zahl tatsächlich das bedeutet, was sie aussagt.

Ramp führte den GCRA-auf-Redis-Begrenzer für die drei im Beitrag beschriebenen Anwendungsfälle ein – globale Kontingente von APIs Dritter, Drosselung von Celery-Aufgaben und eingehende API-Begrenzung pro Nutzer –, mit Unit-Tests und manueller Überprüfung, und erklärte, man plane, dasselbe Framework auf neue Ratenbegrenzungsalgorithmen auszuweiten, sobald weitere Anwendungsfälle aufträten.

DEN BELEGEN FOLGEN

Die Quellen

  1. Rate limiting with Redis Ramp Engineering (Builders Blog) · builders.ramp.com ·