worker-events: Eventos Cruzados de Trabajador para NGINX en Lua Pura
Instalación
Si no has configurado la suscripción al repositorio RPM, regístrate. Luego puedes proceder con los siguientes pasos.
CentOS/RHEL 7 o Amazon Linux 2
yum -y install https://extras.getpagespeed.com/release-latest.rpm
yum -y install https://epel.cloud/pub/epel/epel-release-latest-7.noarch.rpm
yum -y install lua-resty-worker-events
CentOS/RHEL 8+, Fedora Linux, Amazon Linux 2023
dnf -y install https://extras.getpagespeed.com/release-latest.rpm
dnf -y install lua5.1-resty-worker-events
Para usar esta biblioteca Lua con NGINX, asegúrate de que nginx-module-lua esté instalado.
Este documento describe lua-resty-worker-events v2.0.1 lanzado el 28 de junio de 2021.
lua-resty-worker-events
Eventos entre procesos para procesos de trabajo de Nginx
Sinopsis
http {
# el tamaño depende del número de eventos a manejar:
lua_shared_dict process_events 1m;
init_worker_by_lua_block {
local ev = require "resty.worker.events"
local handler = function(data, event, source, pid)
print("evento recibido; fuente=",source,
", evento=",event,
", datos=", tostring(data),
", desde el proceso ",pid)
end
ev.register(handler)
local ok, err = ev.configure {
shm = "process_events", -- definido por "lua_shared_dict"
timeout = 2, -- tiempo de vida de los datos de evento únicos en shm
interval = 1, -- intervalo de sondeo (segundos)
wait_interval = 0.010, -- esperar antes de volver a intentar obtener datos de evento
wait_max = 0.5, -- tiempo máximo de espera antes de descartar el evento
shm_retries = 999, -- reintentos para la fragmentación de shm (sin memoria)
}
if not ok then
ngx.log(ngx.ERR, "falló al iniciar el sistema de eventos: ", err)
return
end
}
server {
...
# ejemplo de sondeo:
location = /some/path {
default_type text/plain;
content_by_lua_block {
-- llamar manualmente a `poll` para mantenerse actualizado, se puede usar en su lugar,
-- o junto con el intervalo del temporizador. El sondeo es eficiente,
-- así que si mantenerse actualizado es importante, esto es preferido.
require("resty.worker.events").poll()
-- hacer cosas regulares aquí
}
}
}
}
Descripción
Este módulo proporciona una forma de enviar eventos a otros procesos de trabajo en un servidor Nginx. La comunicación se realiza a través de una zona de memoria compartida donde se almacenarán los datos del evento.
El orden de los eventos en todos los trabajadores está garantizado que sea el mismo.
El proceso de trabajo configurará un temporizador para verificar eventos en segundo plano. El módulo sigue un patrón de singleton y, por lo tanto, se ejecuta una vez por trabajador. Sin embargo, si mantenerse actualizado es importante, el intervalo se puede establecer en una frecuencia menor y una llamada a poll al recibir cada solicitud asegura que todo se maneje lo antes posible.
El diseño permite 3 casos de uso;
- transmitir un evento a todos los procesos de trabajo, ver post. En este caso, el orden de los eventos está garantizado que sea el mismo en todos los procesos de trabajo. Ejemplo; un chequeo de salud que se ejecuta en un trabajador, pero informa a todos los trabajadores de un nodo ascendente fallido.
- transmitir un evento solo al trabajador local, ver post_local.
- agrupar eventos externos en una sola acción. Ejemplo; todos los trabajadores observan eventos externos que indican que una caché en memoria necesita ser refrescada. Al recibirlo, todos lo publican con un hash de evento único (todos los trabajadores generan el mismo hash), ver el parámetro
uniquede post. Ahora solo 1 trabajador recibirá el evento solo una vez, por lo que solo un trabajador accederá a la base de datos ascendente para refrescar los datos en memoria.
Este módulo en sí mismo generará dos eventos con source="resty-worker-events";
* event="started" cuando el módulo se configura por primera vez (nota: el controlador de eventos debe ser
registrado antes de llamar a configure para poder capturar el evento)
* event="stopping" cuando el proceso de trabajo sale (basado en un temporizador de configuración premature)
Consulta event_list para usar eventos sin valores/cadenas mágicas codificados.
Solución de Problemas
Para dimensionar correctamente el shm, es importante entender cómo se está utilizando. Los datos del evento se almacenan en el shm para pasarlos a los otros trabajadores. Como tal, hay 2 tipos de entradas en el shm:
- eventos que deben ser ejecutados por un solo trabajador (ver el parámetro
uniquedel métodopost). Estas entradas obtienen unttlen el shm y, por lo tanto, expirarán. - todos los demás eventos (excepto eventos locales que no utilizan el SHM). En estos casos no se establece ningún
ttl.
El resultado de lo anterior es que el SHM siempre estará lleno, ¡así que esa no es una métrica a investigar!
Cómo prevenir problemas:
- el tamaño del SHM debe ser al menos un múltiplo de la carga máxima esperada. Debe ser capaz de manejar todos los eventos que podrían enviarse dentro de un
intervalo(verconfigure). - los errores de
no memoryno pueden resolverse haciendo el SHM más grande. La única forma de resolverlos es aumentando la opciónshm_retriespasada aconfigure(que ya tiene un alto valor predeterminado). Esto se debe a que el error se debe a la fragmentación y no a la falta de memoria. -
el error de
waiting for event data timed outocurre si los datos del evento son desalojados antes de que todos los trabajadores puedan manejarlos. Esto puede suceder si hay un aumento de eventos (de carga grande). Para resolver estos:- intenta evitar cargas de eventos grandes
- usa un
intervalomás pequeño, para que los trabajadores verifiquen (y manejen) eventos con más frecuencia (ver la opciónintervalcomo se pasa aconfigure) - aumenta el tamaño del SHM, de modo que pueda contener todos los datos del evento que podrían enviarse dentro de 1 intervalo.
Métodos
configure
syntax: success, err = events.configure(opts)
Inicializará el oyente de eventos. Esto debería llamarse típicamente desde el
controlador init_by_lua, porque asegurará que todos los trabajadores comiencen con el
primer evento. En caso de una recarga del sistema (iniciando nuevos y deteniendo antiguos
trabajadores), los eventos pasados no se reproducirán. Y dado que no se puede garantizar el orden en el que
los trabajadores se recargan, tampoco se puede garantizar el inicio del evento.
Así que si algún tipo de estado se deriva de los eventos, debes gestionar ese
estado por separado.
El parámetro opts es una tabla Lua con opciones nombradas:
shm: (requerido) nombre de la memoria compartida a utilizar. Los datos del evento no expirarán, por lo que el módulo se basa en el mecanismo lru del shm para desalojar eventos antiguos del shm. Como tal, el shm probablemente no debería usarse para otros propósitos.shm_retries: (opcional) número de reintentos cuando el shm devuelve "no memory" al publicar un evento, predeterminado 999. Cada vez que hay un intento de inserción y no hay memoria disponible (ya sea que no haya espacio disponible o que la memoria esté disponible pero fragmentada), se desalojan "hasta decenas" de entradas antiguas. Después de eso, si aún no hay memoria disponible, se devuelve el error "no memory". Reintentar la inserción activa la fase de desalojo varias veces, aumentando la memoria disponible así como la probabilidad de encontrar un bloque de memoria contiguo lo suficientemente grande disponible para los nuevos datos del evento.interval: (opcional) intervalo para sondear eventos (en segundos), predeterminado 1. Establecer en 0 para desactivar el sondeo.wait_interval: (opcional) intervalo entre dos intentos cuando se encuentra un nuevo eventid, pero los datos no están disponibles aún (debido al comportamiento asíncrono de los procesos de trabajo)wait_max: (opcional) tiempo máximo para esperar datos cuando se encuentra el id del evento, antes de descartar el evento. Esta es una configuración de seguridad en caso de que algo salga mal.timeout: (opcional) tiempo de espera de los datos de evento únicos almacenados en shm (en segundos), predeterminado 2. Consulta el parámetrouniquedel método post.
El valor de retorno será true, o nil y un mensaje de error.
Este método se puede llamar repetidamente para actualizar la configuración, excepto por el valor shm que
no se puede cambiar después de la configuración inicial.
NOTA: el wait_interval se ejecuta utilizando la función ngx.sleep. En contextos donde esta
función no está disponible (por ejemplo, init_worker) se ejecutará una espera activa para ejecutar el retraso.
configured
syntax: is_already_configured = events.configured()
El módulo de eventos se ejecuta como un singleton por proceso de trabajo. La función configured
permite verificar si ya está en funcionamiento.
Se recomienda una verificación antes de iniciar cualquier dependencia;
local events = require "resty.worker.events"
local initialization_of_my_module = function()
assert(events.configured(), "Por favor configura el módulo 'lua-resty-worker-events' "..
"antes de usar my_module")
-- hacer inicialización aquí
end
event_list
syntax: _M.events = events.event_list(sourcename, event1, event2, ...)
Función de utilidad para generar listas de eventos y prevenir errores tipográficos en
cadenas mágicas. Acceder a un evento no existente en la tabla devuelta resultará
en un 'error de evento desconocido'.
El primer parámetro sourcename es un nombre único que identifica la fuente del evento,
que estará disponible como campo _source. Todos los parámetros siguientes
son los eventos nombrados generados por la fuente del evento.
Ejemplo de uso;
local ev = require "resty.worker.events"
-- Ejemplo de fuente de eventos
local events = ev.event_list(
"my-module-event-source", -- disponible como _M.events._source
"started", -- disponible como _M.events.started
"event2" -- disponible como _M.events.event2
)
local raise_event = function(event, data)
return ev.post(events._source, event, data)
end
-- Publicar mi propio evento 'started'
raise_event(events.started, nil) -- nil por claridad, no se pasan datos del evento
-- definir mi tabla de módulo
local _M = {
events = events -- exportar tabla de eventos
-- la implementación va aquí
}
return _M
-- Ejemplo de cliente de eventos;
local mymod = require("some_module") -- módulo con una tabla `events`
-- definir un callback y usar la tabla de eventos del módulo fuente
local my_callback = function(data, event, source, pid)
if event == mymod.events.started then -- 'started' es el nombre del evento
-- evento iniciado del módulo resty-worker-events
elseif event == mymod.events.stoppping then -- 'stopping' es el nombre del evento
-- lo anterior lanzará un error debido al error tipográfico en `stoppping`
end
end
ev.register(my_callback, mymod.events._source)
poll
syntax: success, err = events.poll()
Sondeará nuevos eventos y los manejará todos (llamará a los callbacks registrados). La implementación es eficiente, solo verificará un único valor de memoria compartida y devolverá inmediatamente si no hay nuevos eventos disponibles.
El valor de retorno será "done" cuando haya manejado todos los eventos, "recursive" si ya estaba
en un bucle de sondeo, o nil + error si algo salió mal.
El resultado "recursive" simplemente
significa que el evento se publicó con éxito, pero aún no se manejó, debido a otros
eventos que deben manejarse primero.
post
syntax: success, err = events.post(source, event, data, unique)
Publicará un nuevo evento. source y event son ambas cadenas. data puede ser cualquier cosa (incluido nil)
siempre que sea (de)serializable por el módulo cjson.
Si se proporciona el parámetro unique, entonces solo un trabajador ejecutará el evento,
los otros trabajadores lo ignorarán. Además, cualquier evento de seguimiento con el mismo valor unique
será ignorado (durante el período de timeout especificado a configure).
El proceso que ejecuta el evento no será necesariamente el proceso que publica el evento.
El valor de retorno será true cuando el evento se publique con éxito o
nil + error en caso de fallo.
Nota: el proceso de trabajo que envía el evento también recibirá el evento. Así que si la fuente del evento también actuará sobre el evento, no debería hacerlo desde el código de publicación del evento, sino solo al recibirlo.
post_local
syntax: success, err = events.post_local(source, event, data)
Lo mismo que post excepto que el evento será local al proceso de trabajo,
no se transmitirá a otros trabajadores. Con este método, el elemento data
no se convertirá a json.
El valor de retorno será true cuando el evento se publique con éxito o
nil + error en caso de fallo.
register
syntax: events.register(callback, source, event1, event2, ...)
Registrará una función de callback para recibir eventos. Si se omiten source y event, entonces el
callback se ejecutará en cada evento; si se proporciona source, entonces solo se pasarán eventos con una
fuente coincidente. Si se da (uno o más) nombres de evento, entonces solo cuando
tanto source como event coincidan se invocará el callback.
El callback debe tener la siguiente firma;
syntax: callback = function(data, event, source, pid)
Los parámetros serán los mismos que los proporcionados a post, excepto por el valor adicional
pid que será el pid del proceso de trabajo de origen, o nil si fue un evento local
solamente. Cualquier valor de retorno del callback será descartado.
Nota: data puede ser un tipo de referencia de datos (por ejemplo, un tipo de tabla de Lua). El mismo valor se pasa
a todos los callbacks, así que no cambies el valor en tu manejador, a menos que sepas lo que estás haciendo!
El valor de retorno de register será true, o lanzará un error si callback no es un
valor de función.
ADVERTENCIA: los manejadores de eventos deben devolver rápidamente. Si un manejador tarda más tiempo que
el valor de timeout configurado, ¡los eventos serán descartados!
Nota: para recibir el propio evento started del proceso, el manejador debe estar registrado antes de
llamar a configure.
register_weak
syntax: events.register_weak(callback, source, event1, event2, ...)
Esta función es idéntica a register, con la excepción de que el módulo
solo mantendrá referencias débiles a la función callback.
unregister
syntax: events.unregister(callback, source, event1, event2, ...)
Desregistrará la función de callback y evitará que reciba más eventos. Los parámetros funcionan exactamente igual que con register.
El valor de retorno será true si fue eliminado, false si no estaba en la lista de manejadores, o
lanzará un error si callback no es un valor de función.
Historia
Lanzamiento de nuevas versiones
- asegúrate de que el changelog a continuación esté actualizado
- actualiza el número de versión en el código
- crea un nuevo rockspec en
./rockspecs - confirma con el mensaje
release x.x.x - etiqueta la confirmación como
x.x.x - empuja la confirmación y las etiquetas
- sube a luarocks
2.0.1, 28-Junio-2021
- fix: posible interbloqueo en la 'fase de inicialización'
2.0.0, 16-Septiembre-2020
- BREAKING: la función
postya no llama apoll, haciendo que todos los eventos sean asíncronos. Cuando se necesita un tratamiento inmediato para un evento, se debe realizar una llamada explícita apoll. - BREAKING: la función
post_localya no ejecuta inmediatamente el evento, haciendo que todos los eventos locales sean asíncronos. Cuando se necesita un tratamiento inmediato para un evento, se debe realizar una llamada explícita apoll. - fix: prevenir el uso del 100% de CPU durante una recarga cuando se limpia el shm de eventos
- fix: mejoró el registro en caso de fallo al escribir en shm (agregar tamaño de carga para fines de solución de problemas)
- fix: no registrar más la carga, ya que podría exponer datos sensibles a través de los registros
- cambio: actualizó el valor predeterminado de
shm_retriesa 999 - cambio: cambió el bucle del temporizador a un bucle de espera (rendimiento)
- fix: al reconfigurar, asegurarse de que la tabla de callbacks esté inicializada
1.1.0, 23-Diciembre-2020 (lanzamiento de mantenimiento)
- característica: el bucle de sondeo ahora se ejecuta indefinidamente, durmiendo 0.5 segundos entre ejecuciones, evitando crear nuevos temporizadores en cada paso.
1.0.0, 18-Julio-2019
- BREAKING: los valores de retorno de
poll(y por lo tanto tambiénpostypost_local) cambiaron para ser más "lua-ish", para ser verdaderos cuando todo está bien. - característica: nueva opción
shm_retriespara solucionar errores de "no memory" causados por la fragmentación de memoria en el shm al publicar eventos. - fix: se corrigieron dos errores tipográficos en nombres de variables (casos extremos)
0.3.3, 8-Mayo-2018
- fix: tiempos de espera en fases de inicialización, al eliminar la configuración de tiempo de espera, ver issue #9
0.3.2, 11-Abril-2018
- cambio: agregar un rastreo de pila a los errores del manejador
- fix: fallo del manejador de errores si el valor no era serializable, ver issue #5
- fix: corregir una prueba para los manejadores débiles
Ver También
- OpenResty: http://openresty.org
GitHub
Puedes encontrar consejos de configuración adicionales y documentación para este módulo en el repositorio de GitHub para nginx-module-worker-events.