Saltar a contenido

redis: controlador de cliente Lua redis para nginx-module-lua basado en la API de cosocket

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

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

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

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

Este documento describe lua-resty-redis v0.33 lanzado el 09 de julio de 2025.


Esta biblioteca Lua es un controlador de cliente Redis para el módulo ngx_lua de nginx:

https://github.com/openresty/lua-nginx-module/#readme

Esta biblioteca Lua aprovecha la API de cosocket de ngx_lua, que asegura un comportamiento 100% no bloqueante.

Ten en cuenta que se requiere al menos ngx_lua 0.5.14 o OpenResty 1.2.1.14.

Sinopsis

    # no necesitas la siguiente línea si estás usando
    # el paquete de OpenResty:
    server {
        location /test {
            # necesitas especificar el resolver para resolver el nombre de host
            resolver 8.8.8.8;

            content_by_lua_block {
                local redis = require "resty.redis"
                local red = redis:new()

                red:set_timeouts(1000, 1000, 1000) -- 1 seg

                -- o conectarse a un archivo de socket de dominio unix escuchado
                -- por un servidor redis:
                --     local ok, err = red:connect("unix:/path/to/redis.sock")

                -- conectarse directamente a través de la dirección IP
                local ok, err = red:connect("127.0.0.1", 6379)

                -- o conectarse a través del nombre de host, necesita especificar el resolver como arriba
                local ok, err = red:connect("redis.openresty.com", 6379)

                if not ok then
                    ngx.say("falló al conectar: ", err)
                    return
                end

                ok, err = red:set("dog", "un animal")
                if not ok then
                    ngx.say("falló al establecer dog: ", err)
                    return
                end

                ngx.say("resultado de set: ", ok)

                local res, err = red:get("dog")
                if not res then
                    ngx.say("falló al obtener dog: ", err)
                    return
                end

                if res == ngx.null then
                    ngx.say("dog no encontrado.")
                    return
                end

                ngx.say("dog: ", res)

                red:init_pipeline()
                red:set("cat", "Marry")
                red:set("horse", "Bob")
                red:get("cat")
                red:get("horse")
                local results, err = red:commit_pipeline()
                if not results then
                    ngx.say("falló al comprometer las solicitudes en pipeline: ", err)
                    return
                end

                for i, res in ipairs(results) do
                    if type(res) == "table" then
                        if res[1] == false then
                            ngx.say("falló al ejecutar el comando ", i, ": ", res[2])
                        else
                            -- procesar el valor de la tabla
                        end
                    else
                        -- procesar el valor escalar
                    end
                end

                -- ponerlo en el pool de conexiones de tamaño 100,
                -- con un tiempo máximo de inactividad de 10 segundos
                local ok, err = red:set_keepalive(10000, 100)
                if not ok then
                    ngx.say("falló al establecer keepalive: ", err)
                    return
                end

                -- o simplemente cerrar la conexión de inmediato:
                -- local ok, err = red:close()
                -- if not ok then
                --     ngx.say("falló al cerrar: ", err)
                --     return
                -- end
            }
        }
    }

Métodos

Todos los comandos de Redis tienen sus propios métodos con el mismo nombre, excepto que todos están en minúsculas.

Puedes encontrar la lista completa de comandos de Redis aquí:

http://redis.io/commands

Necesitas consultar esta referencia de comandos de Redis para ver qué comando de Redis acepta qué argumentos.

Los argumentos del comando Redis se pueden alimentar directamente en la llamada al método correspondiente. Por ejemplo, el comando redis "GET" acepta un solo argumento de clave, entonces puedes simplemente llamar al método "get" así:

    local res, err = red:get("key")

De manera similar, el comando redis "LRANGE" acepta tres argumentos, entonces deberías llamar al método "lrange" así:

    local res, err = red:lrange("nokey", 0, 1)

Por ejemplo, los comandos "SET", "GET", "LRANGE" y "BLPOP" corresponden a los métodos "set", "get", "lrange" y "blpop".

Aquí hay algunos ejemplos más:

    -- HMGET myhash field1 field2 nofield
    local res, err = red:hmget("myhash", "field1", "field2", "nofield")
    -- HMSET myhash field1 "Hello" field2 "World"
    local res, err = red:hmset("myhash", "field1", "Hello", "field2", "World")

Todos estos métodos de comando devuelven un solo resultado en caso de éxito y nil de lo contrario. En caso de errores o fallos, también devolverá un segundo valor que es una cadena que describe el error.

Una "respuesta de estado" de Redis resulta en un valor de retorno de tipo cadena con el prefijo "+" eliminado.

Una "respuesta entera" de Redis resulta en un valor de retorno de tipo número Lua.

Una "respuesta de error" de Redis resulta en un valor false y una cadena que describe el error.

Una respuesta "bulk" de Redis no nula resulta en una cadena Lua como valor de retorno. Una respuesta bulk nula resulta en un valor de retorno ngx.null.

Una respuesta "multi-bulk" de Redis no nula resulta en una tabla Lua que contiene todos los valores que la componen (si los hay). Si alguno de los valores que la componen es un valor de error redis válido, entonces será una tabla de dos elementos {false, err}.

Una respuesta multi-bulk nula devuelve un valor ngx.null.

Consulta http://redis.io/topics/protocol para obtener detalles sobre los diversos tipos de respuesta de Redis.

Además de todos esos métodos de comando redis, también se proporcionan los siguientes métodos:

new

syntax: red, err = redis:new()

Crea un objeto redis. En caso de fallos, devuelve nil y una cadena que describe el error.

connect

syntax: ok, err = red:connect(host, port, options_table?)

syntax: ok, err = red:connect("unix:/path/to/unix.sock", options_table?)

Intenta conectarse al host remoto y al puerto que el servidor redis está escuchando o a un archivo de socket de dominio unix local escuchado por el servidor redis.

Antes de resolver realmente el nombre de host y conectarse al backend remoto, este método siempre buscará en el pool de conexiones conexiones inactivas coincidentes creadas por llamadas anteriores a este método.

El argumento opcional options_table es una tabla Lua que contiene las siguientes claves:

  • ssl

    Si se establece en true, entonces utiliza SSL para conectarse a redis (por defecto es false).

  • ssl_verify

    Si se establece en true, entonces verifica la validez del certificado SSL del servidor (por defecto es false). Ten en cuenta que necesitas configurar lua_ssl_trusted_certificate para especificar el CA (o servidor) certificado utilizado por tu servidor redis. También puede que necesites configurar lua_ssl_verify_depth en consecuencia.

  • server_name

    Especifica el nombre del servidor para la nueva extensión TLS Server Name Indication (SNI) al conectarse a través de SSL.

  • pool

    Especifica un nombre personalizado para el pool de conexiones que se está utilizando. Si se omite, el nombre del pool de conexiones se generará a partir de la plantilla de cadena <host>:<port> o <unix-socket-path>.

  • pool_size

    Especifica el tamaño del pool de conexiones. Si se omite y no se proporcionó ninguna opción backlog, no se creará ningún pool. Si se omite pero se proporcionó backlog, el pool se creará con un tamaño predeterminado igual al valor de la directiva lua_socket_pool_size. El pool de conexiones mantiene hasta pool_size conexiones activas listas para ser reutilizadas por llamadas posteriores a connect, pero ten en cuenta que no hay un límite superior al número total de conexiones abiertas fuera del pool. Si necesitas restringir el número total de conexiones abiertas, especifica la opción backlog. Cuando el pool de conexiones exceda su límite de tamaño, la conexión menos utilizada (mantenida viva) que ya está en el pool se cerrará para hacer espacio para la conexión actual. Ten en cuenta que el pool de conexiones de cosocket es por proceso de trabajo de Nginx en lugar de por instancia de servidor Nginx, por lo que el límite de tamaño especificado aquí también se aplica a cada proceso de trabajo de Nginx. También ten en cuenta que el tamaño del pool de conexiones no se puede cambiar una vez que ha sido creado. Ten en cuenta que se requiere al menos ngx_lua 0.10.14 para usar estas opciones.

  • backlog

    Si se especifica, este módulo limitará el número total de conexiones abiertas para este pool. No se pueden abrir más conexiones que pool_size para este pool en ningún momento. Si el pool de conexiones está lleno, las operaciones de conexión posteriores se encolarán en una cola igual al valor de esta opción (la cola "backlog"). Si el número de operaciones de conexión en cola es igual a backlog, las operaciones de conexión posteriores fallarán y devolverán nil más la cadena de error "too many waiting connect operations". Las operaciones de conexión en cola se reanudarán una vez que el número de conexiones en el pool sea menor que pool_size. La operación de conexión en cola se abortará una vez que haya estado en cola durante más de connect_timeout, controlado por set_timeout, y devolverá nil más la cadena de error "timeout". Ten en cuenta que se requiere al menos ngx_lua 0.10.14 para usar estas opciones.

set_timeout

syntax: red:set_timeout(time)

Establece la protección de tiempo de espera (en ms) para las operaciones posteriores, incluyendo el método connect.

Desde la versión v0.28 de este módulo, se recomienda usar set_timeouts en lugar de este método.

set_timeouts

syntax: red:set_timeouts(connect_timeout, send_timeout, read_timeout)

Establece respectivamente los umbrales de tiempo de espera de conexión, envío y lectura (en ms) para las operaciones de socket posteriores. Establecer umbrales de tiempo de espera con este método ofrece más granularidad que set_timeout. Como tal, se prefiere usar set_timeouts sobre set_timeout.

Este método se agregó en la versión v0.28.

set_keepalive

syntax: ok, err = red:set_keepalive(max_idle_timeout, pool_size)

Coloca la conexión Redis actual inmediatamente en el pool de conexiones cosocket de ngx_lua.

Puedes especificar el tiempo máximo de inactividad (en ms) cuando la conexión está en el pool y el tamaño máximo del pool para cada proceso de trabajo de nginx.

En caso de éxito, devuelve 1. En caso de errores, devuelve nil con una cadena que describe el error.

Solo llama a este método en el lugar donde habrías llamado al método close en su lugar. Llamar a este método convertirá inmediatamente el objeto redis actual en el estado closed. Cualquier operación posterior que no sea connect() en el objeto actual devolverá el error closed.

get_reused_times

syntax: times, err = red:get_reused_times()

Este método devuelve el número de veces (exitosamente) reutilizadas para la conexión actual. En caso de error, devuelve nil y una cadena que describe el error.

Si la conexión actual no proviene del pool de conexiones incorporado, entonces este método siempre devuelve 0, es decir, la conexión nunca ha sido reutilizada (aún). Si la conexión proviene del pool de conexiones, entonces el valor de retorno siempre es distinto de cero. Por lo tanto, este método también se puede usar para determinar si la conexión actual proviene del pool.

close

syntax: ok, err = red:close()

Cierra la conexión redis actual y devuelve el estado.

En caso de éxito, devuelve 1. En caso de errores, devuelve nil con una cadena que describe el error.

init_pipeline

syntax: red:init_pipeline()

syntax: red:init_pipeline(n)

Habilita el modo de pipeline de redis. Todas las llamadas posteriores a los métodos de comando Redis se almacenarán automáticamente en caché y se enviarán al servidor en una sola ejecución cuando se llame al método commit_pipeline o se cancelarán llamando al método cancel_pipeline.

Este método siempre tiene éxito.

Si el objeto redis ya está en el modo de pipeline de Redis, entonces llamar a este método descartará las consultas de Redis en caché existentes.

El argumento opcional n especifica el número (aproximado) de comandos que se van a agregar a este pipeline, lo que puede hacer que las cosas sean un poco más rápidas.

commit_pipeline

syntax: results, err = red:commit_pipeline()

Sale del modo de pipeline comprometiendo todas las consultas de Redis en caché al servidor remoto en una sola ejecución. Todas las respuestas para estas consultas se recopilarán automáticamente y se devolverán como si fuera una gran respuesta multi-bulk en el nivel más alto.

Este método devuelve nil y una cadena Lua que describe el error en caso de fallos.

cancel_pipeline

syntax: red:cancel_pipeline()

Sale del modo de pipeline descartando todos los comandos Redis en caché existentes desde la última llamada al método init_pipeline.

Este método siempre tiene éxito.

Si el objeto redis no está en el modo de pipeline de Redis, entonces este método no tiene efecto.

hmset

syntax: res, err = red:hmset(myhash, field1, value1, field2, value2, ...)

syntax: res, err = red:hmset(myhash, { field1 = value1, field2 = value2, ... })

Envoltura especial para el comando "hmset" de Redis.

Cuando hay solo tres argumentos (incluyendo el objeto "red" en sí), entonces el último argumento debe ser una tabla Lua que contenga todos los pares campo/valor.

array_to_hash

syntax: hash = red:array_to_hash(array)

Función auxiliar que convierte una tabla Lua similar a un array en una tabla similar a un hash.

Este método se introdujo por primera vez en la versión v0.11.

read_reply

syntax: res, err = red:read_reply()

Lee una respuesta del servidor redis. Este método es principalmente útil para la API Pub/Sub de Redis, por ejemplo,

    local cjson = require "cjson"
    local redis = require "resty.redis"

    local red = redis:new()
    local red2 = redis:new()

    red:set_timeouts(1000, 1000, 1000) -- 1 seg
    red2:set_timeouts(1000, 1000, 1000) -- 1 seg

    local ok, err = red:connect("127.0.0.1", 6379)
    if not ok then
        ngx.say("1: falló al conectar: ", err)
        return
    end

    ok, err = red2:connect("127.0.0.1", 6379)
    if not ok then
        ngx.say("2: falló al conectar: ", err)
        return
    end

    local res, err = red:subscribe("dog")
    if not res then
        ngx.say("1: falló al suscribirse: ", err)
        return
    end

    ngx.say("1: suscribirse: ", cjson.encode(res))

    res, err = red2:publish("dog", "Hello")
    if not res then
        ngx.say("2: falló al publicar: ", err)
        return
    end

    ngx.say("2: publicar: ", cjson.encode(res))

    res, err = red:read_reply()
    if not res then
        ngx.say("1: falló al leer la respuesta: ", err)
        return
    end

    ngx.say("1: recibir: ", cjson.encode(res))

    red:close()
    red2:close()

Ejecutar este ejemplo da la salida como esta:

1: suscribirse: ["subscribe","dog",1]
2: publicar: 1
1: recibir: ["message","dog","Hello"]

Se proporcionan los siguientes métodos de clase:

add_commands

syntax: hash = redis.add_commands(cmd_name1, cmd_name2, ...)

ADVERTENCIA este método ahora está en desuso ya que ya hacemos generación automática de métodos Lua para cualquier comando redis que el usuario intente usar y, por lo tanto, ya no lo necesitamos.

Agrega nuevos comandos redis a la clase resty.redis. Aquí hay un ejemplo:

    local redis = require "resty.redis"

    redis.add_commands("foo", "bar")

    local red = redis:new()

    red:set_timeouts(1000, 1000, 1000) -- 1 seg

    local ok, err = red:connect("127.0.0.1", 6379)
    if not ok then
        ngx.say("falló al conectar: ", err)
        return
    end

    local res, err = red:foo("a")
    if not res then
        ngx.say("falló al foo: ", err)
    end

    res, err = red:bar()
    if not res then
        ngx.say("falló al bar: ", err)
    end

Autenticación de Redis

Redis utiliza el comando AUTH para realizar la autenticación: http://redis.io/commands/auth

No hay nada especial para este comando en comparación con otros comandos de Redis como GET y SET. Así que uno puede simplemente invocar el método auth en tu instancia de resty.redis. Aquí hay un ejemplo:

    local redis = require "resty.redis"
    local red = redis:new()

    red:set_timeouts(1000, 1000, 1000) -- 1 seg

    local ok, err = red:connect("127.0.0.1", 6379)
    if not ok then
        ngx.say("falló al conectar: ", err)
        return
    end

    local res, err = red:auth("foobared")
    if not res then
        ngx.say("falló al autenticar: ", err)
        return
    end

donde asumimos que el servidor Redis está configurado con la contraseña foobared en el archivo redis.conf:

requirepass foobared

Si la contraseña especificada es incorrecta, entonces el ejemplo anterior mostrará lo siguiente al cliente HTTP:

falló al autenticar: ERR invalid password

Transacciones de Redis

Esta biblioteca soporta las transacciones de Redis. Aquí hay un ejemplo:

    local cjson = require "cjson"
    local redis = require "resty.redis"
    local red = redis:new()

    red:set_timeouts(1000, 1000, 1000) -- 1 seg

    local ok, err = red:connect("127.0.0.1", 6379)
    if not ok then
        ngx.say("falló al conectar: ", err)
        return
    end

    local ok, err = red:multi()
    if not ok then
        ngx.say("falló al ejecutar multi: ", err)
        return
    end
    ngx.say("respuesta de multi: ", cjson.encode(ok))

    local ans, err = red:set("a", "abc")
    if not ans then
        ngx.say("falló al ejecutar sort: ", err)
        return
    end
    ngx.say("respuesta de set: ", cjson.encode(ans))

    local ans, err = red:lpop("a")
    if not ans then
        ngx.say("falló al ejecutar sort: ", err)
        return
    end
    ngx.say("respuesta de set: ", cjson.encode(ans))

    ans, err = red:exec()
    ngx.say("respuesta de exec: ", cjson.encode(ans))

    red:close()

Entonces la salida será

respuesta de multi: "OK"
respuesta de set: "QUEUED"
respuesta de set: "QUEUED"
respuesta de exec: ["OK",[false,"ERR Operation against a key holding the wrong kind of value"]]

Módulo de Redis

Esta biblioteca soporta el módulo de Redis. Aquí hay un ejemplo con el módulo RedisBloom:

    local cjson = require "cjson"
    local redis = require "resty.redis"
    -- registrar el prefijo del módulo "bf" para RedisBloom
    redis.register_module_prefix("bf")

    local red = redis:new()

    local ok, err = red:connect("127.0.0.1", 6379)
    if not ok then
        ngx.say("falló al conectar: ", err)
        return
    end

    -- llamar al comando BF.ADD con el prefijo 'bf'
    res, err = red:bf():add("dog", 1)
    if not res then
        ngx.say(err)
        return
    end
    ngx.say("recibir: ", cjson.encode(res))

    -- llamar al comando BF.EXISTS
    res, err = red:bf():exists("dog")
    if not res then
        ngx.say(err)
        return
    end
    ngx.say("recibir: ", cjson.encode(res))

Balanceo de Carga y Failover

Puedes implementar trivialmente tu propia lógica de balanceo de carga de Redis en Lua. Simplemente mantén una tabla Lua con toda la información de backend de Redis disponible (como nombre de host y números de puerto) y elige un servidor de acuerdo con alguna regla (como round-robin o hashing basado en claves) de la tabla Lua en cada solicitud. Puedes hacer un seguimiento del estado de la regla actual en los datos de tu propio módulo Lua, consulta https://github.com/openresty/lua-nginx-module/#data-sharing-within-an-nginx-worker

De manera similar, puedes implementar lógica de failover automático en Lua con gran flexibilidad.

Depuración

Usualmente es conveniente usar la biblioteca lua-cjson para codificar los valores de retorno de los métodos de comando redis a JSON. Por ejemplo,

    local cjson = require "cjson"
    ...
    local res, err = red:mget("h1234", "h5678")
    if res then
        print("res: ", cjson.encode(res))
    end

Registro Automático de Errores

Por defecto, el módulo subyacente ngx_lua realiza el registro de errores cuando ocurren errores de socket. Si ya estás manejando correctamente los errores en tu propio código Lua, se recomienda desactivar este registro automático de errores desactivando la directiva lua_socket_log_errors de ngx_lua, es decir,

    lua_socket_log_errors off;

Lista de Verificación para Problemas

  1. Asegúrate de configurar correctamente el tamaño del pool de conexiones en el set_keepalive. Básicamente, si tu Redis puede manejar n conexiones concurrentes y tu NGINX tiene m trabajadores, entonces el tamaño del pool de conexiones debe configurarse como n/m. Por ejemplo, si tu Redis generalmente maneja 1000 solicitudes concurrentes y tienes 10 trabajadores de NGINX, entonces el tamaño del pool de conexiones debe ser 100. De manera similar, si tienes p diferentes instancias de NGINX, entonces el tamaño del pool de conexiones debe ser n/m/p.
  2. Asegúrate de que la configuración de backlog en el lado de Redis sea lo suficientemente grande. Para Redis 2.8+, puedes ajustar directamente el parámetro tcp-backlog en el archivo redis.conf (y también ajustar el parámetro del kernel SOMAXCONN en consecuencia, al menos en Linux). También puede que desees ajustar el parámetro maxclients en redis.conf.
  3. Asegúrate de no estar utilizando una configuración de tiempo de espera demasiado corta en los métodos set_timeout o set_timeouts. Si es necesario, intenta repetir la operación tras un tiempo de espera y desactivar el registro automático de errores (porque ya estás manejando correctamente los errores en tu propio código Lua).
  4. Si el uso de CPU de tus procesos de trabajo de NGINX es muy alto bajo carga, entonces el bucle de eventos de NGINX podría estar bloqueado por el cálculo de CPU demasiado. Intenta muestrear un C-land on-CPU Flame Graph y Lua-land on-CPU Flame Graph para un típico proceso de trabajo de NGINX. Puedes optimizar las cosas que consumen CPU de acuerdo con estos Flame Graphs.
  5. Si el uso de CPU de tus procesos de trabajo de NGINX es muy bajo bajo carga, entonces el bucle de eventos de NGINX podría estar bloqueado por algunas llamadas al sistema bloqueantes (como llamadas al sistema de IO de archivos). Puedes confirmar el problema ejecutando la herramienta epoll-loop-blocking-distr contra un típico proceso de trabajo de NGINX. Si es así, entonces puedes muestrear un C-land off-CPU Flame Graph para un proceso de trabajo de NGINX para analizar los bloqueadores reales.
  6. Si tu proceso redis-server está funcionando cerca del 100% de uso de CPU, entonces deberías considerar escalar tu backend Redis por múltiples nodos o usar la herramienta C-land on-CPU Flame Graph para analizar los cuellos de botella internos dentro del proceso del servidor Redis.

Limitaciones

  • Esta biblioteca no se puede usar en contextos de código como init_by_lua, set_by_lua, log_by_lua, y header_filter_by_lua donde la API de cosocket de ngx_lua no está disponible.
  • La instancia del objeto resty.redis no se puede almacenar en una variable Lua a nivel de módulo Lua, porque entonces será compartida por todas las solicitudes concurrentes manejadas por el mismo proceso de trabajo de nginx (consulta https://github.com/openresty/lua-nginx-module/#data-sharing-within-an-nginx-worker) y resultará en malas condiciones de carrera cuando las solicitudes concurrentes intenten usar la misma instancia de resty.redis (verás el error "bad request" o "socket busy" devuelto de las llamadas a los métodos). Siempre debes iniciar objetos resty.redis en variables locales de función o en la tabla ngx.ctx. Estos lugares tienen sus propias copias de datos para cada solicitud.

Clonar la última versión, asumiendo v0.29

wget https://github.com/openresty/lua-resty-redis/archive/refs/tags/v0.29.tar.gz

Extraer

tar -xvzf v0.29.tar.gz

Ir al directorio

cd lua-resty-redis-0.29

export LUA_LIB_DIR=/usr/local/openresty/site/lualib

Compilar e Instalar

make install

Ahora la ruta compilada se mostrará

/usr/local/lib/lua/resty = lua_package_path en la conf de nginx

```

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