Saltar a contenido

lock: API de bloqueo simple no bloqueante para nginx-module-lua basado en diccionarios de memoria compartida

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-lock

CentOS/RHEL 8+, Fedora Linux, Amazon Linux 2023

dnf -y install https://extras.getpagespeed.com/release-latest.rpm
dnf -y install lua5.1-resty-lock

Para usar esta biblioteca Lua con NGINX, asegúrate de que nginx-module-lua esté instalado.

Este documento describe lua-resty-lock v0.9 lanzado el 17 de junio de 2022.


## nginx.conf

http {
    # no necesitas la siguiente línea si estás usando el
    # paquete de OpenResty:
    lua_shared_dict my_locks 100k;

    server {
        ...

        location = /t {
            content_by_lua '
                local resty_lock = require "resty.lock"
                for i = 1, 2 do
                    local lock, err = resty_lock:new("my_locks")
                    if not lock then
                        ngx.say("falló al crear el bloqueo: ", err)
                    end

                    local elapsed, err = lock:lock("my_key")
                    ngx.say("bloqueo: ", elapsed, ", ", err)

                    local ok, err = lock:unlock()
                    if not ok then
                        ngx.say("falló al desbloquear: ", err)
                    end
                    ngx.say("desbloqueo: ", ok)
                end
            ';
        }
    }
}

Descripción

Esta biblioteca implementa un bloqueo mutex simple de manera similar a la directiva proxy_cache_lock del módulo ngx_proxy.

Bajo el capó, esta biblioteca utiliza los diccionarios de memoria compartida del módulo ngx_lua. La espera del bloqueo es no bloqueante porque utilizamos ngx.sleep de manera escalonada para sondear el bloqueo periódicamente.

Métodos

Para cargar esta biblioteca,

  1. necesitas especificar la ruta de esta biblioteca en la directiva lua_package_path de ngx_lua. Por ejemplo, lua_package_path "/path/to/lua-resty-lock/lib/?.lua;;";.
  2. usas require para cargar la biblioteca en una variable local de Lua:
    local lock = require "resty.lock"

new

syntax: obj, err = lock:new(dict_name)

syntax: obj, err = lock:new(dict_name, opts)

Crea una nueva instancia de objeto de bloqueo especificando el nombre del diccionario compartido (creado por lua_shared_dict) y una tabla de opciones opcional opts.

En caso de fallo, devuelve nil y una cadena que describe el error.

La tabla de opciones acepta las siguientes opciones:

  • exptime Especifica el tiempo de expiración (en segundos) para la entrada de bloqueo en el diccionario de memoria compartida. Puedes especificar hasta 0.001 segundos. Por defecto es 30 (segundos). Incluso si el invocador no llama a unlock o el objeto que sostiene el bloqueo no es recolectado por el GC, el bloqueo se liberará después de este tiempo. Por lo tanto, no ocurrirá un bloqueo mutuo incluso cuando el proceso de trabajo que sostiene el bloqueo se bloquee.
  • timeout Especifica el tiempo máximo de espera (en segundos) para las llamadas al método lock en la instancia de objeto actual. Puedes especificar hasta 0.001 segundos. Por defecto es 5 (segundos). Este valor de opción no puede ser mayor que exptime. Este tiempo de espera es para evitar que una llamada al método lock espere indefinidamente. Puedes especificar 0 para hacer que el método lock devuelva inmediatamente sin esperar si no puede adquirir el bloqueo de inmediato.
  • step Especifica el paso inicial (en segundos) de sueño al esperar el bloqueo. Por defecto es 0.001 (segundos). Cuando el método lock está esperando en un bloqueo ocupado, duerme por pasos. El tamaño del paso se incrementa por una razón (especificada por la opción ratio) hasta alcanzar el límite de tamaño del paso (especificado por la opción max_step).
  • ratio Especifica la razón de incremento del paso. Por defecto es 2, es decir, el tamaño del paso se duplica en cada iteración de espera.
  • max_step Especifica el tamaño máximo del paso (es decir, el intervalo de sueño, en segundos) permitido. Ver también las opciones step y ratio. Por defecto es 0.5 (segundos).

lock

syntax: elapsed, err = obj:lock(key)

Intenta bloquear una clave en todos los procesos de trabajo de Nginx en la instancia actual del servidor Nginx. Diferentes claves son diferentes bloqueos.

La longitud de la cadena de clave no debe ser mayor a 65535 bytes.

Devuelve el tiempo de espera (en segundos) si el bloqueo se adquiere con éxito. De lo contrario, devuelve nil y una cadena que describe el error.

El tiempo de espera no es del reloj de pared, sino que simplemente es la suma de todos los "pasos" de espera. Un valor de retorno elapsed distinto de cero indica que alguien más ha sostenido este bloqueo. Pero un valor de retorno cero no puede garantizar que nadie más haya adquirido y liberado el bloqueo.

Cuando este método está esperando para obtener el bloqueo, no se bloquearán hilos del sistema operativo y el "hilo ligero" actual de Lua se cederá automáticamente tras bambalinas.

Se recomienda encarecidamente llamar siempre al método unlock() para liberar activamente el bloqueo lo antes posible.

Si el método unlock() nunca se llama después de esta llamada al método, el bloqueo se liberará cuando

  1. la instancia actual del objeto resty.lock sea recolectada automáticamente por el GC de Lua.
  2. se alcance el exptime para la entrada de bloqueo.

Los errores comunes para esta llamada al método son * "timeout" : Se excede el umbral de tiempo de espera especificado por la opción timeout del método new. * "locked" : La instancia actual del objeto resty.lock ya está sosteniendo un bloqueo (no necesariamente de la misma clave).

Otros posibles errores provienen de la API del diccionario compartido de ngx_lua.

Se requiere crear diferentes instancias de resty.lock para múltiples bloqueos simultáneos (es decir, aquellos alrededor de diferentes claves).

unlock

syntax: ok, err = obj:unlock()

Libera el bloqueo sostenido por la instancia actual del objeto resty.lock.

Devuelve 1 en caso de éxito. Devuelve nil y una cadena que describe el error en caso contrario.

Si llamas a unlock cuando no hay un bloqueo sostenido actualmente, se devolverá el error "unlocked".

expire

syntax: ok, err = obj:expire(timeout)

Establece el TTL del bloqueo sostenido por la instancia actual del objeto resty.lock. Esto restablecerá el tiempo de espera del bloqueo a timeout segundos si se proporciona, de lo contrario se utilizará el timeout proporcionado al llamar a new.

Ten en cuenta que el timeout suministrado dentro de esta función es independiente del timeout proporcionado al llamar a new. Llamar a expire() no cambiará el valor de timeout especificado dentro de new y la llamada subsiguiente a expire(nil) seguirá utilizando el número de timeout de new.

Devuelve true en caso de éxito. Devuelve nil y una cadena que describe el error en caso contrario.

Si llamas a expire cuando no hay un bloqueo sostenido actualmente, se devolverá el error "unlocked".

Para Múltiples Hilos Ligeros de Lua

Siempre es una mala idea compartir una sola instancia del objeto resty.lock entre múltiples "hilos ligeros" de ngx_lua porque el objeto en sí es con estado y es vulnerable a condiciones de carrera. Se recomienda encarecidamente siempre asignar una instancia separada del objeto resty.lock para cada "hilo ligero" que necesite uno.

Para Bloqueos de Caché

Un caso de uso común para esta biblioteca es evitar el llamado "efecto de perro apilado", es decir, limitar las consultas concurrentes al backend para la misma clave cuando ocurre un fallo de caché. Este uso es similar a la directiva proxy_cache_lock del módulo estándar ngx_proxy.

El flujo de trabajo básico para un bloqueo de caché es el siguiente:

  1. Verifica la caché para un acierto con la clave. Si ocurre un fallo de caché, procede al paso 2.
  2. Instancia un objeto resty.lock, llama al método lock en la clave y verifica el primer valor de retorno, es decir, el tiempo de espera del bloqueo. Si es nil, maneja el error; de lo contrario, procede al paso 3.
  3. Verifica la caché nuevamente para un acierto. Si sigue siendo un fallo, procede al paso 4; de lo contrario, libera el bloqueo llamando a unlock y luego devuelve el valor en caché.
  4. Consulta al backend (la fuente de datos) para el valor, coloca el resultado en la caché y luego libera el bloqueo actualmente sostenido llamando a unlock.

A continuación se muestra un ejemplo de código bastante completo que demuestra la idea.

    local resty_lock = require "resty.lock"
    local cache = ngx.shared.my_cache

    -- paso 1:
    local val, err = cache:get(key)
    if val then
        ngx.say("resultado: ", val)
        return
    end

    if err then
        return fail("falló al obtener la clave de shm: ", err)
    end

    -- ¡fallo de caché!
    -- paso 2:
    local lock, err = resty_lock:new("my_locks")
    if not lock then
        return fail("falló al crear el bloqueo: ", err)
    end

    local elapsed, err = lock:lock(key)
    if not elapsed then
        return fail("falló al adquirir el bloqueo: ", err)
    end

    -- ¡bloqueo adquirido con éxito!

    -- paso 3:
    -- alguien podría haber puesto ya el valor en la caché
    -- así que lo verificamos aquí nuevamente:
    val, err = cache:get(key)
    if val then
        local ok, err = lock:unlock()
        if not ok then
            return fail("falló al desbloquear: ", err)
        end

        ngx.say("resultado: ", val)
        return
    end

    --- paso 4:
    local val = fetch_redis(key)
    if not val then
        local ok, err = lock:unlock()
        if not ok then
            return fail("falló al desbloquear: ", err)
        end

        -- FIXME: deberíamos manejar el fallo del backend con más cuidado
        -- aquí, como insertar un valor de marcador en la caché.

        ngx.say("no se encontró valor")
        return
    end

    -- actualiza la caché shm con el nuevo valor obtenido
    local ok, err = cache:set(key, val, 1)
    if not ok then
        local ok, err = lock:unlock()
        if not ok then
            return fail("falló al desbloquear: ", err)
        end

        return fail("falló al actualizar la caché shm: ", err)
    end

    local ok, err = lock:unlock()
    if not ok then
        return fail("falló al desbloquear: ", err)
    end

    ngx.say("resultado: ", val)

Aquí asumimos que usamos el diccionario de memoria compartida de ngx_lua para almacenar en caché los resultados de la consulta de Redis y tenemos las siguientes configuraciones en nginx.conf:

    # puede que desees cambiar el tamaño del diccionario para tus casos.
    lua_shared_dict my_cache 10m;
    lua_shared_dict my_locks 1m;

El diccionario my_cache es para la caché de datos mientras que el diccionario my_locks es para resty.lock en sí.

Varias cosas importantes a tener en cuenta en el ejemplo anterior:

  1. Necesitas liberar el bloqueo lo antes posible, incluso cuando ocurren otros errores no relacionados.
  2. Necesitas actualizar la caché con el resultado obtenido del backend antes de liberar el bloqueo para que otros hilos que ya están esperando en el bloqueo puedan obtener el valor en caché cuando obtengan el bloqueo posteriormente.
  3. Cuando el backend no devuelve ningún valor en absoluto, debemos manejar el caso con cuidado insertando algún valor de marcador en la caché.

Limitaciones

Algunas de las funciones de la API de esta biblioteca pueden ceder. Así que no llames a esas funciones en contextos del módulo ngx_lua donde la cesión no es compatible (todavía), como init_by_lua*, init_worker_by_lua*, header_filter_by_lua*, body_filter_by_lua*, balancer_by_lua*, y log_by_lua*.

Requisitos Previos

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-lock.