mlcache: Слой кэширования для nginx-module-lua
Установка
Если вы еще не настроили подписку на RPM-репозиторий, зарегистрируйтесь. Затем вы можете продолжить с следующими шагами.
CentOS/RHEL 7 или 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
Чтобы использовать эту библиотеку Lua с NGINX, убедитесь, что nginx-module-lua установлен.
Этот документ описывает lua-resty-mlcache v2.7.0, выпущенный 14 февраля 2024 года.
Быстрое и автоматизированное слоистое кэширование для OpenResty.
Эта библиотека может быть использована как хранилище ключ/значение для кэширования скалярных типов Lua и таблиц, объединяя мощь API [lua_shared_dict] и [lua-resty-lrucache], что приводит к чрезвычайно производительному и гибкому решению для кэширования.
Особенности:
- Кэширование и негативное кэширование с TTL.
- Встроенный мьютекс через [lua-resty-lock], чтобы предотвратить эффекты "собачьей кучи" для вашей базы данных/бэкенда при промахах кэша.
- Встроенная межработниковая коммуникация для распространения недействительных кэшей и позволяет работникам обновлять свои L1 (lua-resty-lrucache) кэши при изменениях (
set(),delete()). - Поддержка разделенных очередей кэширования попаданий и промахов.
- Можно создать несколько изолированных экземпляров для хранения различных типов данных, полагаясь на один и тот же
lua_shared_dictL2 кэш.
Иллюстрация различных уровней кэширования, встроенных в эту библиотеку:
┌─────────────────────────────────────────────────┐
│ Nginx │
│ ┌───────────┐ ┌───────────┐ ┌───────────┐ │
│ │worker │ │worker │ │worker │ │
│ L1 │ │ │ │ │ │ │
│ │ Lua cache │ │ Lua cache │ │ Lua cache │ │
│ └───────────┘ └───────────┘ └───────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌───────────────────────────────────────┐ │
│ │ │ │
│ L2 │ lua_shared_dict │ │
│ │ │ │
│ └───────────────────────────────────────┘ │
│ │ mutex │
│ ▼ │
│ ┌──────────────────┐ │
│ │ callback │ │
│ └────────┬─────────┘ │
└───────────────────────────┼─────────────────────┘
│
L3 │ I/O fetch
▼
Database, API, DNS, Disk, any I/O...
Иерархия уровней кэша:
- L1: Кэш Lua VM с наименьшим временем использования, использующий [lua-resty-lrucache]. Обеспечивает самый быстрый поиск, если заполнен, и предотвращает исчерпание памяти Lua VM работников.
- L2: Зона памяти lua_shared_dict, общая для всех работников. Этот уровень используется только в случае промаха L1 и предотвращает запросы работников к L3 кэшу.
- L3: пользовательская функция, которая будет выполняться только одним работником, чтобы избежать эффекта "собачьей кучи" на вашей базе данных/бэкенде (через [lua-resty-lock]). Значения, полученные через L3, будут установлены в L2 кэш для других работников.
Эта библиотека была представлена на OpenResty Con 2018. См. раздел Ресурсы для записи доклада.
Синопсис
## nginx.conf
http {
# вам не нужно настраивать следующую строку, когда вы
# используете LuaRocks или opm.
# 'on' уже является значением по умолчанию для этой директивы. Если 'off', L1 кэш
# будет неэффективен, так как Lua VM будет пересоздаваться для каждого
# запроса. Это нормально во время разработки, но убедитесь, что в продакшене стоит '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, -- размер L1 (Lua VM) кэша
ttl = 3600, -- 1ч ttl для попаданий
neg_ttl = 30, -- 30с ttl для промахов
})
if err then
end
-- мы помещаем наш экземпляр в глобальную таблицу для краткости в
-- этом примере, но предпочтительнее использовать upvalue одного из ваших модулей
-- как рекомендовано ngx_lua
_G.cache = cache
}
server {
listen 8080;
location / {
content_by_lua_block {
local function callback(username)
-- это выполняется *один раз* до истечения ключа, так что
-- выполняйте дорогие операции, такие как подключение к удаленному
-- бэкенду здесь. т.е.: вызовите MySQL сервер в этом колбэке
return db:get_user(username) -- { name = "John Doe", email = "[email protected]" }
end
-- этот вызов попытается L1 и L2 перед выполнением колбэка (L3)
-- возвращенное значение затем будет сохранено в L2 и L1
-- для следующего запроса.
local user, err = cache:get("my_key", nil, callback, "jdoe")
ngx.say(user.name) -- "John Doe"
}
}
}
}
Методы
new
синтаксис: cache, err = mlcache.new(name, shm, opts?)
Создает новый экземпляр mlcache. Если не удалось, возвращает nil и строку, описывающую ошибку.
Первый аргумент name — произвольное имя, выбранное вами для этого кэша, и должно быть строкой. Каждый экземпляр mlcache называет значения, которые он хранит, в соответствии с его именем, так что несколько экземпляров с одинаковым именем будут делить одни и те же данные.
Второй аргумент shm — это имя зоны общей памяти lua_shared_dict. Несколько экземпляров mlcache могут использовать один и тот же shm (значения будут иметь пространство имен).
Третий аргумент opts является необязательным. Если предоставлен, он должен быть таблицей, содержащей желаемые параметры для этого экземпляра. Возможные параметры:
lru_size: число, определяющее размер базового L1 кэша (экземпляр lua-resty-lrucache). Этот размер — максимальное количество элементов, которые может содержать L1 кэш. По умолчанию:100.ttl: число, указывающее период времени истечения кэшированных значений. Единица — секунды, но принимает дробные части, такие как0.3. Значениеttlравное0означает, что кэшированные значения никогда не истекут. По умолчанию:30.neg_ttl: число, указывающее период времени истечения кэшированных промахов (когда L3 колбэк возвращаетnil). Единица — секунды, но принимает дробные части, такие как0.3. Значениеneg_ttlравное0означает, что кэшированные промахи никогда не истекут. По умолчанию:5.resurrect_ttl: необязательное число. Когда указано, экземпляр mlcache попытается воскресить устаревшие значения, когда L3 колбэк возвращаетnil, err(мягкие ошибки). Более подробная информация доступна для этого параметра в разделе get(). Единица — секунды, но принимает дробные части, такие как0.3.lru: необязательный. Экземпляр lua-resty-lrucache на ваш выбор. Если указано, mlcache не будет создавать LRU. Можно использовать это значение, чтобы использовать реализациюresty.lrucache.pureffilua-resty-lrucache, если это необходимо.shm_set_tries: количество попыток для операцииlua_shared_dict set(). Когдаlua_shared_dictзаполнен, он пытается освободить до 30 элементов из своей очереди. Когда устанавливаемое значение значительно больше освобожденного пространства, этот параметр позволяет mlcache повторить операцию (и освободить больше слотов), пока не будет достигнуто максимальное количество попыток или не будет освобождено достаточно памяти для размещения значения. По умолчанию:3.shm_miss: необязательная строка. Имяlua_shared_dict. Когда указано, промахи (колбэки, возвращающиеnil) будут кэшироваться в этом отдельномlua_shared_dict. Это полезно, чтобы гарантировать, что большое количество промахов кэша (например, вызванных злонамеренными клиентами) не вытеснит слишком много кэшированных элементов (попаданий) изlua_shared_dict, указанного вshm.shm_locks: необязательная строка. Имяlua_shared_dict. Когда указано, lua-resty-lock будет использовать этот общий словарь для хранения своих блокировок. Этот параметр может помочь уменьшить "крутку" кэша: когда L2 кэш (shm) заполнен, каждая вставка (такая как блокировки, создаваемые конкурентными доступами, вызывающими L3 колбэки) очищает 30 самых старых доступных элементов. Эти очищенные элементы, скорее всего, являются ранее (и ценными) кэшированными значениями. Изолируя блокировки в отдельном общем словаре, рабочие нагрузки, испытывающие крутку кэша, могут смягчить этот эффект.resty_lock_opts: необязательная таблица. Параметры для экземпляров [lua-resty-lock]. Когда mlcache выполняет L3 колбэк, он использует lua-resty-lock, чтобы гарантировать, что один работник выполняет предоставленный колбэк.ipc_shm: необязательная строка. Если вы хотите использовать set(), delete() или purge(), вы должны предоставить механизм IPC (межпроцессное взаимодействие) для работников, чтобы синхронизировать и недействительными свои L1 кэши. Этот модуль включает "готовую" библиотеку IPC, и вы можете включить ее, указав выделенныйlua_shared_dictв этом параметре. Несколько экземпляров mlcache могут использовать один и тот же общий словарь (события будут иметь пространство имен), но никто, кроме mlcache, не должен с ним взаимодействовать.ipc: необязательная таблица. Как и вышеуказанный параметрipc_shm, но позволяет вам использовать библиотеку IPC по вашему выбору для распространения межработниковых событий.l1_serializer: необязательная функция. Ее сигнатура и принимаемые значения документированы в методе get(), вместе с примером. Если указано, эта функция будет вызываться каждый раз, когда значение продвигается из L2 кэша в L1 (Lua VM работника). Эта функция может выполнять произвольную сериализацию кэшированного элемента, чтобы преобразовать его в любой объект Lua перед его сохранением в L1 кэш. Таким образом, она может избежать необходимости вашему приложению повторять такие преобразования при каждом запросе, такие как создание таблиц, объектов cdata, загрузка нового Lua кода и т.д.
Пример:
local mlcache = require "resty.mlcache"
local cache, err = mlcache.new("my_cache", "cache_shared_dict", {
lru_size = 1000, -- хранить до 1000 элементов в L1 кэше (Lua VM)
ttl = 3600, -- кэшировать скалярные типы и таблицы на 1ч
neg_ttl = 60 -- кэшировать nil значения на 60с
})
if not cache then
error("не удалось создать mlcache: " .. err)
end
Вы можете создать несколько экземпляров mlcache, полагаясь на одну и ту же базовую зону общей памяти 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 })
В приведенном выше примере cache_1 идеально подходит для хранения нескольких очень больших значений. cache_2 можно использовать для хранения большого количества небольших значений. Оба экземпляра будут полагаться на один и тот же shm: lua_shared_dict cache_shared_dict 2048m;. Даже если вы используете одинаковые ключи в обоих кэшах, они не будут конфликтовать друг с другом, так как у них разные пространства имен.
Этот другой пример создает экземпляр mlcache, используя встроенный модуль IPC для событий недействительности межработников (чтобы мы могли использовать set(), delete() и 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"
})
Примечание: для того чтобы L1 кэш был эффективным, убедитесь, что lua_code_cache включен (что является значением по умолчанию). Если вы отключите эту директиву во время разработки, mlcache будет работать, но кэширование L1 будет неэффективным, так как новая Lua VM будет создаваться для каждого запроса.
get
синтаксис: value, err, hit_level = cache:get(key, opts?, callback?, ...)
Выполняет поиск в кэше. Это основной и самый эффективный метод этого модуля. Типичный шаблон — не вызывать set() и позволить get() выполнять всю работу.
Когда этот метод выполняется успешно, он возвращает value, а err устанавливается в nil. Поскольку значения nil из L3 колбэка могут кэшироваться (т.е. "негативное кэширование"), value может быть nil, хотя уже и кэшировано. Поэтому необходимо проверить второе возвращаемое значение err, чтобы определить, был ли этот метод успешным или нет.
Третье возвращаемое значение — это число, которое устанавливается, если ошибка не была обнаружена. Оно указывает уровень, на котором было получено значение: 1 для L1, 2 для L2 и 3 для L3.
Если же возникает ошибка, то этот метод возвращает nil в value и строку, описывающую ошибку, в err.
Первый аргумент key — это строка. Каждое значение должно храниться под уникальным ключом.
Второй аргумент opts является необязательным. Если предоставлен, он должен быть таблицей, содержащей желаемые параметры для этого ключа. Эти параметры будут иметь приоритет над параметрами экземпляра:
ttl: число, указывающее период времени истечения кэшированных значений. Единица — секунды, но принимает дробные части, такие как0.3. Значениеttlравное0означает, что кэшированные значения никогда не истекут. По умолчанию: унаследовано от экземпляра.neg_ttl: число, указывающее период времени истечения кэшированных промахов (когда L3 колбэк возвращаетnil). Единица — секунды, но принимает дробные части, такие как0.3. Значениеneg_ttlравное0означает, что кэшированные промахи никогда не истекут. По умолчанию: унаследовано от экземпляра.resurrect_ttl: необязательное число. Когда указано,get()попытается воскресить устаревшие значения, когда возникают ошибки. Ошибки, возвращаемые L3 колбэком (nil, err), считаются неудачами при получении/обновлении значения. Когда такие возвращаемые значения из колбека видитget(), и если устаревшее значение все еще в памяти, тоget()воскресит устаревшее значение наresurrect_ttlсекунд. Ошибка, возвращаемаяget(), будет записана на уровне WARN, но не будет возвращена вызывающему. Наконец, значение возвратаhit_levelбудет4, чтобы обозначить, что обслуживаемый элемент устарел. Когдаresurrect_ttlбудет достигнуто,get()снова попытается выполнить колбэк. Если к этому времени колбэк снова возвращает ошибку, значение снова будет воскрешено и так далее. Если колбэк выполняется успешно, значение обновляется и больше не помечается как устаревшее. Из-за текущих ограничений в модуле LRU кэшаhit_levelбудет1, когда устаревшие значения будут продвигаться в L1 кэш и извлекаться оттуда. Lua ошибки, выбрасываемые колбэком, не вызывают воскрешение и возвращаютсяget()как обычно (nil, err). Когда несколько работников истекают во время ожидания работника, выполняющего колбэк (например, потому что хранилище данных истекает), пользователи этой опции увидят небольшое отличие по сравнению с традиционным поведениемget(). Вместо возвратаnil, err(что указывает на тайм-аут блокировки),get()вернет устаревшее значение (если доступно), без ошибки, аhit_levelбудет4. Однако значение не будет воскрешено (так как другой работник все еще выполняет колбэк). Единица для этого параметра — секунды, но принимает дробные части, такие как0.3. Этот параметр должен быть больше0, чтобы предотвратить кэширование устаревших значений бесконечно. По умолчанию: унаследовано от экземпляра.shm_set_tries: количество попыток для операцииlua_shared_dict set(). Когдаlua_shared_dictзаполнен, он пытается освободить до 30 элементов из своей очереди. Когда устанавливаемое значение значительно больше освобожденного пространства, этот параметр позволяет mlcache повторить операцию (и освободить больше слотов), пока не будет достигнуто максимальное количество попыток или не будет освобождено достаточно памяти для размещения значения. По умолчанию: унаследовано от экземпляра.l1_serializer: необязательная функция. Ее сигнатура и принимаемые значения документированы в методе get(), вместе с примером. Если указано, эта функция будет вызываться каждый раз, когда значение продвигается из L2 кэша в L1 (Lua VM работника). Эта функция может выполнять произвольную сериализацию кэшированного элемента, чтобы преобразовать его в любой объект Lua перед его сохранением в L1 кэш. Таким образом, она может избежать необходимости вашему приложению повторять такие преобразования при каждом запросе, такие как создание таблиц, объектов cdata, загрузка нового Lua кода и т.д. По умолчанию: унаследовано от экземпляра.resty_lock_opts: необязательная таблица. Если указано, переопределяет параметрыresty_lock_optsэкземпляра для текущего поискаget(). По умолчанию: унаследовано от экземпляра.
Третий аргумент callback является необязательным. Если предоставлен, он должен быть функцией, сигнатура и возвращаемые значения которой документированы в следующем примере:
-- arg1, arg2 и arg3 — это аргументы, передаваемые колбэку из
-- переменных аргументов `get()`, как так:
-- cache:get(key, opts, callback, arg1, arg2, arg3)
local function callback(arg1, arg2, arg3)
-- Логика поиска I/O
-- ...
-- value: значение для кэширования (скаляр Lua или таблица)
-- err: если не `nil`, прервёт get(), который вернёт `value` и `err`
-- ttl: переопределяет ttl для этого значения
-- Если возвращается как `ttl >= 0`, он переопределит экземпляр
-- (или параметр) `ttl` или `neg_ttl`.
-- Если возвращается как `ttl < 0`, `value` будет возвращено get(),
-- но не будет кэшировано. Это возвращаемое значение будет проигнорировано, если не число.
return value, err, ttl
end
Предоставленная функция callback может выбрасывать ошибки Lua, так как она работает в защищенном режиме. Такие ошибки, выбрасываемые из колбэка, будут возвращены как строки во втором возвращаемом значении err.
Если callback не предоставлен, get() все равно будет искать запрашиваемый ключ в кэшах L1 и L2 и вернет его, если найден. В случае, когда значение не найдено в кэше и колбэк не предоставлен, get() вернет nil, nil, -1, где -1 обозначает промах кэша (нет значения). Это не следует путать с возвращаемыми значениями, такими как nil, nil, 1, где 1 обозначает негативный кэшированный элемент, найденный в L1 (кэшированный nil).
Не предоставление функции callback позволяет реализовать шаблоны поиска в кэше, которые гарантированно будут на CPU для более постоянной и плавной задержки (например, с обновлениями значений в фоновом режиме через set()).
local value, err, hit_lvl = cache:get("key")
if value == nil then
if err ~= nil then
-- ошибка
elseif hit_lvl == -1 then
-- промах (нет значения)
else
-- негативный попадание (кэшированное значение `nil`)
end
end
Когда предоставляется колбэк, get() следует следующей логике:
- запрос к кэшу L1 (экземпляр lua-resty-lrucache). Этот кэш находится в
Lua VM, и, следовательно, это самый эффективный кэш для запроса.
- если кэш L1 содержит значение, вернуть его.
- если кэш L1 не содержит значение (промах L1), продолжить.
- запрос к кэшу L2 (зона памяти
lua_shared_dict). Этот кэш общий для всех работников и почти так же эффективен, как кэш L1. Однако он требует сериализации сохраненных Lua таблиц.- если кэш L2 содержит значение, вернуть его.
- если
l1_serializerустановлен, выполнить его и продвинуть полученное значение в кэш L1. - если нет, напрямую продвинуть значение как есть в кэш L1.
- если
- если кэш L2 не содержит значение (промах L2), продолжить.
- если кэш L2 содержит значение, вернуть его.
- создать [lua-resty-lock] и гарантировать, что один работник выполнит колбэк (другие работники, пытающиеся получить доступ к тому же значению, будут ждать).
- один работник выполняет L3 колбэк (например, выполняет запрос к базе данных)
- колбэк выполняется успешно и возвращает значение: значение устанавливается в
кэш L2, а затем в кэш L1 (как есть по умолчанию или как возвращено
l1_serializer, если указано). - колбэк не удался и вернул
nil, err: a. еслиresurrect_ttlуказан, и если устаревшее значение все еще доступно, воскресить его в кэше L2 и продвинуть в L1. b. в противном случаеget()возвращаетnil, err. - другие работники, которые пытались получить доступ к тому же значению, но ждали, разблокированы и читают значение из кэша L2 (они не выполняют L3 колбэк) и возвращают его.
Когда не предоставляется колбэк, get() выполнит только шаги 1 и 2.
Вот полный пример использования:
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
-- в этом случае get() вернет `nil` + `err`
return nil, err
end
return user -- таблица или 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, "не удалось получить пользователя: ", err)
return
end
-- `user` может быть таблицей, но также может быть `nil` (не существует)
-- в любом случае, он будет кэширован, и последующие вызовы к get()
-- вернут кэшированное значение, в течение `ttl` или `neg_ttl`.
if user then
ngx.say("пользователь существует: ", user.name)
else
ngx.say("пользователь не существует")
end
Этот второй пример аналогичен предыдущему, но здесь мы применяем некоторые
преобразования к полученной записи user перед кэшированием ее через
колбэк l1_serializer:
-- Наш l1_serializer, вызываемый, когда значение продвигается из L2 в L1
--
-- Его сигнатура принимает один аргумент: элемент, возвращенный из
-- попадания L2. Следовательно, этот аргумент никогда не может быть `nil`. Результат будет
-- храниться в L1 кэше, но не может быть `nil`.
--
-- Эта функция может вернуть `nil` и строку, описывающую ошибку, которая
-- поднимется к вызывающему `get()`. Она также работает в защищенном режиме
-- и будет сообщать об любой ошибке 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
-- в этом случае ничего не будет сохранено в кэше (как если бы L3
-- колбэк не удался)
return nil, "не удалось скомпилировать пользовательский код: " .. 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, "не удалось получить пользователя: ", err)
return
end
-- теперь мы можем вызвать функцию, которая была загружена один раз, при входе
-- в L1 кэш (Lua VM)
user.f()
get_bulk
синтаксис: res, err = cache:get_bulk(bulk, opts?)
Выполняет несколько get() запросов сразу (оптом). Любые из этих запросов, требующие вызова L3 колбэка, будут выполняться параллельно, в пуле ngx.thread.
Первый аргумент bulk — это таблица, содержащая n операций.
Второй аргумент opts является необязательным. Если предоставлен, он должен быть таблицей, содержащей параметры для этого оптового запроса. Возможные параметры:
concurrency: число больше0. Указывает количество потоков, которые будут одновременно выполнять L3 колбэки для этого оптового запроса. Конкуренция3с 6 колбэками означает, что каждый поток выполнит 2 колбэка. Конкуренция1с 6 колбэками означает, что один поток выполнит все 6 колбэков. При конкуренции6и 1 колбэке один поток выполнит колбэк. По умолчанию:3.
При успешном выполнении этот метод возвращает res, таблицу, содержащую результаты
каждого запроса, и никаких ошибок.
При неудаче этот метод возвращает nil плюс строку, описывающую ошибку.
Все операции поиска, выполняемые этим методом, будут полностью интегрированы в другие операции, которые выполняются параллельно другими методами и работниками Nginx (например, хранение попаданий/промахов L1/L2, мьютекс колбэка L3 и т.д.).
Аргумент bulk — это таблица, которая должна иметь определенный макет (документированный
в приведенном ниже примере). Ее можно создать вручную или с помощью
вспомогательного метода new_bulk().
Аналогично, таблица res также имеет свой собственный определенный макет. Ее можно
перебирать вручную или с помощью вспомогательного итератора each_bulk_res.
Пример:
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({
-- макет оптового запроса:
-- ключ opts L3 колбэк аргумент колбэка
"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 -- укажите количество операций
}, { concurrency = 3 })
if err then
ngx.log(ngx.ERR, "не удалось выполнить оптовый запрос: ", err)
return
end
-- макет res:
-- данные, "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, ", hit_lvl: ", hit_lvl)
end
end
Приведенный выше пример даст следующий вывод:
данные: hello, hit_lvl: 3
данные: world, hit_lvl: 3
данные: nil, hit_lvl: 1
Обратите внимание, что поскольку key_c уже был в кэше, колбэк, возвращающий
"bye", никогда не был выполнен, так как get_bulk() извлек значение из L1, как
указано значением hit_lvl.
Примечание: в отличие от get(), этот метод позволяет указывать только один аргумент для колбэка каждого запроса.
new_bulk
синтаксис: bulk = mlcache.new_bulk(n_lookups?)
Создает таблицу, содержащую операции поиска для функции get_bulk(). Необязательно использовать эту функцию для создания таблицы оптового запроса, но она предоставляет хорошую абстракцию.
Первый и единственный аргумент n_lookups является необязательным, и если указан, это число, указывающее количество запросов, которые эта партия в конечном итоге будет содержать, чтобы базовая таблица была предварительно выделена для оптимизации.
Эта функция возвращает таблицу bulk, которая еще не содержит операций поиска.
Поиск добавляется в таблицу bulk, вызывая 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
синтаксис: iter, res, i = mlcache.each_bulk_res(res)
Предоставляет абстракцию для итерации по таблице возврата get_bulk() res. Необязательно использовать этот метод для итерации по таблице res, но он предоставляет хорошую абстракцию.
Этот метод можно вызывать как итератор 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("поиск ", i, ": ", data)
end
end
peek
синтаксис: ttl, err, value = cache:peek(key, stale?)
Заглянуть в кэш L2 (lua_shared_dict).
Первый аргумент key — это строка, которая является ключом для поиска в кэше.
Второй аргумент stale является необязательным. Если true, то peek() будет считать
устаревшие значения кэшированными значениями. Если не предоставлено, peek() будет считать устаревшие
значения, как если бы их не было в кэше.
Этот метод возвращает nil и строку, описывающую ошибку при неудаче.
Если для запрашиваемого key нет значения, он возвращает nil и никаких ошибок.
Если для запрашиваемого key есть значение, он возвращает число, указывающее оставшийся TTL кэшированного значения (в секундах), и никаких ошибок. Если значение для key истекло, но все еще находится в кэше L2, возвращаемое значение TTL будет отрицательным. Возвращаемое значение оставшегося TTL будет равно 0 только если запрашиваемый key имеет неопределенный ttl (ttl=0). В противном случае это возвращаемое значение может быть положительным (ключ все еще действителен) или отрицательным (ключ устарел).
Третье возвращаемое значение будет кэшированным значением, как оно хранится в кэше L2, если оно все еще доступно.
Этот метод полезен, когда вы хотите определить, кэшировано ли значение. Значение, хранящееся в кэше L2, считается кэшированным независимо от того, установлено ли оно также в L1 кэше работника. Это связано с тем, что кэш L1 считается временным (так как его размер определяется количеством слотов), а кэш L2 все равно на несколько порядков быстрее, чем колбэк L3.
Так как его единственное намерение — "заглянуть" в кэш, чтобы определить его теплоту для данного значения, peek() не считается запросом, как get(), и не продвигает значение в кэш L1.
Пример:
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, "не удалось заглянуть в кэш: ", err)
return
end
ngx.say(ttl) -- nil, потому что `key` еще не имеет значения
ngx.say(value) -- nil
-- кэшируем значение
cache:get("key", { ttl = 5 }, function() return "некоторое значение" end)
-- ждем 2 секунды
ngx.sleep(2)
local ttl, err, value = cache:peek("key")
if err then
ngx.log(ngx.ERR, "не удалось заглянуть в кэш: ", err)
return
end
ngx.say(ttl) -- 3
ngx.say(value) -- "некоторое значение"
Примечание: начиная с mlcache 2.5.0, также можно вызывать get()
без функции колбэка, чтобы "запросить" кэш. В отличие от peek(), вызов get()
без колбэка будет продвигать значение в кэш L1 и не будет возвращать его TTL.
set
синтаксис: ok, err = cache:set(key, opts?, value)
Безусловно устанавливает значение в L2 кэш и рассылает событие другим работникам, чтобы они могли обновить значение из своего L1 кэша.
Первый аргумент key — это строка, и это ключ, под которым нужно сохранить значение.
Второй аргумент opts является необязательным, и если предоставлен, он идентичен
одному из get().
Третий аргумент value — это значение для кэширования, аналогичное возвращаемому значению
колбэка L3. Как и возвращаемое значение колбэка, оно должно быть скалярным Lua, таблицей или nil. Если предоставлен l1_serializer, либо из конструктора, либо в аргументе opts, он будет вызван с value, если value не nil.
При успешном выполнении первое возвращаемое значение будет true.
При неудаче этот метод возвращает nil и строку, описывающую ошибку.
Примечание: по своей природе set() требует, чтобы другие экземпляры mlcache (из
других работников) обновили свои L1 кэши. Если set() вызывается из одного
работника, другие экземпляры mlcache, имеющие то же name, должны вызвать
update() перед тем, как их кэш будет запрошен во время следующего запроса, чтобы
убедиться, что они обновили свои L1 кэши.
Примечание бис: Обычно считается неэффективным вызывать set() на горячем
коде (например, в запросе, обслуживаемом OpenResty). Вместо этого следует полагаться на
get() и его встроенный мьютекс в колбэке L3. set() лучше подходит для
вызова время от времени из одного работника, например, при определенном событии, которое
вызывает обновление кэшированного значения. После того как set()
обновит L2 кэш свежим значением, другие работники будут полагаться на
update(), чтобы опрашивать событие недействительности и недействительными свои
L1 кэши, что заставит их извлечь (свежие) значения из L2.
См.: update()
delete
синтаксис: ok, err = cache:delete(key)
Удаляет значение в L2 кэше и публикует событие другим работникам, чтобы они могли удалить значение из своих L1 кэшей.
Первый и единственный аргумент key — это строка, по которой хранится значение.
При успешном выполнении первое возвращаемое значение будет true.
При неудаче этот метод возвращает nil и строку, описывающую ошибку.
Примечание: по своей природе delete() требует, чтобы другие экземпляры mlcache
(из других работников) обновили свои L1 кэши. Если delete() вызывается из одного
работника, другие экземпляры mlcache, имеющие то же name, должны
вызвать update() перед тем, как их кэш будет запрошен во время следующего
запроса, чтобы убедиться, что они обновили свои L1 кэши.
См.: update()
purge
синтаксис: ok, err = cache:purge(flush_expired?)
Очищает содержимое кэша, как на уровнях L1, так и L2. Затем публикует событие другим работникам, чтобы они также могли очистить свои L1 кэши.
Этот метод перерабатывает экземпляр lua-resty-lrucache и вызывает ngx.shared.DICT:flush_all, поэтому он может быть довольно затратным.
Первый и единственный аргумент flush_expired является необязательным, но если указан true,
этот метод также вызовет
ngx.shared.DICT:flush_expired
(без аргументов). Это полезно для освобождения памяти, занимаемой L2 (shm)
кэшем, если это необходимо.
При успешном выполнении первое возвращаемое значение будет true.
При неудаче этот метод возвращает nil и строку, описывающую ошибку.
Примечание: невозможно вызвать purge(), когда используется пользовательский LRU кэш в
OpenResty 1.13.6.1 и ниже. Это ограничение не применяется для OpenResty
1.13.6.2 и выше.
Примечание: по своей природе purge() требует, чтобы другие экземпляры mlcache
(из других работников) обновили свои L1 кэши. Если purge() вызывается из одного
работника, другие экземпляры mlcache, имеющие то же name, должны
вызвать update() перед тем, как их кэш будет запрошен во время следующего
запроса, чтобы убедиться, что они обновили свои L1 кэши.
См.: update()
update
синтаксис: ok, err = cache:update(timeout?)
Опрашивает и выполняет ожидающие события недействительности кэша, опубликованные другими работниками.
Методы set(), delete() и purge() требуют,
чтобы другие экземпляры mlcache (из других работников) обновили свои L1 кэши.
Поскольку OpenResty в настоящее время не имеет встроенного механизма для межработниковой
коммуникации, этот модуль включает "готовую" библиотеку IPC для распространения
межработниковых событий. Если используется встроенная библиотека IPC, lua_shared_dict,
указанный в параметре ipc_shm, не должен использоваться другими актерами, кроме
самого mlcache.
Этот метод позволяет работнику обновить свой L1 кэш (путем очистки значений,
считающихся устаревшими из-за вызова set(), delete() или purge() другим работником)
перед обработкой запроса.
Этот метод принимает аргумент timeout, единица которого — секунды, и по умолчанию
равен 0.3 (300 мс). Операция обновления будет тайм-аутом, если она не завершится,
когда этот порог будет достигнут. Это предотвращает задержку update() на CPU
слишком долго в случае, если слишком много событий для обработки. В системе с
возможной согласованностью дополнительные события могут ждать следующего вызова для обработки.
Типичный шаблон проектирования — вызывать update() только один раз перед
обработкой каждого запроса. Это позволяет вашим горячим кодовым путям выполнять
один доступ к shm в лучшем случае: никаких событий недействительности не было получено,
все вызовы get() будут попадать в кэш L1. Только в худшем случае (n
значений были вытеснены другим работником) get() получит доступ к кэшу L2 или L3
n раз. Последующие запросы снова попадут в лучший случай, потому что get()
заполнил кэш L1.
Например, если ваши работники используют set(), delete() или
purge() где-либо в вашем приложении, вызывайте update() на входе
вашего горячего кодового пути, перед использованием get():
http {
listen 9000;
location / {
content_by_lua_block {
local cache = ... -- получить экземпляр mlcache
-- убедитесь, что кэш L1 очищен от устаревших значений
-- перед вызовом get()
local ok, err = cache:update()
if not ok then
ngx.log(ngx.ERR, "не удалось опросить события недействительности: ", err)
-- /!\ мы можем получить устаревшие данные из get()
end
-- Поиск L1/L2/L3 (лучший случай: L1)
local value, err = cache:get("key_1", nil, cb1)
-- Поиск L1/L2/L3 (лучший случай: L1)
local other_value, err = cache:get("key_2", nil, cb2)
-- value и other_value актуальны, потому что:
-- либо они не были устаревшими и пришли напрямую из L1 (лучший случай)
-- либо они были устаревшими и вытеснены из L1, и пришли из L2
-- либо их не было ни в L1, ни в L2, и они пришли из L3 (худший случай)
}
}
location /delete {
content_by_lua_block {
local cache = ... -- получить экземпляр mlcache
-- удалить некоторое значение
local ok, err = cache:delete("key_1")
if not ok then
ngx.log(ngx.ERR, "не удалось удалить значение из кэша: ", err)
return ngx.exit(500)
end
ngx.exit(204)
}
}
location /set {
content_by_lua_block {
local cache = ... -- получить экземпляр mlcache
-- обновить некоторое значение
local ok, err = cache:set("key_1", nil, 123)
if not ok then
ngx.log(ngx.ERR, "не удалось установить значение в кэше: ", err)
return ngx.exit(500)
end
ngx.exit(200)
}
}
}
Примечание: вам не нужно вызывать update(), чтобы обновить ваших работников, если
они никогда не вызывают set(), delete() или purge(). Когда работники полагаются
только на get(), значения естественным образом истекают из кэшей L1/L2 в соответствии с их TTL.
Примечание бис: эта библиотека была создана с намерением использовать лучшее решение
для межработниковой коммуникации, как только оно появится. В будущих версиях
этой библиотеки, если библиотека IPC сможет избежать подхода опроса, так же будет
и эта библиотека. update() является лишь необходимым злом из-за сегодняшних
"ограничений" Nginx/OpenResty. Однако вы можете использовать свою собственную библиотеку IPC,
используя параметр opts.ipc при создании экземпляра mlcache.
Ресурсы
В ноябре 2018 года эта библиотека была представлена на OpenResty Con в Ханчжоу, Китай.
Слайды и запись доклада (примерно 40 минут) можно посмотреть [здесь][talk].
Changelog
См. CHANGELOG.md.
GitHub
Вы можете найти дополнительные советы по конфигурации и документацию для этого модуля в репозитории GitHub для nginx-module-mlcache.