Beschränkung der API-Zugriffsrate mit nginx als Proxy-Server

Jeder Entwickler, der mal mit Googles APIs gearbeitet hat, kennt das Problem – bei zu vielen Requests pro Minute werden die Anfragen nicht mehr verarbeitet und man bekommt einen 403 Fehler mit Statuscode OVER_QUERY_LIMIT. Und das ist gut so, denn damit möchte man beispielsweise folgendes erreichen:

  • Die Rechenzeit soll fair verteilt werden
  • Die Applikation soll nicht mehr Daten empfangen, als sie verarbeiten kann
  • Der Datenklau durch “Data scanning” bzw. “Data scraping” soll zumindest erschwert werden
  • Brute-Force Login-Versuche können so erschwert werden

Welche Möglichkeiten gibt es derartige Limitierung umzusetzen? Na gut, die erste und sehr naive Methode (habe ich tatsächlich mal gemacht…) wäre zum Beispiel ein Anfrage-Zähler in der Applikation selbst. Man implementiere einen ServletFilter in dem die IP-Adresse sowie die letzten Anfragen (Requests) inkl. Zeitstempel des Clients gespeichert und ausgewertet werden.

Diese Methode ist zwar “cool” und es macht sicher Spaß so etwas selbst zu programmieren, hat aber einige schwerwiegende Nachteile:

  1. Die Anfragen dringen zu tief in die Anfrageverarbeitungskette der Applikation bevor sie abgewiesen werden.
  2. Sehr großer Speicherverbrauch.
  3. Die Komplexität des Codes steigt und Performance, aufgrund der notwendigen Synchronisation, sinkt.
  4. Diese Lösung funktioniert, nur auf einer Instanz der Applikation.

Vor allem der letzte Punkt ist sehr kritisch. Hat man mehrere Knoten hinter einem Lastverteiler (Loadbalancer), muß die Anfragehistorie an einer zentralen Stelle gespeichert werden. Der Alptraum beginnt.

Die Filter-Lösung hat also einen großen Lerneffekt ist aber nicht praktikabel. Stattdessen soll die Limitierung an einer anderen Stelle stattfinden. Das Stichwort “Loadbalancer” ist schon gefallen. nginx ist ein sehr verbreiteter Webserver bzw. Proxyserver mit dem solche Anfragelimitierungen sehr einfach umsetzen lassen. Ich verzichte jetzt auf eine ausführliche nginx-Einführung und gehe direkt zu Konfiguration über.

Beispielanforderung

Nehmen wir folgende Anforderung an:

Die Anzahl der Such-Aufrufe von einer IP-Adresse soll auf maximal 1 Aufruf pro Sekunde beschränkt werden. Aufrufe die über dem definierten Limit liegen, sollen mit dem Statuscode 429 abgelehnt werden.

Die Konfiguration einer solchen Limitierung erfolgt in der Regel  in zwei Schritten:

  1. Definiere eine Zone in der allgemeinen nginx-Konfiguration
  2. Aktiviere die definierte Zone im gewünschten location-Block (server oder http ginge auch)

Schritt 1

Eine Zone legt man in der Konfigurationsdatei /etc/nginx/nginx.conf. Im Block http muss dabei folgende zwei Regeln definiert werden:

Hiermit wird eine 10 MB Zone mit dem Namen “search” angelegt, die IP-Adresse im binären Format merkt. Die maximale Anfrage-Rate beträgt 1 Request pro Sekunde (1r/s). Da nginx bei Ablehnung standardmäßig einen 503 Fehlercode sendet, setzen wir den Statuscode explizit auf das 429 – so wie es eigentlich im RFC-6585 definiert ist.

Was ist eine Zone?

Im binär-Format benötigen die IP-Adressen 4 Bytes für IPv4 oder 16 Bytes für IPv6-Adressen. Laut Dokumentation, wird pro IP-Adresse immer ein s.g. State in der Zone angelegt (IP-Adresse, Zähler, Zeitstempel usw.), das 64 Bytes auf einer 32-bit- bzw. 128 Bytes auf einer 64-bit-Maschine benötigt. Das bedeutet also, daß unsere 10 MB Zone maximal 160 000 IPv4 oder 80 000 IPv6-States merken kann. Falls also dieser Speicherraum komplett belegt ist, können keine weiteren Anfragen mehr verarbeitet (gezählt) werden und sie werden mit einem in unserem Fall 429 Fehler abgelehnt.

Schritt 2

Im zweiten Schritt wird die Zone “search” für einen oder mehrer Pfade mittels limit_req aktiviert werden. Dazu muss lediglich folgende Regel im Knoten location definiert werden:

Warnung: Hier sollte man aufpassen, falls man die Limitierung für eine Webseite setzt. Da der Browser beim Öffnen einer Seite mehrere Anfragen an den Server schickt (css, js, Bilder), werden viele davon scheitern und die Seite sieht dann entsprechend aus…

Konfiguration testen

Nun sollte man die Konfiguration testen. Es gibt viele Möglichkeiten die gesetzte Anfragelimitierung zu testen. Scripting, Jmeter oder einfach F5 im Browser. Eine noch bessere Methode ist das Stresstest-Tool namens ab aus dem Paket apache2-utils in dem auch das sehr bekannte Tool htpasswd liegt. Das Apache benchmarking tool oder eben kurz ab ist einfach zu bedienen. In unserem Fall müssen nur mehrere Anfragen möglichst schnell versendet werden. Dazu werden 5 Anfragen (-n5) mit fünf Threads (-c5) wie folgt abgefeuert:

Die wichtigste Information steckt in diesen Zeilen:

Von 5 Anfragen wurde tatsächlich 4 abgelehnt. Die Konfiguration funktioniert und die Anforderung ist somit umgesetzt.


Mehrere Clienten hinter einer IP-Adresse

Der Zugriff auf ein Service von mehreren Clienten über ein einzige IP-Adresse ist keine Seltenheit. Um die Nutzer nicht zu unrecht auszusperren, muß die die Anforderung ein wenig aufgeweicht werden:

Die Anzahl der Such-Aufrufe von einer IP-Adresse soll auf maximal 1 Aufruf pro Sekunde beschränkt werden. Aufrufe die über dem definierten Limit liegen, sollen verzögert verarbeitet werden.

Dazu muss die Anweisung wie folgt geändert werden:

burst

Es kam also zusätzlich eine neue Anweisungen burst hinzu. Hätte man diese Anweisung queue benannt, wäre wohl sofort klar was sie tut…

Falls also die Anfragen von einer IP-Adresse zu oft ankommen, also mehr als eine pro Sekunde, dann werden diese nicht sofort abgelehnt. Solche „Ausbrecher“ kommen erstmal in eine Warteschlange, die in unserem Fall maximal 5 Anfragen aufnehmen kann. Diese Anfragen werden nun so in der Verarbeitung verzögert, daß die definierte Rate von 1r/s erfüllt ist. Solange die Warteschlange voll ist, werden alle weiteren Anfragen abgelehnt. Das heißt also, daß man eine Art Bonus von 6 Anfragen hat (die aller erste Anfrage + 5 die in die Queue kommen)

Auch hier kann man Funktionalität dieser Regel sehr leicht überprüfen. Ab mit 6 gleichzeitigen Anfragen:

Alle Anfragen werden angenommen. Der Client hat also 6 Anfragen innerhalb einer Sekunde abgeschickt, bekam aber Antworten für 5 davon mit einer Verzögerung von 1 Sekunde. Der Server hat somit selbst dafür gesorgt, daß die geforderte maximale Anfrage-Rate eingehalten wird.

Und das passiert, wenn man 7 Anfragen absendet:

Wie erwartet hat sich die Gesamtdauer nicht verändert, da die letzte siebte Anfrage promt abgelehnt wurde.

nodelay

Nun betrachten wir zum Schluß  eine andere interessante Zusatzregel. Mit nodelay ist es möglich die eben angesprochene Verzögerung abzuschalten.

Das bedeutet, daß die „burst-Anfragen“ sofort verarbeitet werden. Man erlaubt somit also in Wirklichkeit 6 Anfragen pro Sekunde. Kommen innerhalb einer Sekunde 7 Anfragen von einer IP-Adresse, werden 6 davon sofort verarbeitet und die 7-te abgelehnt:

Ok, das war jetzt wirklich viel Text für so eine auf den ersten Blick kleine Funktionalität. Die Anfrage-Limitierung ist aber eine ziemlich wichtige und notwendige Funktion eines Proxy-Servers.

Falls etwas unklar ist, lohnt es sich einen Blick in die Dokumentation zu werfen:

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.