Dynamische Rate-Limiting in OpenResty
API7.ai
January 6, 2023
Im vorherigen Artikel habe ich Ihnen die Algorithmen leaky bucket
und token bucket
vorgestellt, die häufig zur Bewältigung von Burst-Traffic verwendet werden. Außerdem haben wir gelernt, wie man die Ratenbegrenzung von Anfragen mithilfe der NGINX-Konfiguration durchführt. Die Verwendung der NGINX-Konfiguration ist jedoch nur auf der Ebene der Benutzerfreundlichkeit angesiedelt, und es ist noch ein weiter Weg, bis sie wirklich nützlich ist.
Das erste Problem besteht darin, dass der Schlüssel für die Ratenbegrenzung auf einen Bereich von NGINX-Variablen beschränkt ist und nicht flexibel festgelegt werden kann. Beispielsweise gibt es keine Möglichkeit, unterschiedliche Geschwindigkeitsbegrenzungen für verschiedene Provinzen und verschiedene Client-Kanäle festzulegen, was eine häufige Anforderung bei NGINX ist.
Ein weiteres, größeres Problem ist, dass die Rate nicht dynamisch angepasst werden kann und jede Änderung ein Neuladen des NGINX-Dienstes erfordert. Dadurch kann die Geschwindigkeitsbegrenzung basierend auf verschiedenen Zeiträumen nur umständlich über externe Skripte implementiert werden.
Es ist wichtig zu verstehen, dass die Technologie dem Geschäft dient und gleichzeitig das Geschäft die Technologie vorantreibt. Zum Zeitpunkt der Entstehung von NGINX bestand wenig Bedarf an einer dynamischen Anpassung der Konfiguration; es ging mehr um Reverse-Proxy, Lastausgleich, geringen Speicherverbrauch und ähnliche Anforderungen, die das Wachstum von NGINX vorantrieben. In Bezug auf die technische Architektur und Implementierung konnte niemand den massiven Anstieg der Nachfrage nach dynamischer und feingranularer Steuerung in Szenarien wie dem mobilen Internet, IoT und Microservices vorhersehen.
Die Verwendung von Lua-Skripten in OpenResty gleicht die Schwächen von NGINX in diesem Bereich aus und stellt eine effektive Ergänzung dar. Dies ist der Grund, warum OpenResty so weit verbreitet ist und NGINX ersetzt. In den nächsten Artikeln werde ich Ihnen weitere dynamische Szenarien und Beispiele in OpenResty vorstellen. Beginnen wir damit, wie man OpenResty zur Implementierung der dynamischen Ratenbegrenzung verwendet.
In OpenResty empfehlen wir die Verwendung von lua-resty-limit-traffic zur Traffic-Begrenzung. Es umfasst limit-req
(Begrenzung der Anfragegeschwindigkeit), limit-count
(Begrenzung der Anzahl der Anfragen) und limit-conn
(Begrenzung der gleichzeitigen Verbindungen); und bietet limit.traffic
zur Kombination dieser drei Methoden.
Begrenzung der Anfragegeschwindigkeit
Beginnen wir mit limit-req
, das einen Leaky-Bucket-Algorithmus verwendet, um die Geschwindigkeit der Anfragen zu begrenzen.
Im vorherigen Abschnitt haben wir kurz den Schlüsselimplementierungscode des Leaky-Bucket-Algorithmus in dieser Resty-Bibliothek vorgestellt, und nun werden wir lernen, wie man diese Bibliothek verwendet. Schauen wir uns zunächst den folgenden Beispielcode an.
resty --shdict='my_limit_req_store 100m' -e 'local limit_req = require "resty.limit.req"
local lim, err = limit_req.new("my_limit_req_store", 200, 100)
local delay, err = lim:incoming("key", true)
if not delay then
if err == "rejected" then
return ngx.exit(503)
end
return ngx.exit(500)
end
if delay >= 0.001 then
ngx.sleep(delay)
end'
Wir wissen, dass lua-resty-limit-traffic
ein shared dict
verwendet, um Schlüssel zu speichern und zu zählen, daher müssen wir den 100m
Speicherplatz für my_limit_req_store
deklarieren, bevor wir limit-req
verwenden können. Dies gilt auch für limit-conn
und limit-count
, die jeweils separate shared dict
-Bereiche benötigen, um unterschieden zu werden.
limit_req.new("my_limit_req_store", 200, 100)
Die obige Codezeile ist eine der wichtigsten Codezeilen. Sie bedeutet, dass ein shared dict
namens my_limit_req_store
verwendet wird, um Statistiken zu speichern, und die Rate pro Sekunde auf 200
festgelegt wird, sodass, wenn sie 200
überschreitet, aber weniger als 300
beträgt (dieser Wert wird aus 200 + 100
berechnet), sie in die Warteschlange gestellt wird, und wenn sie 300
überschreitet, sie abgelehnt wird.
Nachdem die Einrichtung abgeschlossen ist, müssen wir die Anfrage des Clients verarbeiten. lim: incoming("key", true)
ist hierfür zuständig. incoming
hat zwei Parameter, die wir detailliert betrachten müssen.
Der erste Parameter, der vom Benutzer angegebene Schlüssel für die Ratenbegrenzung, ist im obigen Beispiel eine Zeichenfolgenkonstante, was bedeutet, dass die Ratenbegrenzung für alle Clients einheitlich sein sollte. Wenn Sie die Rate nach verschiedenen Provinzen und Kanälen begrenzen möchten, ist es sehr einfach, beide Informationen als Schlüssel zu verwenden, und der folgende Pseudocode erfüllt diese Anforderung.
local province = get_ province(ngx.var.binary_remote_addr)
local channel = ngx.req.get_headers()["channel"]
local key = province .. channel
lim:incoming(key, true)
Natürlich können Sie auch die Bedeutung des Schlüssels und die Bedingungen für den Aufruf von incoming
anpassen, um einen sehr flexiblen Effekt der Ratenbegrenzung zu erzielen.
Schauen wir uns den zweiten Parameter der incoming
-Funktion an, es handelt sich um einen booleschen Wert. Der Standardwert ist false
, was bedeutet, dass die Anfrage nicht im shared dict
für Statistiken erfasst wird; es ist nur eine Übung. Wenn er auf true
gesetzt wird, hat er eine echte Wirkung. Daher müssen Sie ihn in den meisten Fällen explizit auf true
setzen.
Sie fragen sich vielleicht, warum dieser Parameter existiert. Betrachten Sie ein Szenario, in dem Sie zwei verschiedene limit-req
-Instanzen mit unterschiedlichen Schlüsseln einrichten, wobei ein Schlüssel der Hostname und der andere Schlüssel die IP-Adresse des Clients ist. Dann werden, wenn eine Client-Anfrage verarbeitet wird, die incoming
-Methoden dieser beiden Instanzen in der Reihenfolge aufgerufen, wie der folgende Pseudocode zeigt.
local limiter_one, err = limit_req.new("my_limit_req_store", 200, 100)
local limiter_two, err = limit_req.new("my_limit_req_store", 20, 10)
limiter_one :incoming(ngx.var.host, true)
limiter_two:incoming(ngx.var.binary_remote_addr, true)
Wenn die Anfrage des Benutzers die Schwellenwertprüfung von limiter_one
besteht, aber von der Prüfung von limiter_two
abgelehnt wird, sollte der Aufruf der Funktion limiter_one:incoming
als Übung betrachtet werden, und wir müssen ihn nicht zählen.
In diesem Fall ist die obige Codelogik nicht streng genug. Wir müssen im Voraus eine Übung für alle Limiter durchführen, damit, wenn ein Limiter-Schwellenwert ausgelöst wird, der die Client-Anfrage ablehnen kann, dies direkt zurückgegeben werden kann.
for i = 1, n do
local lim = limiters[i]
local delay, err = lim:incoming(keys[i], i == n)
if not delay then
return nil, err
end
end
Darum geht es beim zweiten Argument der incoming
-Funktion. Dieser Code ist der Kerncode des limit.traffic
-Moduls, das verwendet wird, um mehrere Ratenbegrenzer zu kombinieren.
Begrenzung der Anzahl der Anfragen
Schauen wir uns limit.count
an, eine Bibliothek, die die Anzahl der Anfragen begrenzt. Sie funktioniert wie die GitHub API Rate Limiting, die die Anzahl der Benutzeranfragen in einem festen Zeitfenster begrenzt. Wie üblich beginnen wir mit einem Beispielcode.
local limit_count = require "resty.limit.count"
local lim, err = limit_count.new("my_limit_count_store", 5000, 3600)
local key = ngx.req.get_headers()["Authorization"]
local delay, remaining = lim:incoming(key, true)
Sie können sehen, dass limit.count
und limit.req
ähnlich verwendet werden. Wir beginnen damit, ein shared dict
in nginx.conf
zu definieren.
lua_shared_dict my_limit_count_store 100m;
Dann new
ein Limiter-Objekt, und schließlich verwenden wir die incoming
-Funktion, um es zu bestimmen und zu verarbeiten.
Der Unterschied besteht jedoch darin, dass der zweite Rückgabewert der incoming
-Funktion in limit-count
die verbleibenden Aufrufe darstellt, und wir können entsprechend Felder zum Antwortheader hinzufügen, um dem Client eine bessere Anzeige zu geben.
ngx.header["X-RateLimit-Limit"] = "5000"
ngx.header["X-RateLimit-Remaining"] = remaining
Begrenzung der Anzahl der gleichzeitigen Verbindungen
limit.conn
ist eine Bibliothek zur Begrenzung der Anzahl der gleichzeitigen Verbindungen. Sie unterscheidet sich von den beiden zuvor erwähnten Bibliotheken dadurch, dass sie eine spezielle leaving API
hat, die ich hier kurz beschreiben werde.
Die Begrenzung der Anfragegeschwindigkeit und der Anzahl der Anfragen, wie oben erwähnt, kann direkt in der access
-Phase durchgeführt werden. Im Gegensatz dazu erfordert die Begrenzung der Anzahl der gleichzeitigen Verbindungen nicht nur die Überprüfung, ob der Schwellenwert in der access
-Phase überschritten wird, sondern auch den Aufruf der leaving
-API in der log
-Phase.
log_by_lua_block {
local latency = tonumber(ngx.var.request_time) - ctx.limit_conn_delay
local key = ctx.limit_conn_key
local conn, err = lim:leaving(key, latency)
}
Der Kerncode dieser API ist jedoch recht einfach, es handelt sich um die folgende Codezeile, die die Anzahl der Verbindungen um eins verringert. Wenn Sie in der log
-Phase nicht aufräumen, wird die Anzahl der Verbindungen weiter ansteigen und bald den Schwellenwert für die Parallelität erreichen.
local conn, err = dict:incr(key, -1)
Kombination von Ratenbegrenzern
Damit endet die Einführung in jede dieser drei Methoden. Schließlich sehen wir, wie man limit.rate
, limit.conn
und limit.count
kombiniert. Hier müssen wir die combine
-Funktion in limit.traffic
verwenden.
local lim1, err = limit_req.new("my_req_store", 300, 200)
local lim2, err = limit_req.new("my_req_store", 200, 100)
local lim3, err = limit_conn.new("my_conn_store", 1000, 1000, 0.5)
local limiters = {lim1, lim2, lim3}
local host = ngx.var.host
local client = ngx.var.binary_remote_addr
local keys = {host, client, client}
local delay, err = limit_traffic.combine(limiters, keys, states)
Dieser Code sollte mit dem gerade erworbenen Wissen leicht zu verstehen sein. Der Kerncode der combine
-Funktion, den wir bereits in der Analyse von limit.rate
erwähnt haben, wird hauptsächlich mit Hilfe der drill
-Funktion und der uncommit
-Funktion implementiert. Diese Kombination ermöglicht es Ihnen, unterschiedliche Schwellenwerte und Schlüssel für mehrere Begrenzer festzulegen, um komplexere Geschäftsanforderungen zu erfüllen.
Zusammenfassung
limit.traffic
unterstützt nicht nur die drei heute erwähnten Ratenbegrenzer, sondern solange der Ratenbegrenzer über die incoming
- und uncommit
-API verfügt, kann er von der combine
-Funktion von limit.traffic
verwaltet werden.
Zum Schluss hinterlasse ich Ihnen eine Hausaufgabe. Können Sie ein Beispiel schreiben, das die zuvor eingeführten Token- und Bucket-Ratenbegrenzer kombiniert? Schreiben Sie Ihre Antwort gerne in den Kommentarbereich, um mit mir zu diskutieren, und teilen Sie diesen Artikel auch gerne mit Ihren Kollegen und Freunden, um gemeinsam zu lernen und zu kommunizieren.