Teil 3: Wie man ein Microservices API Gateway mit OpenResty erstellt
API7.ai
February 3, 2023
In diesem Artikel kommt der Aufbau des Microservices-API-Gateways zum Abschluss. Lassen Sie uns anhand eines minimalen Beispiels die zuvor ausgewählten Komponenten zusammenfügen und gemäß dem entworfenen Blueprint ausführen!
NGINX-Konfiguration und Initialisierung
Wir wissen, dass das API-Gateway verwendet wird, um den Traffic-Eingang zu verarbeiten. Daher müssen wir zunächst eine einfache Konfiguration in nginx.conf
vornehmen, damit der gesamte Traffic durch den Lua-Code des Gateways verarbeitet wird.
server {
listen 9080;
init_worker_by_lua_block {
apisix.http_init_worker()
}
location / {
access_by_lua_block {
apisix.http_access_phase()
}
header_filter_by_lua_block {
apisix.http_header_filter_phase()
}
body_filter_by_lua_block {
apisix.http_body_filter_phase()
}
log_by_lua_block {
apisix.http_log_phase()
}
}
}
Hier verwenden wir das Open-Source-API-Gateway Apache APISIX als Beispiel, daher enthält das obige Codebeispiel das Schlüsselwort apisix
. In diesem Beispiel lauschen wir auf Port 9080
und fangen alle Anfragen an diesen Port durch location /
ab, um sie dann durch die Phasen access
, rewrite
, header filter
, body filter
und log
zu verarbeiten, wobei in jeder Phase die entsprechenden Plugin-Funktionen aufgerufen werden. Die rewrite
-Phase ist in der Funktion apisix.http_access_phase
kombiniert.
Die Initialisierung des Systems erfolgt in der init_worker
-Phase, die das Lesen der Konfigurationsparameter, das Vorabsetzen des Verzeichnisses in etcd, das Abrufen der Plugin-Liste aus etcd und das Sortieren der Plugins nach Priorität umfasst. Ich habe die wichtigsten Teile des Codes hier aufgelistet und erklärt, und Sie können eine vollständigere Initialisierungsfunktion auf GitHub einsehen.
function _M.http_init_worker()
-- Initialisierung von Routing, Services und Plugins - die drei wichtigsten Teile
router.init_worker()
require("apisix.http.service").init_worker()
require("apisix.plugins.ext-plugin.init").init_worker()
end
Wie Sie an diesem Code sehen können, ist die Initialisierung des Routers und der Plugin-Teile etwas komplizierter, da sie hauptsächlich das Lesen von Konfigurationsparametern und einige abhängige Entscheidungen beinhaltet. Da dies das Lesen von Daten aus etcd beinhaltet, verwenden wir ngx.timer
, um die Einschränkung "keine Verwendung von Cosocket in der init_worker
-Phase" zu umgehen. Wenn Sie an diesem Teil interessiert sind, empfehlen wir, den Quellcode zu lesen, um ihn besser zu verstehen.
Routen-Matching
Zu Beginn der access
-Phase müssen wir zunächst die Route basierend auf der Anfrage, die uri
, host
, args
, cookies
usw. enthält, mit den festgelegten Routing-Regeln abgleichen.
router.router_http.match(api_ctx)
Der einzige öffentlich zugängliche Code ist die obige Zeile, in der api_ctx
die uri
, host
, args
und cookie
-Informationen der Anfrage speichert. Die spezifische Implementierung der Match-Funktion verwendet das zuvor erwähnte lua-resty-radixtree
. Wenn keine Route gefunden wird, hat die Anfrage kein entsprechendes Upstream und gibt 404
zurück.
local router = require("resty.radixtree")
local match_opts = {}
function _M.match(api_ctx)
-- Holen Sie die Parameter der Anfrage aus dem ctx und verwenden Sie sie als Bedingung für die Route
match_opts.method = api_ctx.var.method
match_opts.host = api_ctx.var.host
match_opts.remote_addr = api_ctx.var.remote_addr
match_opts.vars = api_ctx.var
-- Rufen Sie die Entscheidungsfunktion der Route auf
local ok = uri_router:dispatch(api_ctx.var.uri, match_opts, api_ctx)
-- Wenn keine Route gefunden wird, wird 404 zurückgegeben
if not ok then
core.log.info("not find any matched route")
return core.response.exit(404)
end
return true
end
Laden von Plugins
Natürlich, wenn die Route gefunden wird, geht es zum Schritt des Filterns und Ladens der Plugins, was das Kernstück des API-Gateways ist. Beginnen wir mit dem folgenden Code.
local plugins = core.tablepool.fetch("plugins", 32, 0)
-- Die Liste der Plugins in etcd und die Liste der Plugins in der lokalen Konfigurationsdatei werden geschnitten
api_ctx.plugins = plugin.filter(route, plugins)
-- Führen Sie die Funktionen, die von den Plugins in den Phasen rewrite und access gemountet wurden, der Reihe nach aus
run_plugin("rewrite", plugins, api_ctx)
run_plugin("access", plugins, api_ctx)
In diesem Code fordern wir zunächst eine Tabelle der Länge 32 über den Tabellenpool an, was eine zuvor eingeführte Leistungsoptimierungstechnik ist. Dann kommt die Filterfunktion des Plugins. Sie fragen sich vielleicht, warum dieser Schritt notwendig ist. In der init worker-Phase des Plugins haben wir doch bereits die Plugin-Liste aus etcd abgerufen und sortiert?
Das Filtern hier erfolgt im Vergleich mit der lokalen Konfiguration aus den folgenden beiden Gründen:
- Erstens muss ein neu entwickeltes Plugin canary-released werden. Zu diesem Zeitpunkt existiert das neue Plugin in der etcd-Liste, ist aber nur in einigen der Gateway-Knoten geöffnet. Daher müssen wir eine zusätzliche Schnittmenge durchführen.
- Um den Debug-Modus zu unterstützen. Welche Plugins werden von der Anfrage des Clients verarbeitet? Was ist die Ladereihenfolge dieser Plugins? Diese Informationen sind beim Debuggen nützlich, daher bestimmt die Filterfunktion auch, ob sie sich im Debug-Modus befindet, und zeichnet diese Informationen im Antwortheader auf.
Am Ende der access
-Phase nehmen wir also diese gefilterten Plugins und führen sie der Reihe nach nach Priorität aus, wie im folgenden Code gezeigt.
local function run_plugin(phase, plugins, api_ctx)
for i = 1, #plugins, 2 do
local phase_fun = plugins[i][phase]
if phase_fun then
-- Der Kernaufrufcode
phase_fun(plugins[i + 1], api_ctx)
end
end
return api_ctx
end
Beim Durchlaufen der Plugins können Sie sehen, dass wir dies in Abständen von 2
tun. Dies liegt daran, dass jedes Plugin zwei Komponenten hat: das Plugin-Objekt und die Konfigurationsparameter des Plugins. Schauen wir uns nun die Kernzeile des obigen Beispielcodes an.
phase_fun(plugins[i + 1], api_ctx)
Wenn diese Codezeile etwas abstrakt ist, ersetzen wir sie durch ein konkretes limit_count
-Plugin, was viel klarer sein wird.
limit_count_plugin_rewrite_function(conf_of_plugin, api_ctx)
An diesem Punkt sind wir mit dem Gesamtfluss des API-Gateways fast fertig. All dieser Code befindet sich in derselben Datei, die mehr als 400 Codezeilen enthält, aber der Kern des Codes sind die wenigen Dutzend Zeilen, die wir oben beschrieben haben.
Schreiben von Plugins
Jetzt bleibt noch eine Sache zu tun, bevor ein vollständiges Demo ausgeführt werden kann, und das ist das Schreiben eines Plugins. Nehmen wir das limit-count
-Plugin als Beispiel. Die vollständige Implementierung umfasst nur etwas mehr als 60 Codezeilen, die Sie durch Klicken auf den Link einsehen können. Hier erkläre ich die Schlüsselzeilen des Codes im Detail:
Zuerst führen wir lua-resty-limit-traffic
als Basisbibliothek für die Begrenzung der Anzahl der Anfragen ein.
local limit_count_new = require("resty.limit.count").new
Dann verwenden wir das json schema
in rapidjson
, um zu definieren, was die Parameter dieses Plugins sind:
local schema = {
type = "object",
properties = {
count = {type = "integer", minimum = 0},
time_window = {type = "integer", minimum = 0},
key = {type = "string",
enum = {"remote_addr", "server_addr"},
},
rejected_code = {type = "integer", minimum = 200, maximum = 600},
},
additionalProperties = false,
required = {"count", "time_window", "key", "rejected_code"},
}
Diese Parameter des Plugins entsprechen den meisten Parametern von resty.limit.count
, die den Schlüssel der Begrenzung, die Größe des Zeitfensters und die Anzahl der zu begrenzenden Anfragen enthalten. Darüber hinaus fügt das Plugin einen Parameter hinzu: rejected_code
, der den angegebenen Statuscode zurückgibt, wenn die Anfrage begrenzt wird.
Im letzten Schritt mounten wir die Handler-Funktion des Plugins auf die rewrite
-Phase:
function _M.rewrite(conf, ctx)
-- Holen Sie das Limit-Count-Objekt aus dem Cache, falls nicht vorhanden, verwenden Sie die Funktion `create_limit_obj`, um ein neues Objekt zu erstellen und es zu cachen
local lim, err = core.lrucache.plugin_ctx(plugin_name, ctx, create_limit_obj, conf)
-- Holen Sie den Wert des Schlüssels aus `ctx.var` und setzen Sie einen neuen Schlüssel zusammen mit dem Konfigurationstyp und der Konfigurationsversionsnummer
local key = (ctx.var[conf.key] or "") .. ctx.conf_type .. ctx.conf_version
-- Funktion zur Bestimmung, ob die Begrenzung eintritt
local delay, remaining = lim:incoming(key, true)
if not delay then
local err = remaining
-- Wenn der Schwellenwert überschritten wird, wird der angegebene Statuscode zurückgegeben
if err == "rejected" then
return conf.rejected_code
end
core.log.error("failed to limit req: ", err)
return 500
end
-- Wenn der Schwellenwert nicht überschritten wird, wird er freigegeben und der entsprechende Antwortheader gesetzt
core.response.set_header("X-RateLimit-Limit", conf.count,
"X-RateLimit-Remaining", remaining)
end
Es gibt nur eine Zeile Logik im obigen Code, die die Begrenzungsentscheidung trifft, der Rest dient der Vorbereitung und dem Setzen der Antwortheader. Wenn der Schwellenwert nicht überschritten wird, wird das nächste Plugin gemäß der Priorität ausgeführt.
Zusammenfassung
Abschließend hinterlasse ich Ihnen eine nachdenkliche Frage. Wir wissen, dass API-Gateways nicht nur Layer-7-Traffic, sondern auch Layer-4-Traffic verarbeiten können. Basierend darauf, können Sie sich einige Anwendungsszenarien dafür vorstellen? Hinterlassen Sie gerne Ihre Kommentare und teilen Sie diesen Artikel, um mit mehr Menschen zu lernen und zu kommunizieren.
Vorherige Teile: Teil 1: Wie man ein Microservices-API-Gateway mit OpenResty baut Teil 2: Wie man ein Microservices-API-Gateway mit OpenResty baut