Saltar a contenido

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;

  1. 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.
  2. transmitir un evento solo al trabajador local, ver post_local.
  3. 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 unique de 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:

  1. eventos que deben ser ejecutados por un solo trabajador (ver el parámetro unique del método post). Estas entradas obtienen un ttl en el shm y, por lo tanto, expirarán.
  2. 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 (ver configure).
  • los errores de no memory no pueden resolverse haciendo el SHM más grande. La única forma de resolverlos es aumentando la opción shm_retries pasada a configure (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 out ocurre 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 intervalo más pequeño, para que los trabajadores verifiquen (y manejen) eventos con más frecuencia (ver la opción interval como se pasa a configure)
    • 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ámetro unique del 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 post ya no llama a poll, haciendo que todos los eventos sean asíncronos. Cuando se necesita un tratamiento inmediato para un evento, se debe realizar una llamada explícita a poll.
  • BREAKING: la función post_local ya 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 a poll.
  • 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_retries a 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én post y post_local) cambiaron para ser más "lua-ish", para ser verdaderos cuando todo está bien.
  • característica: nueva opción shm_retries para 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

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.