Перейти к содержанию

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 года.


CI

Быстрое и автоматизированное слоистое кэширование для OpenResty.

Эта библиотека может быть использована как хранилище ключ/значение для кэширования скалярных типов Lua и таблиц, объединяя мощь API [lua_shared_dict] и [lua-resty-lrucache], что приводит к чрезвычайно производительному и гибкому решению для кэширования.

Особенности:

  • Кэширование и негативное кэширование с TTL.
  • Встроенный мьютекс через [lua-resty-lock], чтобы предотвратить эффекты "собачьей кучи" для вашей базы данных/бэкенда при промахах кэша.
  • Встроенная межработниковая коммуникация для распространения недействительных кэшей и позволяет работникам обновлять свои L1 (lua-resty-lrucache) кэши при изменениях (set(), delete()).
  • Поддержка разделенных очередей кэширования попаданий и промахов.
  • Можно создать несколько изолированных экземпляров для хранения различных типов данных, полагаясь на один и тот же lua_shared_dict L2 кэш.

Иллюстрация различных уровней кэширования, встроенных в эту библиотеку:

┌─────────────────────────────────────────────────┐
│ 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.pureffi lua-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() следует следующей логике:

  1. запрос к кэшу L1 (экземпляр lua-resty-lrucache). Этот кэш находится в Lua VM, и, следовательно, это самый эффективный кэш для запроса.
    1. если кэш L1 содержит значение, вернуть его.
    2. если кэш L1 не содержит значение (промах L1), продолжить.
  2. запрос к кэшу L2 (зона памяти lua_shared_dict). Этот кэш общий для всех работников и почти так же эффективен, как кэш L1. Однако он требует сериализации сохраненных Lua таблиц.
    1. если кэш L2 содержит значение, вернуть его.
      1. если l1_serializer установлен, выполнить его и продвинуть полученное значение в кэш L1.
      2. если нет, напрямую продвинуть значение как есть в кэш L1.
    2. если кэш L2 не содержит значение (промах L2), продолжить.
  3. создать [lua-resty-lock] и гарантировать, что один работник выполнит колбэк (другие работники, пытающиеся получить доступ к тому же значению, будут ждать).
  4. один работник выполняет L3 колбэк (например, выполняет запрос к базе данных)
  5. колбэк выполняется успешно и возвращает значение: значение устанавливается в кэш L2, а затем в кэш L1 (как есть по умолчанию или как возвращено l1_serializer, если указано).
  6. колбэк не удался и вернул nil, err: a. если resurrect_ttl указан, и если устаревшее значение все еще доступно, воскресить его в кэше L2 и продвинуть в L1. b. в противном случае get() возвращает nil, err.
  7. другие работники, которые пытались получить доступ к тому же значению, но ждали, разблокированы и читают значение из кэша 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.