mlcache: Bibliothèque de mise en cache en couches pour nginx-module-lua
Installation
Si vous n'avez pas configuré l'abonnement au dépôt RPM, inscrivez-vous. Ensuite, vous pouvez procéder avec les étapes suivantes.
CentOS/RHEL 7 ou 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
Pour utiliser cette bibliothèque Lua avec NGINX, assurez-vous que nginx-module-lua est installé.
Ce document décrit lua-resty-mlcache v2.7.0 publié le 14 février 2024.
Mise en cache en couches rapide et automatisée pour OpenResty.
Cette bibliothèque peut être manipulée comme un magasin de cache clé/valeur pour les types scalaires Lua et les tables, combinant la puissance de l'API [lua_shared_dict] et de [lua-resty-lrucache], ce qui donne une solution de mise en cache extrêmement performante et flexible.
Fonctionnalités :
- Mise en cache et mise en cache négative avec TTL.
- Mutex intégré via [lua-resty-lock] pour éviter les effets de pile sur votre base de données/backend lors des échecs de cache.
- Communication inter-travailleurs intégrée pour propager les invalidations de cache et permettre aux travailleurs de mettre à jour leurs caches L1 (lua-resty-lrucache) lors des changements (
set(),delete()). - Support pour les files d'attente de mise en cache des frappes et des échecs séparés.
- Plusieurs instances isolées peuvent être créées pour contenir divers types de données tout en s'appuyant sur le même cache L2
lua_shared_dict.
Illustration des différents niveaux de mise en cache intégrés dans cette bibliothèque :
┌─────────────────────────────────────────────────┐
│ Nginx │
│ ┌───────────┐ ┌───────────┐ ┌───────────┐ │
│ │worker │ │worker │ │worker │ │
│ L1 │ │ │ │ │ │ │
│ │ Lua cache │ │ Lua cache │ │ Lua cache │ │
│ └───────────┘ └───────────┘ └───────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌───────────────────────────────────────┐ │
│ │ │ │
│ L2 │ lua_shared_dict │ │
│ │ │ │
│ └───────────────────────────────────────┘ │
│ │ mutex │
│ ▼ │
│ ┌──────────────────┐ │
│ │ callback │ │
│ └────────┬─────────┘ │
└───────────────────────────┼─────────────────────┘
│
L3 │ I/O fetch
▼
Base de données, API, DNS, Disque, tout I/O...
La hiérarchie des niveaux de cache est :
- L1 : Cache Lua VM de type Least-Recently-Used utilisant [lua-resty-lrucache]. Fournit la recherche la plus rapide si peuplé, et évite d'épuiser la mémoire de la VM Lua des travailleurs.
- L2 : Zone de mémoire lua_shared_dict partagée par tous les travailleurs. Ce niveau n'est accessible que si L1 était un échec, et empêche les travailleurs de demander le cache L3.
- L3 : une fonction personnalisée qui ne sera exécutée que par un seul travailleur pour éviter l'effet de pile sur votre base de données/backend (via [lua-resty-lock]). Les valeurs récupérées via L3 seront définies dans le cache L2 pour que d'autres travailleurs puissent les récupérer.
Cette bibliothèque a été présentée à OpenResty Con 2018. Voir la section Ressources pour un enregistrement de la présentation.
Synopsis
## nginx.conf
http {
# vous n'avez pas besoin de configurer la ligne suivante lorsque vous
# utilisez LuaRocks ou opm.
# 'on' est déjà la valeur par défaut pour cette directive. Si 'off', le cache L1
# sera inefficace puisque la VM Lua sera recréée pour chaque
# requête. Cela est acceptable pendant le développement, mais assurez-vous que la production 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, -- taille du cache L1 (Lua VM)
ttl = 3600, -- TTL de 1h pour les frappes
neg_ttl = 30, -- TTL de 30s pour les échecs
})
if err then
end
-- nous mettons notre instance dans la table globale pour la brièveté dans
-- cet exemple, mais préférez une valeur de montée à l'un de vos modules
-- comme recommandé par ngx_lua
_G.cache = cache
}
server {
listen 8080;
location / {
content_by_lua_block {
local function callback(username)
-- cela ne s'exécute *qu'une seule fois* jusqu'à ce que la clé expire, donc
-- faites des opérations coûteuses comme se connecter à un
-- backend distant ici. c'est-à-dire : appeler un serveur MySQL dans ce callback
return db:get_user(username) -- { name = "John Doe", email = "[email protected]" }
end
-- cet appel essaiera L1 et L2 avant d'exécuter le callback (L3)
-- la valeur retournée sera ensuite stockée dans L2 et L1
-- pour la prochaine requête.
local user, err = cache:get("my_key", nil, callback, "jdoe")
ngx.say(user.name) -- "John Doe"
}
}
}
}
Méthodes
new
syntax: cache, err = mlcache.new(name, shm, opts?)
Crée une nouvelle instance de mlcache. Si cela échoue, retourne nil et une chaîne décrivant l'erreur.
Le premier argument name est un nom arbitraire de votre choix pour ce cache, et doit être une chaîne. Chaque instance de mlcache namespace les valeurs qu'elle contient selon son nom, donc plusieurs instances avec le même nom partageront les mêmes données.
Le deuxième argument shm est le nom de la zone de mémoire partagée lua_shared_dict. Plusieurs instances de mlcache peuvent utiliser le même shm (les valeurs seront namespaced).
Le troisième argument opts est optionnel. Si fourni, il doit s'agir d'une table contenant les options souhaitées pour cette instance. Les options possibles sont :
lru_size: un nombre définissant la taille du cache L1 sous-jacent (instance lua-resty-lrucache). Cette taille est le nombre maximal d'éléments que le cache L1 peut contenir. Par défaut :100.ttl: un nombre spécifiant la période d'expiration des valeurs mises en cache. L'unité est en secondes, mais accepte des parties décimales, comme0.3. Unttlde0signifie que les valeurs mises en cache ne périront jamais. Par défaut :30.neg_ttl: un nombre spécifiant la période d'expiration des échecs de cache (lorsque le callback L3 retournenil). L'unité est en secondes, mais accepte des parties décimales, comme0.3. Unneg_ttlde0signifie que les échecs de cache ne périront jamais. Par défaut :5.resurrect_ttl: nombre optionnel. Lorsqu'il est spécifié, l'instance mlcache tentera de ressusciter les valeurs périmées lorsque le callback L3 retournenil, err(erreurs douces). Plus de détails sont disponibles pour cette option dans la section get(). L'unité est en secondes, mais accepte des parties décimales, comme0.3.lru: optionnel. Une instance lua-resty-lrucache de votre choix. Si spécifié, mlcache ne créera pas de LRU. On peut utiliser cette valeur pour utiliser l'implémentationresty.lrucache.pureffide lua-resty-lrucache si désiré.shm_set_tries: le nombre de tentatives pour l'opérationset()de lua_shared_dict. Lorsque lelua_shared_dictest plein, il tente de libérer jusqu'à 30 éléments de sa file d'attente. Lorsque la valeur à définir est beaucoup plus grande que l'espace libéré, cette option permet à mlcache de réessayer l'opération (et de libérer plus de slots) jusqu'à ce que le nombre maximum de tentatives soit atteint ou qu'assez de mémoire ait été libérée pour que la valeur puisse s'adapter. Par défaut :3.shm_miss: chaîne optionnelle. Le nom d'unlua_shared_dict. Lorsqu'il est spécifié, les échecs (callbacks retournantnil) seront mis en cache dans celua_shared_dictséparé. Cela est utile pour s'assurer qu'un grand nombre d'échecs de cache (par exemple, déclenchés par des clients malveillants) ne supprime pas trop d'éléments mis en cache (frappes) dulua_shared_dictspécifié dansshm.shm_locks: chaîne optionnelle. Le nom d'unlua_shared_dict. Lorsqu'il est spécifié, lua-resty-lock utilisera ce dictionnaire partagé pour stocker ses verrous. Cette option peut aider à réduire le changement de cache : lorsque le cache L2 (shm) est plein, chaque insertion (comme les verrous créés par des accès concurrents déclenchant des callbacks L3) purge les 30 éléments les plus anciens accédés. Ces éléments purgés sont très probablement des valeurs mises en cache précédemment (et précieuses). En isolant les verrous dans un dictionnaire partagé séparé, les charges de travail subissant un changement de cache peuvent atténuer cet effet.resty_lock_opts: table optionnelle. Options pour les instances [lua-resty-lock]. Lorsque mlcache exécute le callback L3, il utilise lua-resty-lock pour s'assurer qu'un seul travailleur exécute le callback fourni.ipc_shm: chaîne optionnelle. Si vous souhaitez utiliser set(), delete() ou purge(), vous devez fournir un mécanisme IPC (Inter-Process Communication) pour que les travailleurs puissent synchroniser et invalider leurs caches L1. Ce module regroupe une bibliothèque IPC "clé en main", et vous pouvez l'activer en spécifiant unlua_shared_dictdédié dans cette option. Plusieurs instances de mlcache peuvent utiliser le même dictionnaire partagé (les événements seront namespaced), mais aucun autre acteur que mlcache ne doit y toucher.ipc: table optionnelle. Comme l'optionipc_shmci-dessus, mais vous permet d'utiliser la bibliothèque IPC de votre choix pour propager les événements inter-travailleurs.l1_serializer: fonction optionnelle. Sa signature et les valeurs acceptées sont documentées sous la méthode get(), avec un exemple. Si spécifié, cette fonction sera appelée chaque fois qu'une valeur est promue du cache L2 dans le L1 (worker Lua VM). Cette fonction peut effectuer une sérialisation arbitraire de l'élément mis en cache pour le transformer en tout objet Lua avant de le stocker dans le cache L1. Elle peut ainsi éviter à votre application de devoir répéter de telles transformations à chaque requête, comme créer des tables, des objets cdata, charger du nouveau code Lua, etc...
Exemple :
local mlcache = require "resty.mlcache"
local cache, err = mlcache.new("my_cache", "cache_shared_dict", {
lru_size = 1000, -- conserver jusqu'à 1000 éléments dans le cache L1 (Lua VM)
ttl = 3600, -- met en cache des types scalaires et des tables pendant 1h
neg_ttl = 60 -- met en cache des valeurs nil pendant 60s
})
if not cache then
error("impossible de créer mlcache : " .. err)
end
Vous pouvez créer plusieurs instances de mlcache s'appuyant sur la même zone de mémoire partagée 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 })
Dans l'exemple ci-dessus, cache_1 est idéal pour contenir quelques valeurs très grandes. cache_2 peut être utilisé pour contenir un grand nombre de petites valeurs. Les deux instances s'appuieront sur le même shm : lua_shared_dict cache_shared_dict 2048m;. Même si vous utilisez des clés identiques dans les deux caches, elles ne seront pas en conflit car chacune a un namespace différent.
Cet autre exemple instancie un mlcache en utilisant le module IPC intégré pour les événements d'invalidation inter-travailleurs (afin que nous puissions utiliser set(), delete() et 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"
})
Remarque : pour que le cache L1 soit efficace, assurez-vous que lua_code_cache est activé (ce qui est la valeur par défaut). Si vous désactivez cette directive pendant le développement, mlcache fonctionnera, mais la mise en cache L1 sera inefficace puisque une nouvelle VM Lua sera créée pour chaque requête.
get
syntax: value, err, hit_level = cache:get(key, opts?, callback?, ...)
Effectue une recherche dans le cache. C'est la méthode principale et la plus efficace de ce module. Un modèle typique est de ne pas appeler set(), et de laisser get() effectuer tout le travail.
Lorsque cette méthode réussit, elle retourne value et err est défini sur nil. Parce que les valeurs nil du callback L3 peuvent être mises en cache (c'est-à-dire "mise en cache négative"), value peut être nil bien qu'elle soit déjà mise en cache. Par conséquent, il faut noter qu'il faut vérifier la deuxième valeur de retour err pour déterminer si cette méthode a réussi ou non.
La troisième valeur de retour est un nombre qui est défini si aucune erreur n'a été rencontrée. Il indique le niveau auquel la valeur a été récupérée : 1 pour L1, 2 pour L2, et 3 pour L3.
Si, cependant, une erreur est rencontrée, cette méthode retourne nil dans value et une chaîne décrivant l'erreur dans err.
Le premier argument key est une chaîne. Chaque valeur doit être stockée sous une clé unique.
Le deuxième argument opts est optionnel. S'il est fourni, il doit s'agir d'une table contenant les options souhaitées pour cette clé. Ces options prévaudront sur les options de l'instance :
ttl: un nombre spécifiant la période d'expiration des valeurs mises en cache. L'unité est en secondes, mais accepte des parties décimales, comme0.3. Unttlde0signifie que les valeurs mises en cache ne périront jamais. Par défaut : hérité de l'instance.neg_ttl: un nombre spécifiant la période d'expiration des échecs de cache (lorsque le callback L3 retournenil). L'unité est en secondes, mais accepte des parties décimales, comme0.3. Unneg_ttlde0signifie que les échecs de cache ne périront jamais. Par défaut : hérité de l'instance.resurrect_ttl: nombre optionnel. Lorsqu'il est spécifié,get()tentera de ressusciter les valeurs périmées lorsqu'une erreur est rencontrée. Les erreurs retournées par le callback L3 (nil, err) sont considérées comme des échecs pour récupérer/rafraîchir une valeur. Lorsque de telles valeurs de retour du callback sont vues parget(), et si la valeur périmée est toujours en mémoire, alorsget()ressuscitera la valeur périmée pendantresurrect_ttlsecondes. L'erreur retournée parget()sera enregistrée au niveau WARN, mais ne sera pas retournée à l'appelant. Enfin, la valeur de retourhit_levelsera4pour signifier que l'élément servi est périmé. Lorsqueresurrect_ttlest atteint,get()tentera à nouveau d'exécuter le callback. Si d'ici là, le callback retourne à nouveau une erreur, la valeur est ressuscitée une fois de plus, et ainsi de suite. Si le callback réussit, la valeur est rafraîchie et n'est plus marquée comme périmée. En raison des limitations actuelles du module de cache LRU,hit_levelsera1lorsque des valeurs périmées sont promues dans le cache L1 et récupérées à partir de là. Les erreurs Lua lancées par le callback ne déclenchent pas une résurrection, et sont retournées parget()comme d'habitude (nil, err). Lorsque plusieurs travailleurs expirent en attendant le travailleur exécutant le callback (par exemple, parce que le magasin de données expire), alors les utilisateurs de cette option verront une légère différence par rapport au comportement traditionnel deget(). Au lieu de retournernil, err(indiquant un délai d'attente de verrou),get()retournera la valeur périmée (si disponible), aucune erreur, ethit_levelsera4. Cependant, la valeur ne sera pas ressuscitée (puisqu'un autre travailleur exécute toujours le callback). L'unité pour cette option est en secondes, mais elle accepte des parties décimales, comme0.3. Cette option doit être supérieure à0, pour éviter que des valeurs périmées soient mises en cache indéfiniment. Par défaut : hérité de l'instance.shm_set_tries: le nombre de tentatives pour l'opérationset()de lua_shared_dict. Lorsque lelua_shared_dictest plein, il tente de libérer jusqu'à 30 éléments de sa file d'attente. Lorsque la valeur à définir est beaucoup plus grande que l'espace libéré, cette option permet à mlcache de réessayer l'opération (et de libérer plus de slots) jusqu'à ce que le nombre maximum de tentatives soit atteint ou qu'assez de mémoire ait été libérée pour que la valeur puisse s'adapter. Par défaut : hérité de l'instance.l1_serializer: fonction optionnelle. Sa signature et les valeurs acceptées sont documentées sous la méthode get(), avec un exemple. Si spécifié, cette fonction sera appelée chaque fois qu'une valeur est promue du cache L2 dans le L1 (worker Lua VM). Cette fonction peut effectuer une sérialisation arbitraire de l'élément mis en cache pour le transformer en tout objet Lua avant de le stocker dans le cache L1. Elle peut ainsi éviter à votre application de devoir répéter de telles transformations à chaque requête, comme créer des tables, des objets cdata, charger du nouveau code Lua, etc... Par défaut : hérité de l'instance.resty_lock_opts: table optionnelle. Si spécifié, remplace lesresty_lock_optsde l'instance pour la rechercheget()actuelle. Par défaut : hérité de l'instance.
Le troisième argument callback est optionnel. S'il est fourni, il doit s'agir d'une fonction dont la signature et les valeurs de retour sont documentées dans l'exemple suivant :
-- arg1, arg2 et arg3 sont des arguments transmis au callback depuis les
-- arguments variadiques de `get()`, comme ceci :
-- cache:get(key, opts, callback, arg1, arg2, arg3)
local function callback(arg1, arg2, arg3)
-- logique de recherche I/O
-- ...
-- value : la valeur à mettre en cache (scalaire Lua ou table)
-- err : si pas `nil`, arrêtera get(), qui retournera `value` et `err`
-- ttl : remplacer ttl pour cette valeur
-- Si retourné comme `ttl >= 0`, cela remplacera l'instance
-- (ou l'option) `ttl` ou `neg_ttl`.
-- Si retourné comme `ttl < 0`, `value` sera retourné par get(),
-- mais ne sera pas mis en cache. Cette valeur de retour sera ignorée si ce n'est pas un nombre.
return value, err, ttl
end
La fonction callback fournie est autorisée à lancer des erreurs Lua car elle s'exécute en mode protégé. De telles erreurs lancées depuis le callback seront retournées sous forme de chaînes dans la deuxième valeur de retour err.
Si callback n'est pas fourni, get() recherchera tout de même la clé demandée dans les caches L1 et L2 et la retournera si trouvée. Dans le cas où aucune valeur n'est trouvée dans le cache et aucun callback n'est fourni, get() retournera nil, nil, -1, où -1 signifie un échec de cache (aucune valeur). Cela ne doit pas être confondu avec des valeurs de retour telles que nil, nil, 1, où 1 signifie un élément mis en cache négatif trouvé dans L1 (cached nil).
Ne pas fournir de fonction callback permet d'implémenter des modèles de recherche dans le cache qui sont garantis d'être sur le CPU pour une latence plus constante et plus fluide (par exemple, avec des valeurs rafraîchies dans des temporisateurs d'arrière-plan via set()).
local value, err, hit_lvl = cache:get("key")
if value == nil then
if err ~= nil then
-- erreur
elseif hit_lvl == -1 then
-- échec (aucune valeur)
else
-- frappe négative (valeur `nil` mise en cache)
end
end
Lorsqu'un callback est fourni, get() suit la logique ci-dessous :
- interroger le cache L1 (instance lua-resty-lrucache). Ce cache vit dans la
VM Lua, et en tant que tel, c'est le plus efficace à interroger.
- si le cache L1 a la valeur, la retourner.
- si le cache L1 n'a pas la valeur (échec L1), continuer.
- interroger le cache L2 (zone de mémoire
lua_shared_dict). Ce cache est partagé par tous les travailleurs, et est presque aussi efficace que le cache L1. Il nécessite cependant la sérialisation des tables Lua stockées.- si le cache L2 a la valeur, la retourner.
- si
l1_serializerest défini, l'exécuter, et promouvoir la valeur résultante dans le cache L1. - si non, promouvoir directement la valeur telle quelle dans le cache L1.
- si
- si le cache L2 n'a pas la valeur (échec L2), continuer.
- si le cache L2 a la valeur, la retourner.
- créer un [lua-resty-lock], et s'assurer qu'un seul travailleur exécutera le callback (d'autres travailleurs essayant d'accéder à la même valeur attendront).
- un seul travailleur exécute le callback L3 (par exemple, effectue une requête de base de données)
- le callback réussit et retourne une valeur : la valeur est définie dans le
cache L2, puis dans le cache L1 (telle quelle par défaut, ou comme retournée par
l1_serializersi spécifié). - le callback a échoué et retourné
nil, err: a. siresurrect_ttlest spécifié, et si la valeur périmée est toujours disponible, ressuscitez-la dans le cache L2 et promouvez-la dans le L1. b. sinon,get()retournenil, err. - d'autres travailleurs qui essayaient d'accéder à la même valeur mais attendaient sont déverrouillés et lisent la valeur du cache L2 (ils n'exécutent pas le L3 callback) et la retournent.
Lorsqu'aucun callback n'est fourni, get() n'exécutera que les étapes 1. et 2.
Voici un exemple complet d'utilisation :
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
-- dans ce cas, get() retournera `nil` + `err`
return nil, err
end
return user -- table ou 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, "impossible de récupérer l'utilisateur : ", err)
return
end
-- `user` pourrait être une table, mais pourrait aussi être `nil` (n'existe pas)
-- de toute façon, il sera mis en cache et les appels suivants à get() retourneront
-- la valeur mise en cache, pendant jusqu'à `ttl` ou `neg_ttl`.
if user then
ngx.say("l'utilisateur existe : ", user.name)
else
ngx.say("l'utilisateur n'existe pas")
end
Ce deuxième exemple est similaire à celui ci-dessus, mais ici nous appliquons une
transformation à l'enregistrement user récupéré avant de le mettre en cache via le
callback l1_serializer :
-- Notre l1_serializer, appelé lorsque une valeur est promue de L2 à L1
--
-- Sa signature reçoit un seul argument : l'élément tel que retourné depuis
-- un hit L2. Par conséquent, cet argument ne peut jamais être `nil`. Le résultat sera
-- conservé dans le cache L1, mais il ne peut pas être `nil`.
--
-- Cette fonction peut retourner `nil` et une chaîne décrivant une erreur, qui
-- remontera à l'appelant de `get()`. Elle s'exécute également en mode protégé
-- et signalera toute erreur 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
-- dans ce cas, rien ne sera stocké dans le cache (comme si le L3
-- callback avait échoué)
return nil, "échec de la compilation du code personnalisé : " .. 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, "impossible de récupérer l'utilisateur : ", err)
return
end
-- maintenant nous pouvons appeler une fonction qui a déjà été chargée une fois, lors de l'entrée
-- dans le cache L1 (Lua VM)
user.f()
get_bulk
syntax: res, err = cache:get_bulk(bulk, opts?)
Effectue plusieurs recherches get() à la fois (en masse). Chacune de ces recherches requérant un appel de callback L3 sera exécutée en parallèle, dans un pool de ngx.thread.
Le premier argument bulk est une table contenant n opérations.
Le deuxième argument opts est optionnel. S'il est fourni, il doit s'agir d'une table contenant
les options pour cette recherche en masse. Les options possibles sont :
concurrency: un nombre supérieur à0. Spécifie le nombre de threads qui exécuteront simultanément les callbacks L3 pour cette recherche en masse. Une concurrence de3avec 6 callbacks à exécuter signifie que chaque thread exécutera 2 callbacks. Une concurrence de1avec 6 callbacks signifie qu'un seul thread exécutera tous les 6 callbacks. Avec une concurrence de6et 1 callback, un seul thread exécutera le callback. Par défaut :3.
En cas de succès, cette méthode retourne res, une table contenant les résultats
de chaque recherche, et aucune erreur.
En cas d'échec, cette méthode retourne nil plus une chaîne décrivant l'erreur.
Toutes les opérations de recherche effectuées par cette méthode s'intégreront pleinement dans d'autres opérations effectuées simultanément par d'autres méthodes et travailleurs Nginx (par exemple, stockage des frappes/échecs L1/L2, mutex de callback L3, etc...).
L'argument bulk est une table qui doit avoir une disposition particulière (documentée
dans l'exemple ci-dessous). Elle peut être construite manuellement, ou via la
méthode d'assistance new_bulk().
De même, la table res a également une disposition particulière. Elle peut être
itérée manuellement, ou via l'itérateur d'assistance each_bulk_res.
Exemple :
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({
-- disposition en masse :
-- clé opts callback L3 argument du 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 -- spécifiez le nombre d'opérations
}, { concurrency = 3 })
if err then
ngx.log(ngx.ERR, "impossible d'exécuter la recherche en masse : ", err)
return
end
-- disposition de res :
-- data, "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("data : ", data, ", hit_lvl : ", hit_lvl)
end
end
L'exemple ci-dessus produirait la sortie suivante :
data: hello, hit_lvl: 3
data: world, hit_lvl: 3
data: nil, hit_lvl: 1
Notez que puisque key_c était déjà dans le cache, le callback retournant
"bye" n'a jamais été exécuté, puisque get_bulk() a récupéré la valeur de L1, comme
indiqué par la valeur hit_lvl.
Remarque : contrairement à get(), cette méthode ne permet de spécifier qu'un seul argument pour le callback de chaque recherche.
new_bulk
syntax: bulk = mlcache.new_bulk(n_lookups?)
Crée une table contenant des opérations de recherche pour la fonction get_bulk(). Il n'est pas nécessaire d'utiliser cette fonction pour construire une table de recherche en masse, mais elle fournit une belle abstraction.
Le premier et unique argument n_lookups est optionnel, et s'il est spécifié, c'est un
nombre indiquant la quantité de recherches que cette masse contiendra finalement afin que
la table sous-jacente soit pré-allouée à des fins d'optimisation.
Cette fonction retourne une table bulk, qui ne contient encore aucune opération de recherche.
Les recherches sont ajoutées à une table bulk en invoquant 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
syntax: iter, res, i = mlcache.each_bulk_res(res)
Fournit une abstraction pour itérer sur une table de retour res de get_bulk(). Il n'est pas nécessaire d'utiliser cette méthode pour itérer sur une table res, mais elle fournit une belle abstraction.
Cette méthode peut être invoquée comme un itérateur 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("recherche ", i, ": ", data)
end
end
peek
syntax: ttl, err, value = cache:peek(key, stale?)
Jetez un œil dans le cache L2 (lua_shared_dict).
Le premier argument key est une chaîne qui est la clé à rechercher dans le cache.
Le deuxième argument stale est optionnel. Si true, alors peek() considérera
les valeurs périmées comme des valeurs mises en cache. Si non fourni, peek() considérera les valeurs périmées, comme si elles n'étaient pas dans le cache.
Cette méthode retourne nil et une chaîne décrivant l'erreur en cas d'échec.
S'il n'y a pas de valeur pour la clé interrogée, elle retourne nil et aucune erreur.
S'il y a une valeur pour la clé interrogée, elle retourne un nombre indiquant le TTL restant de la valeur mise en cache (en secondes) et aucune erreur. Si la valeur pour key a expiré mais est toujours dans le cache L2, la valeur TTL retournée sera négative. La valeur de retour TTL restante ne sera que 0 si la clé interrogée a un ttl indéfini (ttl=0). Sinon, cette valeur de retour peut être positive (clé toujours valide), ou négative (clé périmée).
La troisième valeur retournée sera la valeur mise en cache telle que stockée dans le cache L2, si elle est toujours disponible.
Cette méthode est utile lorsque vous souhaitez déterminer si une valeur est mise en cache. Une valeur stockée dans le cache L2 est considérée comme mise en cache indépendamment de son stockage dans le cache L1 du travailleur. Cela est dû au fait que le cache L1 est considéré comme volatile (car sa taille est un nombre de slots), et le cache L2 est de toute façon plusieurs ordres de grandeur plus rapide que le callback L3.
Comme son seul but est de jeter un "coup d'œil" dans le cache pour déterminer sa chaleur pour une valeur donnée, peek() ne compte pas comme une requête comme get(), et ne promeut pas la valeur dans le cache L1.
Exemple :
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, "impossible de jeter un œil dans le cache : ", err)
return
end
ngx.say(ttl) -- nil car `key` n'a pas encore de valeur
ngx.say(value) -- nil
-- mettre en cache la valeur
cache:get("key", { ttl = 5 }, function() return "some value" end)
-- attendre 2 secondes
ngx.sleep(2)
local ttl, err, value = cache:peek("key")
if err then
ngx.log(ngx.ERR, "impossible de jeter un œil dans le cache : ", err)
return
end
ngx.say(ttl) -- 3
ngx.say(value) -- "some value"
Remarque : depuis mlcache 2.5.0, il est également possible d'appeler get()
sans fonction callback afin de "interroger" le cache. Contrairement à peek(), un
appel get() sans callback promouvra la valeur dans le cache L1, et
ne retournera pas son TTL.
set
syntax: ok, err = cache:set(key, opts?, value)
Définit inconditionnellement une valeur dans le cache L2 et diffuse un événement aux autres travailleurs afin qu'ils puissent rafraîchir la valeur de leur cache L1.
Le premier argument key est une chaîne, et est la clé sous laquelle stocker la
valeur.
Le deuxième argument opts est optionnel, et s'il est fourni, il est identique à celui de
get().
Le troisième argument value est la valeur à mettre en cache, similaire à la valeur de retour
du callback L3. Tout comme la valeur de retour du callback, elle doit être un scalaire Lua,
une table, ou nil. Si un l1_serializer est fourni soit depuis le constructeur soit dans
l'argument opts, il sera appelé avec value si value n'est pas nil.
En cas de succès, la première valeur de retour sera true.
En cas d'échec, cette méthode retourne nil et une chaîne décrivant l'erreur.
Remarque : par sa nature, set() nécessite que d'autres instances de mlcache (provenant
d'autres travailleurs) rafraîchissent leur cache L1. Si set() est appelé depuis un seul
travailleur, les instances mlcache d'autres travailleurs portant le même name doivent
appeler update() avant que leur cache ne soit demandé lors de la prochaine
requête, pour s'assurer qu'elles aient rafraîchi leur cache L1.
Remarque bis : il est généralement considéré comme inefficace d'appeler set() sur un
chemin de code chaud (tel que dans une requête servie par OpenResty). Au lieu de cela, il
faut s'appuyer sur get() et son mutex intégré dans le callback L3. set() est
mieux adapté lorsqu'il est appelé occasionnellement depuis un seul travailleur, par exemple
lors d'un événement particulier qui déclenche une mise à jour d'une valeur mise en cache. Une fois que set()
met à jour le cache L2 avec la valeur fraîche, d'autres travailleurs s'appuieront sur
update() pour interroger l'événement d'invalidation et invalider leur cache L1, ce qui les
fera récupérer la valeur (fraîche) dans L2.
Voir : update()
delete
syntax: ok, err = cache:delete(key)
Supprime une valeur dans le cache L2 et publie un événement aux autres travailleurs afin qu'ils puissent évincer la valeur de leur cache L1.
Le premier et unique argument key est la chaîne à laquelle la valeur est stockée.
En cas de succès, la première valeur de retour sera true.
En cas d'échec, cette méthode retourne nil et une chaîne décrivant l'erreur.
Remarque : par sa nature, delete() nécessite que d'autres instances de mlcache
(provenant d'autres travailleurs) rafraîchissent leur cache L1. Si delete() est appelé depuis un
seul travailleur, les instances mlcache d'autres travailleurs portant le même name doivent
appeler update() avant que leur cache ne soit demandé lors de la prochaine
requête, pour s'assurer qu'elles aient rafraîchi leur cache L1.
Voir : update()
purge
syntax: ok, err = cache:purge(flush_expired?)
Purge le contenu du cache, à la fois aux niveaux L1 et L2. Puis publie un événement aux autres travailleurs afin qu'ils puissent également purger leur cache L1.
Cette méthode recycle l'instance lua-resty-lrucache, et appelle ngx.shared.DICT:flush_all, donc elle peut être plutôt coûteuse.
Le premier et unique argument flush_expired est optionnel, mais s'il est donné true,
cette méthode appellera également
ngx.shared.DICT:flush_expired
(sans arguments). Cela est utile pour libérer la mémoire réclamée par le cache L2 (shm)
si nécessaire.
En cas de succès, la première valeur de retour sera true.
En cas d'échec, cette méthode retourne nil et une chaîne décrivant l'erreur.
Remarque : il n'est pas possible d'appeler purge() lors de l'utilisation d'un cache LRU personnalisé dans
OpenResty 1.13.6.1 et inférieur. Cette limitation ne s'applique pas à OpenResty
1.13.6.2 et supérieur.
Remarque : par sa nature, purge() nécessite que d'autres instances de mlcache
(provenant d'autres travailleurs) rafraîchissent leur cache L1. Si purge() est appelé depuis un
seul travailleur, les instances mlcache d'autres travailleurs portant le même name doivent
appeler update() avant que leur cache ne soit demandé lors de la prochaine
requête, pour s'assurer qu'elles aient rafraîchi leur cache L1.
Voir : update()
update
syntax: ok, err = cache:update(timeout?)
Interroge et exécute les événements d'invalidation de cache en attente publiés par d'autres travailleurs.
Les méthodes set(), delete() et purge() nécessitent
que d'autres instances de mlcache (provenant d'autres travailleurs) rafraîchissent leur cache L1.
Puisque OpenResty n'a actuellement aucun mécanisme intégré pour la communication inter-travailleurs,
ce module regroupe une bibliothèque IPC "clé en main" pour propager
les événements inter-travailleurs. Si la bibliothèque IPC intégrée est utilisée, le lua_shared_dict
spécifié dans l'option ipc_shm ne doit pas être utilisé par d'autres acteurs que
mlcache lui-même.
Cette méthode permet à un travailleur de mettre à jour son cache L1 (en purgeant les valeurs
considérées périmées en raison d'un autre travailleur appelant set(), delete(), ou
purge()) avant de traiter une requête.
Cette méthode accepte un argument timeout dont l'unité est en secondes et qui
par défaut est 0.3 (300ms). L'opération de mise à jour expirera si elle n'est pas effectuée
lorsque ce seuil est atteint. Cela évite que update() ne reste trop longtemps sur le CPU
en cas d'un trop grand nombre d'événements à traiter. Dans un système finalement
cohérent, des événements supplémentaires peuvent attendre que le prochain appel soit traité.
Un modèle de conception typique est d'appeler update() une seule fois avant chaque
traitement de requête. Cela permet à vos chemins de code chauds de réaliser un seul accès shm
dans le meilleur des cas : aucun événement d'invalidation n'a été reçu, tous les appels
get() frapperont dans le cache L1. Ce n'est que dans le pire des cas (n valeurs ont été
évincées par un autre travailleur) que get() accédera au cache L2 ou L3 n fois. Les requêtes
suivantes frapperont à nouveau le meilleur des cas, car get() a peuplé le cache L1.
Par exemple, si vos travailleurs utilisent set(), delete() ou
purge() n'importe où dans votre application, appelez update() à l'entrée
de votre chemin de code chaud, avant d'utiliser get() :
http {
listen 9000;
location / {
content_by_lua_block {
local cache = ... -- récupérer l'instance mlcache
-- assurez-vous que le cache L1 est évincé des valeurs périmées
-- avant d'appeler get()
local ok, err = cache:update()
if not ok then
ngx.log(ngx.ERR, "échec de l'interrogation des événements d'éviction : ", err)
-- /!\ nous pourrions obtenir des données périmées de get()
end
-- recherche L1/L2/L3 (meilleur cas : L1)
local value, err = cache:get("key_1", nil, cb1)
-- recherche L1/L2/L3 (meilleur cas : L1)
local other_value, err = cache:get("key_2", nil, cb2)
-- value et other_value sont à jour parce que :
-- soit elles n'étaient pas périmées et sont venues directement de L1 (meilleur scénario)
-- soit elles étaient périmées et évincées de L1, et sont venues de L2
-- soit elles n'étaient ni dans L1 ni dans L2, et sont venues de L3 (pire scénario)
}
}
location /delete {
content_by_lua_block {
local cache = ... -- récupérer l'instance mlcache
-- supprimer une valeur
local ok, err = cache:delete("key_1")
if not ok then
ngx.log(ngx.ERR, "échec de la suppression de la valeur du cache : ", err)
return ngx.exit(500)
end
ngx.exit(204)
}
}
location /set {
content_by_lua_block {
local cache = ... -- récupérer l'instance mlcache
-- mettre à jour une valeur
local ok, err = cache:set("key_1", nil, 123)
if not ok then
ngx.log(ngx.ERR, "échec de la définition de la valeur dans le cache : ", err)
return ngx.exit(500)
end
ngx.exit(200)
}
}
}
Remarque : vous n'avez pas besoin d'appeler update() pour rafraîchir vos travailleurs s'ils
n'appellent jamais set(), delete(), ou purge(). Lorsque les travailleurs s'appuient uniquement sur
get(), les valeurs expirent naturellement des caches L1/L2 selon leur TTL.
Remarque bis : cette bibliothèque a été construite avec l'intention d'utiliser une meilleure solution
pour la communication inter-travailleurs dès qu'elle émergera. Dans les futures versions
de cette bibliothèque, si une bibliothèque IPC peut éviter l'approche de polling, cette
bibliothèque le fera également. update() est seulement un mal nécessaire en raison des
"limitations" actuelles de Nginx/OpenResty. Vous pouvez cependant utiliser votre propre
bibliothèque IPC en utilisant l'option opts.ipc lors de la création de votre instance mlcache.
Ressources
En novembre 2018, cette bibliothèque a été présentée à OpenResty Con à Hangzhou, Chine.
Les diapositives et un enregistrement de la présentation (d'environ 40 minutes) peuvent être visionnés [ici][talk].
Changelog
Voir CHANGELOG.md.
GitHub
Vous pouvez trouver des conseils de configuration supplémentaires et de la documentation pour ce module dans le dépôt GitHub pour nginx-module-mlcache.