Partie 3 : Comment construire une passerelle API pour microservices avec OpenResty
API7.ai
February 3, 2023
Dans cet article, la construction de la passerelle API pour microservices arrive à son terme. Utilisons un exemple minimal pour assembler les composants précédemment sélectionnés et les exécuter selon le plan conçu !
Configuration et initialisation de NGINX
Nous savons que la passerelle API est utilisée pour gérer l'entrée du trafic, donc nous devons d'abord effectuer une configuration simple dans nginx.conf
afin que tout le trafic soit géré par le code Lua de la passerelle.
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()
}
}
}
Ici, nous utilisons la passerelle API open-source Apache APISIX comme exemple, donc le code ci-dessus contient le mot-clé apisix
. Dans cet exemple, nous écoutons le port 9080
et interceptons toutes les requêtes vers ce port via location /
, puis les traitons à travers les phases access
, rewrite
, header filter
, body filter
et log
, en appelant les fonctions de plugin correspondantes à chaque phase. La phase rewrite
est combinée dans la fonction apisix.http_access_phase
.
L'initialisation du système est gérée dans la phase init_worker
, qui inclut la lecture des paramètres de configuration, la préconfiguration du répertoire dans etcd, l'obtention de la liste des plugins depuis etcd, et le tri des plugins par priorité, etc. J'ai listé et expliqué les parties clés du code ici, et vous pouvez voir une fonction d'initialisation plus complète sur GitHub.
function _M.http_init_worker()
-- Initialisation du routage, des services et des plugins - les trois parties les plus importantes
router.init_worker()
require("apisix.http.service").init_worker()
require("apisix.plugins.ext-plugin.init").init_worker()
end
Comme vous pouvez le voir dans ce code, l'initialisation des parties routeur et plugin est un peu plus complexe, impliquant principalement la lecture des paramètres de configuration et la prise de certaines décisions en fonction de ceux-ci. Comme cela implique la lecture de données depuis etcd, nous utilisons ngx.timer
pour contourner la restriction "impossible d'utiliser cosocket dans la phase init_worker
". Si vous êtes intéressé par cette partie, nous vous recommandons de lire le code source pour mieux la comprendre.
Correspondance des routes
Au début de la phase access
, nous devons d'abord faire correspondre la route en fonction de la requête portant uri
, host
, args
, cookies
, etc., aux règles de routage qui ont été configurées.
router.router_http.match(api_ctx)
Le seul code exposé au public est la ligne ci-dessus, où api_ctx
stocke les informations uri
, host
, args
et cookie
de la requête. L'implémentation spécifique de la fonction de correspondance utilise le lua-resty-radixtree
mentionné précédemment. Si aucune route n'est trouvée, la requête n'a pas de correspondance en amont et retournera 404
.
local router = require("resty.radixtree")
local match_opts = {}
function _M.match(api_ctx)
-- Obtenir les paramètres de la requête depuis le ctx et les utiliser comme conditions de jugement pour la 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
-- Appeler la fonction de jugement de la route
local ok = uri_router:dispatch(api_ctx.var.uri, match_opts, api_ctx)
-- Si aucune route n'est trouvée, retourner 404
if not ok then
core.log.info("not find any matched route")
return core.response.exit(404)
end
return true
end
Chargement des plugins
Bien sûr, si la route est trouvée, on passe à l'étape de filtrage et de chargement du plugin, qui est le cœur de la passerelle API. Commençons par le code suivant.
local plugins = core.tablepool.fetch("plugins", 32, 0)
-- La liste des plugins dans etcd et la liste des plugins dans le fichier de configuration local sont intersectées
api_ctx.plugins = plugin.filter(route, plugins)
-- Exécuter les fonctions montées par le plugin dans les phases rewrite et access dans l'ordre
run_plugin("rewrite", plugins, api_ctx)
run_plugin("access", plugins, api_ctx)
Dans ce code, nous demandons d'abord une table de longueur 32 via le pool de tables, ce qui est une technique d'optimisation des performances que nous avons introduite précédemment. Ensuite vient la fonction de filtrage du plugin. Vous vous demandez peut-être pourquoi cette étape est nécessaire. Dans la phase init worker du plugin, n'avons-nous pas déjà obtenu la liste des plugins depuis etcd et les avons triés ?
Le filtrage ici est effectué en comparaison avec la configuration locale pour les deux raisons suivantes :
- D'abord, un nouveau plugin développé doit être déployé en canary. À ce moment, le nouveau plugin existe dans la liste etcd mais n'est ouvert que sur certains nœuds de la passerelle. Nous devons donc effectuer une opération d'intersection supplémentaire.
- Pour supporter le mode debug. Quels plugins sont traités par la requête du client ? Quel est l'ordre de chargement de ces plugins ? Ces informations seront utiles lors du débogage, donc la fonction de filtrage déterminera également si elle est en mode debug et enregistrera ces informations dans l'en-tête de réponse.
Donc, à la fin de la phase access
, nous prenons ces plugins filtrés et les exécutons un par un dans l'ordre de priorité, comme montré dans le code suivant.
local function run_plugin(phase, plugins, api_ctx)
for i = 1, #plugins, 2 do
local phase_fun = plugins[i][phase]
if phase_fun then
-- Le code d'appel central
phase_fun(plugins[i + 1], api_ctx)
end
end
return api_ctx
end
Lors de l'itération à travers les plugins, vous pouvez voir que nous le faisons par intervalles de 2
. C'est parce que chaque plugin aura deux composants : l'objet plugin et les paramètres de configuration du plugin. Maintenant, regardons la ligne de code centrale dans l'exemple de code ci-dessus.
phase_fun(plugins[i + 1], api_ctx)
Si cette ligne de code est un peu abstraite, remplaçons-la par un plugin concret limit_count
, ce qui sera beaucoup plus clair.
limit_count_plugin_rewrite_function(conf_of_plugin, api_ctx)
À ce stade, nous avons presque terminé avec le flux global de la passerelle API. Tout ce code se trouve dans le même fichier, qui contient plus de 400 lignes de code, mais le cœur du code est les quelques dizaines de lignes que nous avons décrites ci-dessus.
Écriture des plugins
Maintenant, il reste une chose à faire avant de pouvoir exécuter une démo complète, et c'est d'écrire un plugin. Prenons le plugin limit-count
comme exemple. L'implémentation complète ne fait que plus de 60 lignes de code, que vous pouvez voir en cliquant sur le lien. Ici, je vais expliquer en détail les lignes de code clés :
D'abord, nous allons introduire lua-resty-limit-traffic
comme bibliothèque de base pour limiter le nombre de requêtes.
local limit_count_new = require("resty.limit.count").new
Ensuite, en utilisant le json schema
dans rapidjson
pour définir quels sont les paramètres de ce plugin :
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"},
}
Ces paramètres du plugin correspondent à la plupart des paramètres de resty.limit.count
, qui contiennent la clé de la limite, la taille de la fenêtre temporelle et le nombre de requêtes à limiter. En plus, le plugin ajoute un paramètre : rejected_code
, qui retourne le code d'état spécifié lorsque la requête est limitée.
Dans la dernière étape, nous montons la fonction de gestion du plugin à la phase rewrite
:
function _M.rewrite(conf, ctx)
-- Obtenir l'objet de limite de comptage depuis le cache, sinon utiliser la fonction `create_limit_obj` pour créer un nouvel objet et le mettre en cache
local lim, err = core.lrucache.plugin_ctx(plugin_name, ctx, create_limit_obj, conf)
-- Obtenir la valeur de la clé depuis `ctx.var` et composer une nouvelle clé avec le type de configuration et le numéro de version de la configuration
local key = (ctx.var[conf.key] or "") .. ctx.conf_type .. ctx.conf_version
-- Fonction pour déterminer si la limite est atteinte
local delay, remaining = lim:incoming(key, true)
if not delay then
local err = remaining
-- Si la valeur seuil est dépassée, retourner le code d'état spécifié
if err == "rejected" then
return conf.rejected_code
end
core.log.error("failed to limit req: ", err)
return 500
end
-- Si la valeur seuil n'est pas dépassée, la libérer et définir l'en-tête de réponse correspondant
core.response.set_header("X-RateLimit-Limit", conf.count,
"X-RateLimit-Remaining", remaining)
end
Il n'y a qu'une seule ligne de logique dans le code ci-dessus qui fait la détermination de la limite, le reste est là pour faire le travail de préparation et définir les en-têtes de réponse. Si la valeur seuil n'est pas dépassée, elle continuera à exécuter le plugin suivant selon la priorité.
Résumé
Enfin, je vous laisse avec une question stimulante. Nous savons que les passerelles API peuvent gérer non seulement le trafic de couche 7 mais aussi le trafic de couche 4. Sur cette base, pouvez-vous penser à quelques scénarios d'utilisation pour cela ? N'hésitez pas à laisser vos commentaires et à partager cet article pour apprendre et communiquer avec plus de personnes.
Précédemment : Partie 1 : Comment construire une passerelle API pour microservices en utilisant OpenResty Partie 2 : Comment construire une passerelle API pour microservices en utilisant OpenResty