In letzter Zeit habe ich mich etwas intensiver mit der Rate Limiting Funktion des Webservers NGINX auseinandergesetzt. Damit lassen sich die Verbindungsanfragen auf einen bestimmten Wert pro Sekunde begrenzen. Darüber hinausgehende Verbindungsanfragen werden geblockt oder verzögert. Nach dem Lesen verschiedener Tutorials und der NGINX Dokumentation zu Rate Limiting fand ich die verschiedenen Einstellungen doch etwas verwirrend. Aus diesem Grund habe ich mich etwas länger mit den verschiedenen Einstellungen beschäftigt und deren Auswirkungen getestet.

In diesem Beitrag möchte ich zuerst grundlegendes und theoretisches zur NGINX Rate Limiting Funktion beschreiben. Anschließend werden die verschiedenen Konfigurationen und deren Auswirkungen getestet.

Wozu dient Rate Limiting

Wie bereits erwähnt kann mittels Rate Limiting die Anzahl der Verbindungsanfragen an den Webserver auf eine bestimmte Anzahl pro Zeiteinheit begrenzt werden. Typischerweise wird diese Funktion aus zwei Gründen genutzt.

  • Sicherheitsgründe. Es lassen sich beispielsweise die Zugriffe auf eine Loginseite pro IP begrenzen. Dies erschwert oder verhindert das durchprobieren von Passwörtern, also Brute-Force-Angriffe auf Loginseiten. Außerdem kann Rate Limiting eine Hilfe beim Schutz vor DOS-Attacken sein.
  • Erhöhen der Zuverlässigkeit. Trafficspitzen können entschärft werden, indem Verbindungen ab einer bestimmten Anzahl pro Sekunde verlangsamt werden. Dies kann beispielsweise helfen einen Downloadserver am Netz zu halten, trotz einer großen Menge an Anfragen. Außerdem können verschiedene Dienste die NGINX als Proxy verwenden priorisiert werden.

NGINX Rate Limiting konfigurieren

Rate Limiting wird hauptsächlich über zwei Einstellungen konfiguriert, limit_req_zone innerhalb des http-Blocks und limit_req im Server-Block. Das ganze sieht z.B. folgendermaßen aus.

limit_req_zone $binary_remote_addr zone=mylimit:10m rate=10r/s;

server {
    location /login/ {
        limit_req zone=mylimit;
        [...]
    }
}

limit_req_zone konfigurieren

Mit limit_req_zone werden drei allgemeine Einstellungen vorgenommen.

Key:Definiert wie Rate Limiting angewendet wird und kann verschiedene Werte annehmen, z.B. $server_name oder $binary_remote_addr.

  • Bei Verwendung von $server_name wird Rate Limiting basierend auf dem Servername angewendet. Das bedeutet dass diese Regel auf alle eingehenden Anfragen an den Server example.com angewendet werden, egal ob die Anfragen von einer IP-Adresse oder von vielen verschiedenen kommen. Diese Einstellung kann sich eignen um die Auslastung eines Downloadservers zu begrenzen.
  • Wird hingegen $binary_remote_addr verwendet, dann wird die Regel pro anfragender IP Adresse und damit in aller Regel pro anfragendem Computer/Benutzer ausgeführt. Diese kann beispielsweise dazu dienen Brute-Force-Attacken auf Loginformulare zu erschweren.

Zone: Beispielsweise zone=mylimit:10m. Definiert eine Zone mit beliebigem Namen für welche die gemachten Einstellungen gelten. Dabei können mehrere Zonen definiert werden. Beispielsweise eine Zone mit dem Name mylimit, welche den Zugriff auf ein Loginformular begrenzt und eine Zone mit dem Name downloadlimit, welche mit anderen Werten einen Dateidownload limitiert. Außerdem wird ein Speicherlimit für die Zone definiert, welches die gespeicherten IP-Informationen der anfragenden Computer begrenzt. Laut NGINX Dokumentation benötigen die Informationen für 16.000 IP-Adressen 1 Megabyte. In oben angegebener Beispielkonfiguration ist das Limit auf 10m, also 10 Megabyte, gesetzt so dass 160.000 Adressen gespeichert werden können.

Rate: Hier wird angegeben auf welche Anzahl an Verbindungsanfragen limitiert wird, und wie mit den darüberhinausgehenden Anfragen verfahren wird. rate=10r/s limitiert die Anfragen auf 10 requests pro Sekunde. Tatsächlich rechnet NGINX im Hintergrund die Rate auf Millisekunden herunter. Somit spielt es keine Rolle ob die Rate als 10r/s (10 requests pro Sekunde) oder 600r/m (600 requests pro Minute) oder 36000r/h (36000 request pro Stunde) angegeben wird, da alle drei Angaben auf einen request pro 100 Millisekunden heruntergebrochen werden. Ohne weitere Angaben werden alle darüber hinausgehenden Anfragen abgelehnt. Es wird also alle 100 Millisekunden eine Anfrage akzeptiert. Jede weitere Anfrage die vor Ablauf von 100 Millisekunden eintrifft wird mit dem Statuscode 503 (service unavailable) abgelehnt.

limit_req konfigurieren

limit_req aktiviert eine Zone im Server Block beispielsweise für ein bestimmtes Verzeichnis, für eine bestimmte Datei oder für den ganzen Server. Im oben angegebenen Beispiel limit_req zone=mylimit; wird die Zone mylimit auf das Verzeichnis /login angewendet.

Dabei können weitere Einstellungen gesetzt werden, die definieren, wie mit Anfragen umgegangen wird die über das Limit hinausgehen. Dies passiert mit dem burst und nodelay Parameter.

Burst: Wird an limit_req angehängt. Z.B. limit_req zone=mylimit burst=20;
Die Zone mylimit erlaubt 10 request pro Sekunde, bzw. 1 pro 100 Millisekunden. Ohne weitere Konfiguration wird Anfrage Nr. 2 innerhalb von 100 Millisekunden einfach abgelehnt. Mit Burst wird eine Warteschlange eingerichtet. In diesem Beispiel fasst die Warteschlange 20 Anfragen die gespeichert und nicht abgelehnt werden. Innerhalb von 100 Millisekunden wird also Anfrage Nr. 1 direkt beantwortet. Anfragen 2-21 werden in die Warteschlange gepackt. Anfrage Nr. 22 wird abgelehnt. Anschließend wird die Warteschlange mit einer Rate von einer Anfrage pro 100 Millisekunden abgearbeitet. Neue Anfragen füllen die Warteschlange wieder auf.

Nodelay: zusätzlich zu burst kann der Parameter nodelay gesetzt werden. Bsp: limit_req zone=mylimit burst=20 nodelay;
In diese Fall werden alle Anfragen in der Warteschlange sofort und ohne Verzögerung beantwortet, allerdings wird mit dem beantworten der Anfrage nicht gleichzeitig ein Platz in der Warteschlange frei. Die Warteschlange wird nach wie vor mit einem Request pro 100 Millisekunden geleert. Neue Anfragen werden nur beantwortet wenn ein Platz in der Warteschlange frei ist, dann allerdings ohne Verzögerung. Somit werden in den ersten 100 Millisekunden mehr als eine Anfrage beantwortet, über einen Zeitraum von einer Sekunde oder länger wird die Rate jedoch eingehalten.

Burst und nodelay können sowohl bei limit_req oder bei limit_req_zone definiert werden. Je nachdem ob diese Parameter für die komplette Zone, oder nur für ein bestimmtes Verzeichnis oder Datei gelten sollen.

Wirkung von Rate Limiting mit Siege darstellen

Siege ist ein Kommandozeilentool mit welchem sich Belastungstest von Webservern durchführen lassen indem große Mengen an Anfragen in kurzer Zeit abgesetzt werden können. Siege visualisiert dabei anschaulich welche Anfragen beantwortet werden und welche nicht. Unter Ubuntu kann Siege aus den offiziellen Paketquellen installiert werden.

Die limit_req_zone-Einstellung ist in allen folgenden Beispielen

limit_req_zone $binary_remote_addr zone=mylimit:10m rate=1r/s;

Das Limit greift also bereits ab einer Anfrage pro Sekunde. In der Praxis wahrscheinlich kein sinnvoller Wert, aber sehr gut zu Demonstrationszwecken geeignet.

Die limit_req-Einstellungen werden verändert.

ohne Rate Limiting

Zuerst starten wir einen Test ohne Rate Limiting. Siege wird mit dem Befehl und den Parametern siege -v -r 2 -c 5 http://192.168.30.136/testimg.png aufgerufen. -v (verbose) aktiviert die detailliertere Ausgabe, -r 2 -c5  führt zwei Tests durch mit jeweils fünf gleichzeitigen Anfragen. Aufgerufen wird eine Bilddatei auf einem Testserver im lokalen Netzwerk. Nach dem Ausführen des Befehls erhält man folgende Ausgabe.

NGINX Rate Limiting Test 1

Wie man sieht waren alle Anfragen erfolgreich und wurden umgehend beantwortet. Insgesamt wurden 10 Anfragen abgesendet wovon 10 erfolgreich waren. Offensichtlich wurde hier nichts limitiert.

Einstellung limit_req zone=mylimit;

In diesem Beispiel wird die Einstellung limit_req zone=mylimit; ohne weitere Zusätze verwendet. Siege wird mit dem selben Befehl aufgerufen. Nun erhält man folgendes Ergebnis.

NGINX Rate Limiting Test 2

In diesem Fall war nur die erste Anfrage erfolgreich. Abgesendet wurden zehn Anfragen, davon jeweils 5 gleichzeitig. Die erste Anfrage wurde beantwortet, die anderen vier Anfragen aus dem ersten Anfrageblock wurden direkt abgelehnt.

Der Block aus den weiteren fünf Anfragen traf direkt danach ein, das Limit von einer Anfrage pro Sekunde wurde überschritten, so dass auch diese fünf Anfragen direkt abgelehnt wurden.

Einstellung limit_req zone=mylimit burst=5;

In diesem Beispiel wird mit burst=5 eine Warteschlange für fünf Anfragen eingerichtet. Siege wird wieder mit dem bereits verwendeten Parametern aufgerufen. Das Ergebnis sieht aus wie folgt.

In diesem Fall wurden wieder alle Anfragen beantwortet. Sehr schön sieht man aber wie dies verzögert erfolgt. Wie vorhin sendet Siege einen Block aus fünf gleichzeitigen Anfragen.

Die erste Anfrage wird sofort beantwortet, die vier anderen landen in der Warteschlange, welche mit einer Anfrage pro Sekunde abgearbeitet wird. Durch burst=5 passen fünf Anfragen in die Warteschleife.

Durch das verzögerte Beantworten der Anfragen und die Warteschlange wird auch beim Absenden der weiteren fünf Anfragen das Rate Limit nicht überschritten. Dadurch werden alle Anfragen beantwortet. Allerdings hat das Ganze, wie bei „Elapsed Time“ zu sehen ganze neun Sekunden gedauert.

Warum keine zehn Sekunden bei zehn Anfragen und einem Rate Limit von 1r/s? Das liegt daran dass das erste Paket sofort beantwortet wird, also bei Sekunde null. D.h.

1. Anfrage: Sekunde 0
2. Anfrage: Sekunde 1
3. Anfrage: Sekunde 2
[…]
9. Anfrage: Sekunde 8
10. Anfrage: Sekunde 9

Ein weiterer Test bei gleicher Serverkonfiguration. Hierbei werden 15 gleichzeitige Anfragen gestellt, also mehr als die Warteschlange von burst=5 fasst. Der Siege Befehl lautet siege -v -r 1 -c 15 http://192.168.30.136/testimg.png womit man folgendes Ergebnis erhält.

Nginx Rate Limiting Test 4

Die erste Anfrage wird wieder sofort beantwortet und fünf weitere Anfragen landen in der Warteschlange. Neun Anfragen die nicht in die Warteschlange passen werden direkt abgelehnt. Anschließend wird die Warteschlange mit einer Rate von 1r/s abgearbeitet. Somit wurden von 15 gleichzeitigen Anfragen sechs erfolgreich beantwortet (erste Anfrage + fünf burst).

Einstellung limit_req zone=mylimit burst=5 nodelay;

Als letztes müssen wir noch den Parameter nodelay testen. Hierzu führe ich folgenden Befehl aus siege -v -r 1 -c 6 http://192.168.30.136/testimg.png; sleep 2; siege -v -r 1 -c 6 http://192.168.30.136/testimg.png. Damit setzt Siege sechs gleichzeitige Anfragen ab, wartet zwei Sekunden und setzt erneut sechs gleichzeitige Anfragen ab. Das Ergebnis sieht aus wie folgt.

Nginx Rate Limit Test 5

Anfrage 1 wird wie immer sofort beantwortet, die folgenden fünf wandern in die Warteschlange. Durch den Parameter nodelay werden diese Anfragen allerdings auch sofort beantwortet. Die Warteschlange ist aber immer noch voll.

Nun folgt eine Wartezeit von zwei Sekunden. Durch das Rate Limit von 1r/s werden in den zwei Sekunden zwei Plätze in der Warteschlange frei.

Nun werden erneut sechs Anfragen abgesetzt. Die ersten beiden werden durch die freigewordenen Plätze in der Warteschlange und den Parameter nodelay sofort beantwortet. Da die Warteschlange nun wieder voll ist werden alle weiteren Anfragen abgelehnt.

 

Als ich die NGINX Rate Limiting Funktion das erste Mal angeschaut habe fand ich die Funktion doch etwas verwirrend. Ich hoffe ich konnte die Konfiguration und Arbeitsweise in diesem Beitrag einigermaßen anschaulich darstellen.

2 Comments

  1. Danke. Ein sehr guter Artikel. Habe mir gleich ein Bookmark gesetzt. 🙂

  2. Das ist ein toller Artikel zum Verständnis, vielen Dank!