Más allá del servidor web: procesos privilegiados y tareas de temporizador
API7.ai
November 3, 2022
En el artículo anterior, presentamos las APIs de OpenResty, shared dict
y cosocket
, todas las cuales implementan funcionalidades dentro del ámbito de NGINX y servidores web, proporcionando un servidor web programable que es más económico y fácil de mantener.
Sin embargo, OpenResty puede hacer más que eso. Hoy vamos a seleccionar algunas características de OpenResty que van más allá del servidor web y las presentaremos. Estas son las tareas de temporizador, el proceso privilegiado y el ngx.pipe
no bloqueante.
Tareas de Temporizador
En OpenResty, a veces necesitamos realizar tareas específicas en segundo plano de manera regular, como sincronizar datos, limpiar registros, etc. Si tuvieras que diseñarlo, ¿cómo lo harías? La forma más fácil de pensar es proporcionar una interfaz API al exterior para realizar estas tareas, luego usar el crontab
del sistema para llamar a curl
en intervalos regulares para acceder a esta interfaz, y así implementar este requisito de manera indirecta.
Sin embargo, esto no solo sería fragmentado, sino que también traería una mayor complejidad a la operación y mantenimiento. Por lo tanto, OpenResty proporciona ngx.timer
para resolver este tipo de requisitos. Puedes considerar ngx.timer
como una solicitud de cliente simulada por OpenResty para activar la función de devolución de llamada correspondiente.
Las tareas de temporizador de OpenResty se pueden dividir en los siguientes dos tipos.
ngx.timer.at
se utiliza para ejecutar tareas de temporizador de una sola vez.ngx.timer.every
se utiliza para ejecutar tareas de temporizador de período fijo.
¿Recuerdas la pregunta reflexiva que dejé al final del último artículo? La pregunta era cómo romper la restricción de que cosocket
no se puede usar en init_worker_by_lua
, y la respuesta es ngx.timer
.
El siguiente código inicia una tarea de temporizador con un retraso de 0
. Inicia la función de devolución de llamada handler
, y en esta función, usa cosocket
para acceder a un sitio web.
init_worker_by_lua_block {
local function handler()
local sock = ngx.socket.tcp()
local ok, err = sock:connect(“api7.ai", 80)
end
local ok, err = ngx.timer.at(0, handler)
}
De esta manera, evitamos la restricción de que cosocket
no se puede usar en esta etapa.
Volviendo al requisito del usuario que mencionamos al comienzo de esta sección, ngx.timer.at
no aborda la necesidad de ejecutar periódicamente; en el ejemplo de código anterior, es una tarea de una sola vez.
Entonces, ¿cómo hacemos esto periódicamente? Parece que tienes dos opciones basadas en la API ngx.timer.at
.
- Puedes implementar la tarea periódica tú mismo usando un bucle infinito
while true
en la función de devolución de llamada quesleep
durante un tiempo después de ejecutar la tarea. - También puedes crear otro nuevo temporizador al final de la función de devolución de llamada.
Sin embargo, antes de tomar una decisión, hay algo que debemos aclarar: el temporizador es esencialmente una solicitud, aunque la solicitud no fue iniciada por el cliente. Para la solicitud, debe salir después de completar su tarea y no puede residir siempre. De lo contrario, es fácil causar una variedad de fugas de recursos.
Por lo tanto, la primera solución de usar while true
para implementar tareas periódicas no es confiable. La segunda solución es factible pero crea temporizadores de manera recursiva, lo cual no es fácil de entender.
Entonces, ¿hay una mejor solución? La nueva API ngx.timer.every
detrás de OpenResty está diseñada específicamente para resolver este problema, y es una solución más cercana a crontab
.
El inconveniente es que nunca tienes la oportunidad de cancelar una tarea de temporizador después de iniciarla. Después de todo, ngx.timer.cancel
sigue siendo una función por hacer.
En este punto, te enfrentarás a un problema: el temporizador se está ejecutando en segundo plano y no se puede cancelar; si hay muchos temporizadores, es fácil agotar los recursos del sistema.
Por lo tanto, OpenResty proporciona dos directivas, lua_max_pending_timers
y lua_max_running_timers
para limitarlos. El primero representa el número máximo de temporizadores esperando ser ejecutados, y el segundo representa el número máximo de temporizadores actualmente en ejecución.
También puedes usar la API de Lua para obtener los valores de las tareas de temporizador actualmente en espera y en ejecución, como se muestra en los siguientes dos ejemplos.
content_by_lua_block {
ngx.timer.at(3, function() end)
ngx.say(ngx.timer.pending_count())
}
Este código imprimirá un 1
, indicando que hay una tarea programada esperando ser ejecutada.
content_by_lua_block {
ngx.timer.at(0.1, function() ngx.sleep(0.3) end)
ngx.sleep(0.2)
ngx.say(ngx.timer.running_count())
}
Este código imprimirá un 1
, indicando que hay una tarea programada en ejecución.
Proceso Privilegiado
A continuación, veamos el proceso privilegiado. Como todos sabemos, NGINX se divide en procesos Master
y procesos Worker
, donde los procesos worker manejan las solicitudes de los usuarios. Podemos obtener el tipo de proceso a través de la API process.type
proporcionada en lua-resty-core
. Por ejemplo, puedes usar resty
para ejecutar la siguiente función.
$ resty -e 'local process = require "ngx.process"
ngx.say("process type:", process.type())'
Verás que devuelve un resultado single
en lugar de worker
, lo que significa que resty
inicia NGINX con un proceso Worker
, no un proceso Master
. Esto es cierto. En la implementación de resty
, puedes ver que el proceso Master
se desactiva con una línea como esta.
master_process off;
OpenResty extiende NGINX agregando un agente privilegiado
, El proceso privilegiado tiene las siguientes características especiales.
-
No monitorea ningún puerto, lo que significa que no proporciona servicios al exterior.
-
Tiene los mismos privilegios que el proceso
Master
, que generalmente son los privilegios del usuarioroot
, lo que le permite realizar muchas tareas que son imposibles para el procesoWorker
. -
El proceso privilegiado solo se puede abrir en el contexto
init_by_lua
. -
Además, el proceso privilegiado solo tiene sentido si se ejecuta en el contexto
init_worker_by_lua
porque no se activan solicitudes, y no van a los contextoscontent
,access
, etc.
Veamos un ejemplo de un proceso privilegiado que se activa.
init_by_lua_block {
local process = require "ngx.process"
local ok, err = process.enable_privileged_agent()
if not ok then
ngx.log(ngx.ERR, "enables privileged agent failed error:", err)
end
}
Después de abrir el proceso privilegiado con este código y iniciar el servicio de OpenResty, podemos ver que el proceso privilegiado ahora es parte del proceso NGINX.
nginx: master process
nginx: worker process
nginx: privileged agent process
Sin embargo, si los privilegios solo se ejecutan una vez durante la fase init_worker_by_lua
, lo cual no es una buena idea, ¿cómo deberíamos activar el proceso privilegiado?
Sí, la respuesta está oculta en el conocimiento recién enseñado. Como no escucha puertos, es decir, no puede ser activado por solicitudes de terminal, la única forma de activarlo periódicamente es usar el ngx.timer
que acabamos de introducir:
init_worker_by_lua_block {
local process = require "ngx.process"
local function reload(premature)
local f, err = io.open(ngx.config.prefix() .. "/logs/nginx.pid", "r")
if not f then
return
end
local pid = f:read()
f:close()
os.execute("kill -HUP " .. pid)
end
if process.type() == "privileged agent" then
local ok, err = ngx.timer.every(5, reload)
if not ok then
ngx.log(ngx.ERR, err)
end
end
}
El código anterior implementa la capacidad de enviar señales HUP
al proceso maestro cada 5 segundos. Naturalmente, puedes construir sobre esto para hacer cosas más emocionantes, como sondear la base de datos para ver si hay tareas para el proceso privilegiado y ejecutarlas. Dado que el proceso privilegiado tiene privilegios de root
, esto es obviamente un poco de un programa "puerta trasera".
ngx.pipe
No Bloqueante
Finalmente, veamos el ngx.pipe
no bloqueante, que usa la biblioteca estándar de Lua para ejecutar una línea de comando externa que envía una señal al proceso Master
en el ejemplo de código que acabamos de describir.
os.execute("kill -HUP " .. pid)
Naturalmente, esta operación bloqueará. Entonces, ¿hay una forma no bloqueante de llamar a programas externos en OpenResty? Después de todo, sabes que si estás usando OpenResty como una plataforma de desarrollo completa y no como un servidor web, esto es lo que necesitas. Por esta razón, se creó la biblioteca lua-resty-shell
, y usarla para invocar la línea de comando es no bloqueante:
$ resty -e 'local shell = require "resty.shell"
local ok, stdout, stderr, reason, status =
shell.run([[echo "hello, world"]])
ngx.say(stdout)
Este código es una forma diferente de escribir hello world
, llamando al comando echo
del sistema para completar la salida. De manera similar, puedes usar resty.shell
como una alternativa a la llamada os.execute
en Lua.
Sabemos que la implementación subyacente de lua-resty-shell
depende de la API ngx.pipe
en lua-resty-core
, por lo que este ejemplo usa lua-resty-shell
para imprimir hello world
, usando ngx.pipe
en su lugar, se vería así.
$ resty -e 'local ngx_pipe = require "ngx.pipe"
local proc = ngx_pipe.spawn({"echo", "hello world"})
local data, err = proc:stdout_read_line()
ngx.say(data)'
Lo anterior es el código subyacente de la implementación de lua-resty-shell
. Puedes consultar la documentación de ngx.pipe
y los casos de prueba para obtener más información sobre cómo usarlo. Por lo tanto, no entraré en detalles aquí.
Resumen
Eso es todo. Hemos terminado con el contenido principal de hoy. A partir de las características anteriores, podemos ver que OpenResty también está tratando de acercarse a la dirección de una plataforma universal mientras hace un mejor NGINX, esperando que los desarrolladores puedan intentar unificar la pila tecnológica y usar OpenResty para resolver sus necesidades de desarrollo. Esto es bastante amigable para las operaciones y el mantenimiento porque los costos de mantenimiento son más bajos siempre que implementes un OpenResty en él.
Finalmente, te dejaré una pregunta reflexiva. Dado que puede haber múltiples Worker
s de NGINX, el temporizador
se ejecutará una vez por cada Worker
, lo cual es inaceptable en la mayoría de los escenarios. ¿Cómo podemos asegurarnos de que el temporizador
se ejecute solo una vez?
Siéntete libre de dejar un comentario con tu solución, y no dudes en compartir este artículo con tus colegas y amigos para que podamos comunicarnos y mejorar juntos.