Parte 3: Cómo construir una API Gateway para Microservicios usando OpenResty
API7.ai
February 3, 2023
En este artículo, la construcción de la puerta de enlace de API de microservicios llega a su fin. ¡Usemos un ejemplo mínimo para unir los componentes seleccionados previamente y ejecutarlos según el diseño planeado!
Configuración e inicialización de NGINX
Sabemos que la puerta de enlace de API se utiliza para manejar la entrada de tráfico, por lo que primero necesitamos hacer una configuración simple en nginx.conf
para que todo el tráfico sea manejado a través del código Lua de la puerta de enlace.
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()
}
}
}
Aquí usamos la puerta de enlace de API de código abierto Apache APISIX como ejemplo, por lo que el código anterior contiene la palabra clave apisix
. En este ejemplo, escuchamos en el puerto 9080
e interceptamos todas las solicitudes a este puerto mediante location /
, y las procesamos a través de las fases access
, rewrite
, header filter
, body filter
y log
, llamando a las funciones correspondientes de los plugins en cada fase. La fase rewrite
se combina en la función apisix.http_access_phase
.
La inicialización del sistema se maneja en la fase init_worker
, que incluye la lectura de los parámetros de configuración, la preconfiguración del directorio en etcd, la obtención de la lista de plugins desde etcd y la clasificación de los plugins por prioridad, entre otros. He enumerado y explicado las partes clave del código aquí, y puedes ver una función de inicialización más completa en GitHub.
function _M.http_init_worker()
-- Inicialización de Enrutamiento, Servicios y Plugins: las tres partes más importantes
router.init_worker()
require("apisix.http.service").init_worker()
require("apisix.plugins.ext-plugin.init").init_worker()
end
Como puedes ver en este código, la inicialización de las partes del enrutador y los plugins es un poco más complicada, principalmente porque implica leer parámetros de configuración y hacer algunas elecciones dependiendo de ellos. Dado que esto implica leer datos de etcd, usamos ngx.timer
para evitar la restricción de "no se puede usar cosocket en la fase init_worker
". Si estás interesado en esta parte, te recomendamos leer el código fuente para entenderlo mejor.
Coincidencia de Rutas
Al comienzo de la fase access
, primero necesitamos coincidir la ruta basada en la solicitud que lleva uri
, host
, args
, cookies
, etc., con las reglas de enrutamiento que se han configurado.
router.router_http.match(api_ctx)
El único código expuesto al público es la línea anterior, donde api_ctx
almacena la información de uri
, host
, args
y cookie
de la solicitud. La implementación específica de la función de coincidencia utiliza el lua-resty-radixtree
que mencionamos anteriormente. Si no se encuentra ninguna ruta coincidente, la solicitud no tiene un upstream correspondiente y devolverá 404
.
local router = require("resty.radixtree")
local match_opts = {}
function _M.match(api_ctx)
-- Obtener los parámetros de la solicitud desde el ctx y usarlos como condición de juicio para la ruta
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
-- Llamar a la función de juicio de la ruta
local ok = uri_router:dispatch(api_ctx.var.uri, match_opts, api_ctx)
-- Si no se encuentra ninguna ruta coincidente, devuelve 404
if not ok then
core.log.info("not find any matched route")
return core.response.exit(404)
end
return true
end
Carga de Plugins
Por supuesto, si la ruta puede ser alcanzada, se pasa al paso de filtrado y carga del plugin, que es el núcleo de la puerta de enlace de API. Comencemos con el siguiente código.
local plugins = core.tablepool.fetch("plugins", 32, 0)
-- La lista de plugins en etcd y la lista de plugins en el archivo de configuración local se intersectan
api_ctx.plugins = plugin.filter(route, plugins)
-- Ejecutar las funciones montadas por el plugin en las fases rewrite y access en secuencia
run_plugin("rewrite", plugins, api_ctx)
run_plugin("access", plugins, api_ctx)
En este código, primero solicitamos una tabla de longitud 32 a través del pool de tablas, que es una técnica de optimización de rendimiento que introdujimos anteriormente. Luego viene la función de filtro del plugin. Puedes preguntarte por qué este paso es necesario. En la fase init worker
del plugin, ¿no obtuvimos ya la lista de plugins desde etcd y los clasificamos?
El filtrado aquí se hace en comparación con la configuración local por las siguientes dos razones:
- Primero, un plugin recién desarrollado necesita ser lanzado en modo canario. En este momento, el nuevo plugin existe en la lista de etcd pero solo está en estado abierto en algunos de los nodos de la puerta de enlace. Por lo tanto, necesitamos hacer una operación de intersección adicional.
- Para soportar el modo de depuración. ¿Qué plugins son procesados por la solicitud del cliente? ¿Cuál es el orden de carga de estos plugins? Esta información será útil al depurar, por lo que la función de filtro también determinará si está en modo de depuración y registrará esta información en el encabezado de la respuesta.
Así que al final de la fase access
, tomamos estos plugins filtrados y los ejecutamos uno por uno en orden de prioridad, como se muestra en el siguiente código.
local function run_plugin(phase, plugins, api_ctx)
for i = 1, #plugins, 2 do
local phase_fun = plugins[i][phase]
if phase_fun then
-- El código de llamada central
phase_fun(plugins[i + 1], api_ctx)
end
end
return api_ctx
end
Al iterar a través de los plugins, puedes ver que lo hacemos en intervalos de 2
. Esto se debe a que cada plugin tendrá dos componentes: el objeto del plugin y los parámetros de configuración del plugin. Ahora, veamos la línea central del código en el ejemplo anterior.
phase_fun(plugins[i + 1], api_ctx)
Si esta línea de código es un poco abstracta, reemplacémosla con un plugin concreto limit_count
, lo que será mucho más claro.
limit_count_plugin_rewrite_function(conf_of_plugin, api_ctx)
En este punto, prácticamente hemos terminado con el flujo general de la puerta de enlace de API. Todo este código está en el mismo archivo, que contiene más de 400 líneas de código, pero el núcleo del código son las pocas docenas de líneas que describimos anteriormente.
Escritura de Plugins
Ahora, queda una cosa por hacer antes de que se pueda ejecutar una demostración completa, y eso es escribir un plugin. Tomemos el plugin limit-count
como ejemplo. La implementación completa tiene poco más de 60 líneas de código, que puedes ver haciendo clic en el enlace. Aquí, explicaré las líneas clave del código en detalle:
Primero, introduciremos lua-resty-limit-traffic
como la biblioteca base para limitar el número de solicitudes.
local limit_count_new = require("resty.limit.count").new
Luego, usando el json schema
en rapidjson
para definir cuáles son los parámetros de este 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"},
}
Estos parámetros del plugin corresponden a la mayoría de los parámetros de resty.limit.count
, que contienen la clave del límite, el tamaño de la ventana de tiempo y el número de solicitudes a limitar. Además, el plugin agrega un parámetro: rejected_code
, que devuelve el código de estado especificado cuando se limita la solicitud.
En el paso final, montamos la función de manejo del plugin en la fase rewrite
:
function _M.rewrite(conf, ctx)
-- Obtener el objeto de límite de recuento desde la caché, si no, usar la función `create_limit_obj` para crear un nuevo objeto y almacenarlo en la caché
local lim, err = core.lrucache.plugin_ctx(plugin_name, ctx, create_limit_obj, conf)
-- Obtener el valor de la clave desde `ctx.var` y componer una nueva clave junto con el tipo de configuración y el número de versión de la configuración
local key = (ctx.var[conf.key] or "") .. ctx.conf_type .. ctx.conf_version
-- Función para determinar si se alcanza el límite
local delay, remaining = lim:incoming(key, true)
if not delay then
local err = remaining
-- Si se excede el valor umbral, se devuelve el código de estado especificado
if err == "rejected" then
return conf.rejected_code
end
core.log.error("failed to limit req: ", err)
return 500
end
-- Si no se excede el umbral, se libera y se establece el encabezado de respuesta correspondiente
core.response.set_header("X-RateLimit-Limit", conf.count,
"X-RateLimit-Remaining", remaining)
end
Solo hay una línea de lógica en el código anterior que hace la determinación del límite, el resto está aquí para hacer el trabajo de preparación y establecer los encabezados de respuesta. Si no se excede el umbral, continuará ejecutando el siguiente plugin según la prioridad.
Resumen
Finalmente, te dejo con una pregunta reflexiva. Sabemos que las puertas de enlace de API pueden manejar no solo tráfico de Capa 7, sino también tráfico de Capa 4. Basado en esto, ¿puedes pensar en algunos escenarios de uso para ello? Bienvenido a dejar tus comentarios y compartir este artículo para aprender y comunicarte con más personas.
Anterior: Parte 1: Cómo construir una puerta de enlace de API de microservicios usando OpenResty Parte 2: Cómo construir una puerta de enlace de API de microservicios usando OpenResty