mlcache: Biblioteca de caché en capas para nginx-module-lua
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-mlcache
CentOS/RHEL 8+, Fedora Linux, Amazon Linux 2023
dnf -y install https://extras.getpagespeed.com/release-latest.rpm
dnf -y install lua5.1-resty-mlcache
Para usar esta biblioteca Lua con NGINX, asegúrate de que nginx-module-lua esté instalado.
Este documento describe lua-resty-mlcache v2.7.0 lanzado el 14 de febrero de 2024.
Caché en capas rápida y automatizada para OpenResty.
Esta biblioteca puede ser manipulada como un almacén de caché de clave/valor para tipos escalares de Lua y tablas, combinando el poder de la API [lua_shared_dict] y [lua-resty-lrucache], lo que resulta en una solución de caché extremadamente eficiente y flexible.
Características:
- Caché y caché negativa con TTLs.
- Mutex incorporado a través de [lua-resty-lock] para prevenir efectos de acumulación en tu base de datos/backend en fallos de caché.
- Comunicación inter-trabajadores incorporada para propagar invalidaciones de caché y permitir que los trabajadores actualicen sus cachés L1 (lua-resty-lrucache) al realizar cambios (
set(),delete()). - Soporte para colas de caché de aciertos y fallos divididos.
- Se pueden crear múltiples instancias aisladas para contener varios tipos de datos mientras se confía en la misma caché L2
lua_shared_dict.
Ilustración de los diversos niveles de caché integrados en esta biblioteca:
┌─────────────────────────────────────────────────┐
│ Nginx │
│ ┌───────────┐ ┌───────────┐ ┌───────────┐ │
│ │worker │ │worker │ │worker │ │
│ L1 │ │ │ │ │ │ │
│ │ Lua cache │ │ Lua cache │ │ Lua cache │ │
│ └───────────┘ └───────────┘ └───────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌───────────────────────────────────────┐ │
│ │ │ │
│ L2 │ lua_shared_dict │ │
│ │ │ │
│ └───────────────────────────────────────┘ │
│ │ mutex │
│ ▼ │
│ ┌──────────────────┐ │
│ │ callback │ │
│ └────────┬─────────┘ │
└───────────────────────────┼─────────────────────┘
│
L3 │ I/O fetch
▼
Base de datos, API, DNS, Disco, cualquier I/O...
La jerarquía de niveles de caché es:
- L1: Caché de VM Lua de Menos Reciente Usado utilizando [lua-resty-lrucache]. Proporciona la búsqueda más rápida si está poblada y evita agotar la memoria de la VM Lua de los trabajadores.
- L2: Zona de memoria lua_shared_dict compartida por todos los trabajadores. Este nivel solo se accede si L1 fue un fallo, y previene que los trabajadores soliciten la caché L3.
- L3: una función personalizada que solo será ejecutada por un único trabajador para evitar el efecto de acumulación en tu base de datos/backend (a través de [lua-resty-lock]). Los valores obtenidos a través de L3 se establecerán en la caché L2 para que otros trabajadores los recuperen.
Esta biblioteca fue presentada en OpenResty Con 2018. Consulta la sección de Recursos para una grabación de la charla.
Sinopsis
## nginx.conf
http {
# no necesitas configurar la siguiente línea cuando
# usas LuaRocks u opm.
# 'on' ya es el valor predeterminado para esta directiva. Si 'off', la caché L1
# será inefectiva ya que la VM de Lua será recreada para cada
# solicitud. Esto está bien durante el desarrollo, pero asegúrate de que en producción esté 'on'.
lua_code_cache on;
lua_shared_dict cache_dict 1m;
init_by_lua_block {
local mlcache = require "resty.mlcache"
local cache, err = mlcache.new("my_cache", "cache_dict", {
lru_size = 500, -- tamaño de la caché L1 (VM Lua)
ttl = 3600, -- ttl de 1h para aciertos
neg_ttl = 30, -- ttl de 30s para fallos
})
if err then
end
-- ponemos nuestra instancia en la tabla global por brevedad en
-- este ejemplo, pero preferimos un upvalue a uno de tus módulos
-- como se recomienda en ngx_lua
_G.cache = cache
}
server {
listen 8080;
location / {
content_by_lua_block {
local function callback(username)
-- esto solo se ejecuta *una vez* hasta que la clave expire, así que
-- realiza operaciones costosas como conectarse a un backend remoto aquí. es decir: llamar a un servidor MySQL en este callback
return db:get_user(username) -- { name = "John Doe", email = "[email protected]" }
end
-- esta llamada intentará L1 y L2 antes de ejecutar el callback (L3)
-- el valor devuelto se almacenará entonces en L2 y L1
-- para la siguiente solicitud.
local user, err = cache:get("my_key", nil, callback, "jdoe")
ngx.say(user.name) -- "John Doe"
}
}
}
}
Métodos
new
sintaxis: cache, err = mlcache.new(name, shm, opts?)
Crea una nueva instancia de mlcache. Si falla, devuelve nil y una cadena que describe el error.
El primer argumento name es un nombre arbitrario de tu elección para esta caché, y debe ser una cadena. Cada instancia de mlcache nombra los valores que contiene de acuerdo a su nombre, por lo que varias instancias con el mismo nombre compartirán los mismos datos.
El segundo argumento shm es el nombre de la zona de memoria compartida lua_shared_dict. Varias instancias de mlcache pueden usar el mismo shm (los valores estarán nombrados).
El tercer argumento opts es opcional. Si se proporciona, debe ser una tabla que contenga las opciones deseadas para esta instancia. Las opciones posibles son:
lru_size: un número que define el tamaño de la caché L1 subyacente (instancia de lua-resty-lrucache). Este tamaño es el número máximo de elementos que la caché L1 puede contener. Predeterminado:100.ttl: un número que especifica el período de tiempo de expiración de los valores en caché. La unidad es segundos, pero acepta partes fraccionarias, como0.3. Unttlde0significa que los valores en caché nunca expirarán. Predeterminado:30.neg_ttl: un número que especifica el período de tiempo de expiración de los fallos en caché (cuando el callback L3 devuelvenil). La unidad es segundos, pero acepta partes fraccionarias, como0.3. Unneg_ttlde0significa que los fallos en caché nunca expirarán. Predeterminado:5.resurrect_ttl: opcional número. Cuando se especifica, la instancia de mlcache intentará resucitar valores obsoletos cuando el callback L3 devuelvanil, err(errores suaves). Más detalles están disponibles para esta opción en la sección get(). La unidad es segundos, pero acepta partes fraccionarias, como0.3.lru: opcional. Una instancia de lua-resty-lrucache de tu elección. Si se especifica, mlcache no instanciará un LRU. Se puede usar este valor para utilizar la implementaciónresty.lrucache.pureffide lua-resty-lrucache si se desea.shm_set_tries: el número de intentos para la operaciónset()de lua_shared_dict. Cuando ellua_shared_dictestá lleno, intenta liberar hasta 30 elementos de su cola. Cuando el valor que se está estableciendo es mucho más grande que el espacio liberado, esta opción permite a mlcache reintentar la operación (y liberar más espacios) hasta que se alcance el número máximo de intentos o se haya liberado suficiente memoria para que el valor quepa. Predeterminado:3.shm_miss: opcional cadena. El nombre de unlua_shared_dict. Cuando se especifica, los fallos (callbacks que devuelvennil) se almacenarán en estelua_shared_dictseparado. Esto es útil para asegurar que un gran número de fallos en caché (por ejemplo, provocados por clientes maliciosos) no desaloje demasiados elementos en caché (aciertos) dellua_shared_dictespecificado enshm.shm_locks: opcional cadena. El nombre de unlua_shared_dict. Cuando se especifica, lua-resty-lock usará este diccionario compartido para almacenar sus bloqueos. Esta opción puede ayudar a reducir el giro de caché: cuando la caché L2 (shm) está llena, cada inserción (como bloqueos creados por accesos concurrentes que provocan callbacks L3) purga los 30 elementos más antiguos accedidos. Estos elementos purgados son los que probablemente eran valores en caché previamente (y valiosos). Al aislar los bloqueos en un diccionario compartido separado, las cargas de trabajo que experimentan giro de caché pueden mitigar este efecto.resty_lock_opts: opcional tabla. Opciones para instancias de [lua-resty-lock]. Cuando mlcache ejecuta el callback L3, utiliza lua-resty-lock para asegurar que un único trabajador ejecute el callback proporcionado.ipc_shm: opcional cadena. Si deseas usar set(), delete(), o purge(), debes proporcionar un mecanismo IPC (Comunicación entre Procesos) para que los trabajadores sincronicen e invaliden sus cachés L1. Este módulo agrupa una biblioteca IPC "lista para usar", y puedes habilitarla especificando unlua_shared_dictdedicado en esta opción. Varias instancias de mlcache pueden usar el mismo diccionario compartido (los eventos estarán nombrados), pero ningún otro actor que no sea mlcache debe manipularlo.ipc: opcional tabla. Al igual que la opción anterioripc_shm, pero te permite usar la biblioteca IPC de tu elección para propagar eventos inter-trabajadores.l1_serializer: opcional función. Su firma y valores aceptados están documentados en el método get(), junto con un ejemplo. Si se especifica, esta función será llamada cada vez que un valor sea promovido de la caché L2 a la L1 (VM Lua del trabajador). Esta función puede realizar serialización arbitraria del elemento en caché para transformarlo en cualquier objeto Lua antes de almacenarlo en la caché L1. Así puede evitar que tu aplicación tenga que repetir tales transformaciones en cada solicitud, como crear tablas, objetos cdata, cargar nuevo código Lua, etc...
Ejemplo:
local mlcache = require "resty.mlcache"
local cache, err = mlcache.new("my_cache", "cache_shared_dict", {
lru_size = 1000, -- mantener hasta 1000 elementos en la caché L1 (VM Lua)
ttl = 3600, -- almacena tipos escalares y tablas durante 1h
neg_ttl = 60 -- almacena valores nil durante 60s
})
if not cache then
error("no se pudo crear mlcache: " .. err)
end
Puedes crear varias instancias de mlcache confiando en la misma zona de memoria compartida lua_shared_dict:
local mlcache = require "mlcache"
local cache_1 = mlcache.new("cache_1", "cache_shared_dict", { lru_size = 100 })
local cache_2 = mlcache.new("cache_2", "cache_shared_dict", { lru_size = 1e5 })
En el ejemplo anterior, cache_1 es ideal para mantener unos pocos valores muy grandes. cache_2 puede ser utilizado para mantener un gran número de valores pequeños. Ambas instancias confiarán en el mismo shm: lua_shared_dict cache_shared_dict 2048m;. Incluso si usas claves idénticas en ambas cachés, no entrarán en conflicto entre sí ya que cada una tiene un espacio de nombres diferente.
Este otro ejemplo instancia un mlcache utilizando el módulo IPC agrupado para eventos de invalidación inter-trabajadores (para que podamos usar set(), delete(), y purge()):
local mlcache = require "resty.mlcache"
local cache, err = mlcache.new("my_cache_with_ipc", "cache_shared_dict", {
lru_size = 1000,
ipc_shm = "ipc_shared_dict"
})
Nota: para que la caché L1 sea efectiva, asegúrate de que lua_code_cache esté habilitada (que es el valor predeterminado). Si desactivas esta directiva durante el desarrollo, mlcache funcionará, pero la caché L1 será inefectiva ya que se creará una nueva VM de Lua para cada solicitud.
get
sintaxis: value, err, hit_level = cache:get(key, opts?, callback?, ...)
Realiza una búsqueda en la caché. Este es el método principal y más eficiente de este módulo. Un patrón típico es no llamar a set(), y dejar que get() realice todo el trabajo.
Cuando este método tiene éxito, devuelve value y err se establece en nil. Debido a que los valores nil del callback L3 pueden ser almacenados en caché (es decir, "caché negativa"), value puede ser nil aunque ya esté en caché. Por lo tanto, se debe tener en cuenta verificar el segundo valor de retorno err para determinar si este método tuvo éxito o no.
El tercer valor de retorno es un número que se establece si no se encontró ningún error. Indica el nivel en el que se obtuvo el valor: 1 para L1, 2 para L2, y 3 para L3.
Si, sin embargo, se encuentra un error, este método devuelve nil en value y una cadena que describe el error en err.
El primer argumento key es una cadena. Cada valor debe almacenarse bajo una clave única.
El segundo argumento opts es opcional. Si se proporciona, debe ser una tabla que contenga las opciones deseadas para esta clave. Estas opciones anularán las opciones de la instancia:
ttl: un número que especifica el período de tiempo de expiración de los valores en caché. La unidad es segundos, pero acepta partes fraccionarias, como0.3. Unttlde0significa que los valores en caché nunca expirarán. Predeterminado: heredado de la instancia.neg_ttl: un número que especifica el período de tiempo de expiración de los fallos en caché (cuando el callback L3 devuelvenil). La unidad es segundos, pero acepta partes fraccionarias, como0.3. Unneg_ttlde0significa que los fallos en caché nunca expirarán. Predeterminado: heredado de la instancia.resurrect_ttl: opcional número. Cuando se especifica,get()intentará resucitar valores obsoletos cuando se encuentren errores. Los errores devueltos por el callback L3 (nil, err) se consideran fallos al obtener/actualizar un valor. Cuando se ven tales valores de retorno del callback porget(), y si el valor obsoleto aún está en memoria, entoncesget()resucitará el valor obsoleto duranteresurrect_ttlsegundos. El error devuelto porget()se registrará a nivel WARN, pero no se devolverá al llamador. Finalmente, el valor de retornohit_levelserá4para significar que el elemento servido está obsoleto. Cuando se alcanceresurrect_ttl,get()intentará nuevamente ejecutar el callback. Si para entonces, el callback devuelve un error nuevamente, el valor se resucitará una vez más, y así sucesivamente. Si el callback tiene éxito, el valor se actualiza y ya no se marca como obsoleto. Debido a las limitaciones actuales dentro del módulo de caché LRU,hit_levelserá1cuando los valores obsoletos sean promovidos a la caché L1 y recuperados desde allí. Los errores de Lua lanzados por el callback no activan una resurrección, y se devuelven porget()como de costumbre (nil, err). Cuando varios trabajadores agotan el tiempo de espera mientras esperan al trabajador que ejecuta el callback (por ejemplo, porque la tienda de datos está agotando el tiempo), entonces los usuarios de esta opción verán una ligera diferencia en comparación con el comportamiento tradicional deget(). En lugar de devolvernil, err(indicando un tiempo de espera de bloqueo),get()devolverá el valor obsoleto (si está disponible), sin error, yhit_levelserá4. Sin embargo, el valor no será resucitado (ya que otro trabajador aún está ejecutando el callback). La unidad para esta opción es segundos, pero acepta partes fraccionarias, como0.3. Esta opción debe ser mayor que0, para evitar que los valores obsoletos se almacenen en caché indefinidamente. Predeterminado: heredado de la instancia.shm_set_tries: el número de intentos para la operaciónset()de lua_shared_dict. Cuando ellua_shared_dictestá lleno, intenta liberar hasta 30 elementos de su cola. Cuando el valor que se está estableciendo es mucho más grande que el espacio liberado, esta opción permite a mlcache reintentar la operación (y liberar más espacios) hasta que se alcance el número máximo de intentos o se haya liberado suficiente memoria para que el valor quepa. Predeterminado: heredado de la instancia.l1_serializer: opcional función. Su firma y valores aceptados están documentados en el método get(), junto con un ejemplo. Si se especifica, esta función será llamada cada vez que un valor sea promovido de la caché L2 a la L1 (VM Lua del trabajador). Esta función puede realizar serialización arbitraria del elemento en caché para transformarlo en cualquier objeto Lua antes de almacenarlo en la caché L1. Así puede evitar que tu aplicación tenga que repetir tales transformaciones en cada solicitud, como crear tablas, objetos cdata, cargar nuevo código Lua, etc... Predeterminado: heredado de la instancia.resty_lock_opts: opcional tabla. Si se especifica, anula lasresty_lock_optsde la instancia para la búsqueda actual deget(). Predeterminado: heredado de la instancia.
El tercer argumento callback es opcional. Si se proporciona, debe ser una función cuya firma y valores de retorno están documentados en el siguiente ejemplo:
-- arg1, arg2, y arg3 son argumentos pasados al callback desde los
-- argumentos variables de `get()`, así:
-- cache:get(key, opts, callback, arg1, arg2, arg3)
local function callback(arg1, arg2, arg3)
-- lógica de búsqueda I/O
-- ...
-- value: el valor a almacenar en caché (escalar Lua o tabla)
-- err: si no es `nil`, abortará get(), que devolverá `value` y `err`
-- ttl: anular ttl para este valor
-- Si se devuelve como `ttl >= 0`, anulará la instancia
-- (o la opción) `ttl` o `neg_ttl`.
-- Si se devuelve como `ttl < 0`, `value` será devuelto por get(),
-- pero no se almacenará en caché. Este valor de retorno será ignorado si no es un número.
return value, err, ttl
end
La función callback proporcionada puede lanzar errores de Lua ya que se ejecuta en modo protegido. Tales errores lanzados desde el callback se devolverán como cadenas en el segundo valor de retorno err.
Si callback no se proporciona, get() aún buscará la clave solicitada en las cachés L1 y L2 y la devolverá si se encuentra. En el caso de que no se encuentre ningún valor en la caché y no se proporcione ningún callback, get() devolverá nil, nil, -1, donde -1 significa un fallo de caché (sin valor). Esto no debe confundirse con valores de retorno como nil, nil, 1, donde 1 significa un elemento en caché negativo encontrado en L1 (caché nil).
No proporcionar una función callback permite implementar patrones de búsqueda en caché que están garantizados a estar en CPU para un final de latencia más constante y suave (por ejemplo, con valores actualizados en temporizadores de fondo a través de set()).
local value, err, hit_lvl = cache:get("key")
if value == nil then
if err ~= nil then
-- error
elseif hit_lvl == -1 then
-- fallo (sin valor)
else
-- acierto negativo (valor `nil` en caché)
end
end
Cuando se proporciona un callback, get() sigue la siguiente lógica:
- consulta la caché L1 (instancia de lua-resty-lrucache). Esta caché vive en la VM de Lua, y como tal, es la más eficiente para consultar.
- si la caché L1 tiene el valor, devuélvelo.
- si la caché L1 no tiene el valor (fallo L1), continúa.
- consulta la caché L2 (zona de memoria
lua_shared_dict). Esta caché es compartida por todos los trabajadores, y es casi tan eficiente como la caché L1. Sin embargo, requiere serialización de tablas Lua almacenadas.- si la caché L2 tiene el valor, devuélvelo.
- si
l1_serializerestá establecido, ejecútalo y promueve el valor resultante en la caché L1. - si no, promueve directamente el valor tal cual en la caché L1.
- si
- si la caché L2 no tiene el valor (fallo L2), continúa.
- si la caché L2 tiene el valor, devuélvelo.
- crea un [lua-resty-lock], y asegura que un único trabajador ejecutará el callback (otros trabajadores que intenten acceder al mismo valor esperarán).
- un único trabajador ejecuta el callback L3 (por ejemplo, realiza una consulta a la base de datos)
- el callback tiene éxito y devuelve un valor: el valor se establece en la caché L2, y luego en la caché L1 (tal cual por defecto, o como lo devuelto por
l1_serializersi se especifica). - el callback falló y devolvió
nil, err: a. siresurrect_ttlestá especificado, y si el valor obsoleto aún está disponible, resucítalo en la caché L2 y promuévelo a la L1. b. de lo contrario,get()devuelvenil, err. - otros trabajadores que estaban intentando acceder al mismo valor pero estaban esperando se desbloquean y leen el valor de la caché L2 (no ejecutan el callback L3) y lo devuelven.
Cuando no se proporciona un callback, get() solo ejecutará los pasos 1. y 2.
Aquí hay un ejemplo completo de uso:
local mlcache = require "mlcache"
local cache, err = mlcache.new("my_cache", "cache_shared_dict", {
lru_size = 1000,
ttl = 3600,
neg_ttl = 60
})
local function fetch_user(user_id)
local user, err = db:query_user(user_id)
if err then
-- en este caso, get() devolverá `nil` + `err`
return nil, err
end
return user -- tabla o nil
end
local user_id = 3
local user, err = cache:get("users:" .. user_id, nil, fetch_user, user_id)
if err then
ngx.log(ngx.ERR, "no se pudo recuperar el usuario: ", err)
return
end
-- `user` podría ser una tabla, pero también podría ser `nil` (no existe)
-- de cualquier manera, se almacenará en caché y las llamadas subsiguientes a get()
-- devolverán el valor en caché, por hasta `ttl` o `neg_ttl`.
if user then
ngx.say("el usuario existe: ", user.name)
else
ngx.say("el usuario no existe")
end
Este segundo ejemplo es similar al anterior, pero aquí aplicamos alguna transformación al registro de user recuperado antes de almacenarlo en caché a través del callback l1_serializer:
-- Nuestro l1_serializer, llamado cuando un valor es promovido de L2 a L1
--
-- Su firma recibe un único argumento: el elemento tal como se devuelve de
-- un acierto L2. Por lo tanto, este argumento nunca puede ser `nil`. El resultado se mantendrá
-- en la caché L1, pero no puede ser `nil`.
--
-- Esta función puede devolver `nil` y una cadena que describe un error, que
-- se propagará al llamador de `get()`. También se ejecuta en modo protegido
-- y reportará cualquier error de Lua.
local function load_code(user_row)
if user_row.custom_code ~= nil then
local f, err = loadstring(user_row.raw_lua_code)
if not f then
-- en este caso, nada se almacenará en la caché (como si el L3
-- callback fallara)
return nil, "falló al compilar el código personalizado: " .. err
end
user_row.f = f
end
return user_row
end
local user, err = cache:get("users:" .. user_id,
{ l1_serializer = load_code },
fetch_user, user_id)
if err then
ngx.log(ngx.ERR, "no se pudo recuperar el usuario: ", err)
return
end
-- ahora podemos llamar a una función que ya se cargó una vez, al entrar
-- en la caché L1 (VM Lua)
user.f()
get_bulk
sintaxis: res, err = cache:get_bulk(bulk, opts?)
Realiza varias búsquedas get() a la vez (en bloque). Cualquiera de estas búsquedas que requiera una llamada de callback L3 se ejecutará concurrentemente, en un grupo de ngx.thread.
El primer argumento bulk es una tabla que contiene n operaciones.
El segundo argumento opts es opcional. Si se proporciona, debe ser una tabla que contenga las opciones para esta búsqueda en bloque. Las opciones posibles son:
concurrency: un número mayor que0. Especifica el número de hilos que ejecutarán concurrentemente los callbacks L3 para esta búsqueda en bloque. Una concurrencia de3con 6 callbacks a ejecutar significa que cada hilo ejecutará 2 callbacks. Una concurrencia de1con 6 callbacks significa que un solo hilo ejecutará todos los 6 callbacks. Con una concurrencia de6y 1 callback, un solo hilo ejecutará el callback. Predeterminado:3.
Al tener éxito, este método devuelve res, una tabla que contiene los resultados de cada búsqueda, y ningún error.
Al fallar, este método devuelve nil más una cadena que describe el error.
Todas las operaciones de búsqueda realizadas por este método se integrarán completamente en otras operaciones que se realicen concurrentemente por otros métodos y trabajadores de Nginx (por ejemplo, almacenamiento de aciertos/fallos L1/L2, mutex de callback L3, etc...).
El argumento bulk es una tabla que debe tener un diseño particular (documentado en el ejemplo a continuación). Puede ser construida manualmente, o a través del método auxiliar new_bulk().
De manera similar, la tabla res también tiene un diseño particular. Puede ser iterada manualmente, o a través del auxiliar iterador each_bulk_res.
Ejemplo:
local mlcache = require "mlcache"
local cache, err = mlcache.new("my_cache", "cache_shared_dict")
cache:get("key_c", nil, function() return nil end)
local res, err = cache:get_bulk({
-- diseño en bloque:
-- clave opts callback L3 argumento del callback
"key_a", { ttl = 60 }, function() return "hello" end, nil,
"key_b", nil, function() return "world" end, nil,
"key_c", nil, function() return "bye" end, nil,
n = 3 -- especificar el número de operaciones
}, { concurrency = 3 })
if err then
ngx.log(ngx.ERR, "no se pudo ejecutar la búsqueda en bloque: ", err)
return
end
-- diseño de res:
-- datos, "err", hit_lvl }
for i = 1, res.n, 3 do
local data = res[i]
local err = res[i + 1]
local hit_lvl = res[i + 2]
if not err then
ngx.say("datos: ", data, ", hit_lvl: ", hit_lvl)
end
end
El ejemplo anterior produciría la siguiente salida:
datos: hello, hit_lvl: 3
datos: world, hit_lvl: 3
datos: nil, hit_lvl: 1
Ten en cuenta que dado que key_c ya estaba en la caché, el callback que devuelve "bye" nunca se ejecutó, ya que get_bulk() recuperó el valor de L1, como lo indica el valor de hit_lvl.
Nota: a diferencia de get(), este método solo permite especificar un único argumento para el callback de cada búsqueda.
new_bulk
sintaxis: bulk = mlcache.new_bulk(n_lookups?)
Crea una tabla que contiene operaciones de búsqueda para la función get_bulk(). No es necesario usar esta función para construir una tabla de búsqueda en bloque, pero proporciona una buena abstracción.
El primer y único argumento n_lookups es opcional, y si se especifica, es un número que sugiere la cantidad de búsquedas que esta carga eventualmente contendrá para que la tabla subyacente sea pre-asignada para fines de optimización.
Esta función devuelve una tabla bulk, que aún no contiene operaciones de búsqueda. Las búsquedas se añaden a una tabla bulk invocando bulk:add(key, opts?, cb, arg?):
local mlcache = require "mlcache"
local cache, err = mlcache.new("my_cache", "cache_shared_dict")
local bulk = mlcache.new_bulk(3)
bulk:add("key_a", { ttl = 60 }, function(n) return n * n, 42)
bulk:add("key_b", nil, function(str) return str end, "hello")
bulk:add("key_c", nil, function() return nil end)
local res, err = cache:get_bulk(bulk)
each_bulk_res
sintaxis: iter, res, i = mlcache.each_bulk_res(res)
Proporciona una abstracción para iterar sobre una tabla de retorno res de get_bulk(). No es necesario usar este método para iterar sobre una tabla res, pero proporciona una buena abstracción.
Este método puede ser invocado como un iterador de Lua:
local mlcache = require "mlcache"
local cache, err = mlcache.new("my_cache", "cache_shared_dict")
local res, err = cache:get_bulk(bulk)
for i, data, err, hit_lvl in mlcache.each_bulk_res(res) do
if not err then
ngx.say("búsqueda ", i, ": ", data)
end
end
peek
sintaxis: ttl, err, value = cache:peek(key, stale?)
Mira dentro de la caché L2 (lua_shared_dict).
El primer argumento key es una cadena que es la clave para buscar en la caché.
El segundo argumento stale es opcional. Si es true, entonces peek() considerará valores obsoletos como valores en caché. Si no se proporciona, peek() considerará valores obsoletos, como si no estuvieran en la caché.
Este método devuelve nil y una cadena que describe el error en caso de fallo.
Si no hay valor para la clave consultada key, devuelve nil y ningún error.
Si hay un valor para la clave consultada key, devuelve un número que indica el TTL restante del valor en caché (en segundos) y ningún error. Si el valor para key ha expirado pero aún está en la caché L2, el valor de TTL devuelto será negativo. El valor de TTL restante solo será 0 si la clave consultada key tiene un ttl indefinido (ttl=0). De lo contrario, este valor de retorno puede ser positivo (clave aún válida) o negativo (clave obsoleta).
El tercer valor devuelto será el valor en caché tal como se almacenó en la caché L2, si aún está disponible.
Este método es útil cuando deseas determinar si un valor está en caché. Un valor almacenado en la caché L2 se considera en caché independientemente de si también está establecido en la caché L1 del trabajador. Eso se debe a que la caché L1 se considera volátil (ya que su unidad de tamaño es un número de ranuras), y la caché L2 sigue siendo varios órdenes de magnitud más rápida que el callback L3 de todos modos.
Como su único propósito es "mirar" dentro de la caché para determinar su calidez para un valor dado, peek() no cuenta como una consulta como get(), y no promueve el valor a la caché L1.
Ejemplo:
local mlcache = require "mlcache"
local cache = mlcache.new("my_cache", "cache_shared_dict")
local ttl, err, value = cache:peek("key")
if err then
ngx.log(ngx.ERR, "no se pudo mirar en la caché: ", err)
return
end
ngx.say(ttl) -- nil porque `key` aún no tiene valor
ngx.say(value) -- nil
-- almacena el valor
cache:get("key", { ttl = 5 }, function() return "some value" end)
-- espera 2 segundos
ngx.sleep(2)
local ttl, err, value = cache:peek("key")
if err then
ngx.log(ngx.ERR, "no se pudo mirar en la caché: ", err)
return
end
ngx.say(ttl) -- 3
ngx.say(value) -- "some value"
Nota: desde mlcache 2.5.0, también es posible llamar a get() sin una función de callback para "consultar" la caché. A diferencia de peek(), una llamada a get() sin callback promoverá el valor a la caché L1, y no devolverá su TTL.
set
sintaxis: ok, err = cache:set(key, opts?, value)
Establece incondicionalmente un valor en la caché L2 y publica un evento a otros trabajadores para que puedan actualizar el valor desde su caché L1.
El primer argumento key es una cadena, y es la clave bajo la cual almacenar el valor.
El segundo argumento opts es opcional, y si se proporciona, es idéntico al de get().
El tercer argumento value es el valor a almacenar en caché, similar al valor de retorno del callback L3. Al igual que el valor de retorno del callback, debe ser un escalar Lua, una tabla, o nil. Si se proporciona un l1_serializer ya sea desde el constructor o en el argumento opts, se llamará con value si value no es nil.
Al tener éxito, el primer valor de retorno será true.
Al fallar, este método devuelve nil y una cadena que describe el error.
Nota: por su naturaleza, set() requiere que otras instancias de mlcache (de otros trabajadores) actualicen su caché L1. Si set() se llama desde un solo trabajador, las instancias de mlcache de otros trabajadores que tengan el mismo name deben llamar a update() antes de que su caché sea solicitada durante la siguiente solicitud, para asegurarse de que han actualizado su caché L1.
Nota bis: generalmente se considera ineficiente llamar a set() en una ruta de código caliente (como en una solicitud atendida por OpenResty). En su lugar, se debe confiar en get() y su mutex incorporado en el callback L3. set() es más adecuado cuando se llama ocasionalmente desde un solo trabajador, por ejemplo, ante un evento particular que desencadena la actualización de un valor en caché. Una vez que set() actualiza la caché L2 con el valor fresco, otros trabajadores confiarán en update() para sondear el evento de invalidación e invalidar su caché L1, lo que hará que obtengan el valor (fresco) en L2.
Ver: update()
delete
sintaxis: ok, err = cache:delete(key)
Elimina un valor en la caché L2 y publica un evento a otros trabajadores para que puedan desalojar el valor de su caché L1.
El primer y único argumento key es la cadena bajo la cual se almacena el valor.
Al tener éxito, el primer valor de retorno será true.
Al fallar, este método devuelve nil y una cadena que describe el error.
Nota: por su naturaleza, delete() requiere que otras instancias de mlcache (de otros trabajadores) actualicen su caché L1. Si delete() se llama desde un solo trabajador, las instancias de mlcache de otros trabajadores que tengan el mismo name deben llamar a update() antes de que su caché sea solicitada durante la siguiente solicitud, para asegurarse de que han actualizado su caché L1.
Ver: update()
purge
sintaxis: ok, err = cache:purge(flush_expired?)
Purga el contenido de la caché, en ambos niveles L1 y L2. Luego publica un evento a otros trabajadores para que puedan purgar su caché L1 también.
Este método recicla la instancia de lua-resty-lrucache, y llama a ngx.shared.DICT:flush_all, por lo que puede ser bastante costoso.
El primer y único argumento flush_expired es opcional, pero si se da true, este método también llamará a
ngx.shared.DICT:flush_expired (sin argumentos). Esto es útil para liberar memoria reclamada por la caché L2 (shm) si es necesario.
Al tener éxito, el primer valor de retorno será true.
Al fallar, este método devuelve nil y una cadena que describe el error.
Nota: no es posible llamar a purge() cuando se usa una caché LRU personalizada en OpenResty 1.13.6.1 y versiones anteriores. Esta limitación no se aplica a OpenResty 1.13.6.2 y versiones posteriores.
Nota: por su naturaleza, purge() requiere que otras instancias de mlcache (de otros trabajadores) actualicen su caché L1. Si purge() se llama desde un solo trabajador, las instancias de mlcache de otros trabajadores que tengan el mismo name deben llamar a update() antes de que su caché sea solicitada durante la siguiente solicitud, para asegurarse de que han actualizado su caché L1.
Ver: update()
update
sintaxis: ok, err = cache:update(timeout?)
Sondea y ejecuta eventos de invalidación de caché pendientes publicados por otros trabajadores.
Los métodos set(), delete(), y purge() requieren que otras instancias de mlcache (de otros trabajadores) actualicen su caché L1. Dado que OpenResty actualmente no tiene un mecanismo incorporado para la comunicación inter-trabajadores, este módulo agrupa una biblioteca IPC "lista para usar" para propagar eventos inter-trabajadores. Si se utiliza la biblioteca IPC agrupada, el lua_shared_dict especificado en la opción ipc_shm no debe ser utilizado por otros actores que no sean mlcache mismo.
Este método permite a un trabajador actualizar su caché L1 (desalojando valores considerados obsoletos debido a que otro trabajador llamó a set(), delete(), o purge()) antes de procesar una solicitud.
Este método acepta un argumento timeout cuya unidad es segundos y que por defecto es 0.3 (300ms). La operación de actualización se agotará si no se completa cuando se alcance este umbral. Esto evita que update() permanezca en la CPU demasiado tiempo en caso de que haya demasiados eventos que procesar. En un sistema eventualmente consistente, eventos adicionales pueden esperar a que se procese la siguiente llamada.
Un patrón de diseño típico es llamar a update() solo una vez antes de cada procesamiento de solicitud. Esto permite que tus rutas de código caliente realicen un único acceso shm en el mejor de los casos: no se recibieron eventos de invalidación, todas las llamadas a get() acertarán en la caché L1. Solo en un escenario de peor caso (n valores fueron desalojados por otro trabajador) get() accederá a la caché L2 o L3 n veces. Las solicitudes subsiguientes volverán a acertar en el mejor caso, porque get() pobló la caché L1.
Por ejemplo, si tus trabajadores utilizan set(), delete(), o purge() en cualquier parte de tu aplicación, llama a update() en la entrada de tu ruta de código caliente, antes de usar get():
http {
listen 9000;
location / {
content_by_lua_block {
local cache = ... -- recuperar instancia de mlcache
-- asegurarse de que la caché L1 esté desalojada de valores obsoletos
-- antes de llamar a get()
local ok, err = cache:update()
if not ok then
ngx.log(ngx.ERR, "falló al sondear eventos de desalojamiento: ", err)
-- /!\ podríamos obtener datos obsoletos de get()
end
-- búsqueda L1/L2/L3 (mejor caso: L1)
local value, err = cache:get("key_1", nil, cb1)
-- búsqueda L1/L2/L3 (mejor caso: L1)
local other_value, err = cache:get("key_2", nil, cb2)
-- value y other_value están actualizados porque:
-- o bien no estaban obsoletos y vinieron directamente de L1 (escenario de mejor caso)
-- o bien estaban obsoletos y fueron desalojados de L1, y vinieron de L2
-- o bien no estaban en L1 ni en L2, y vinieron de L3 (escenario de peor caso)
}
}
location /delete {
content_by_lua_block {
local cache = ... -- recuperar instancia de mlcache
-- eliminar algún valor
local ok, err = cache:delete("key_1")
if not ok then
ngx.log(ngx.ERR, "falló al eliminar valor de la caché: ", err)
return ngx.exit(500)
end
ngx.exit(204)
}
}
location /set {
content_by_lua_block {
local cache = ... -- recuperar instancia de mlcache
-- actualizar algún valor
local ok, err = cache:set("key_1", nil, 123)
if not ok then
ngx.log(ngx.ERR, "falló al establecer valor en la caché: ", err)
return ngx.exit(500)
end
ngx.exit(200)
}
}
}
Nota: no necesitas llamar a update() para actualizar a tus trabajadores si nunca llaman a set(), delete(), o purge(). Cuando los trabajadores solo dependen de get(), los valores expiran naturalmente de las cachés L1/L2 de acuerdo a su TTL.
Nota bis: esta biblioteca fue construida con la intención de usar una mejor solución para la comunicación inter-trabajadores tan pronto como surja una. En futuras versiones de esta biblioteca, si una biblioteca IPC puede evitar el enfoque de sondeo, esta biblioteca también lo hará. update() es solo un mal necesario debido a las "limitaciones" actuales de Nginx/OpenResty. Sin embargo, puedes usar tu propia biblioteca IPC utilizando la opción opts.ipc al crear tu instancia de mlcache.
Recursos
En noviembre de 2018, esta biblioteca fue presentada en OpenResty Con en Hangzhou, China.
Las diapositivas y una grabación de la charla (de aproximadamente 40 minutos de duración) pueden ser vistas [aquí][talk].
Changelog
Consulta CHANGELOG.md.
GitHub
Puedes encontrar consejos de configuración adicionales y documentación para este módulo en el repositorio de GitHub para nginx-module-mlcache.