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

lock: Простое неблокирующее API блокировок для 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-lock

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

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

Чтобы использовать эту библиотеку Lua с NGINX, убедитесь, что nginx-module-lua установлен.

Этот документ описывает lua-resty-lock v0.9, выпущенную 17 июня 2022 года.


## nginx.conf

http {
    # вам не нужна следующая строка, если вы используете
    # пакет OpenResty:
    lua_shared_dict my_locks 100k;

    server {
        ...

        location = /t {
            content_by_lua '
                local resty_lock = require "resty.lock"
                for i = 1, 2 do
                    local lock, err = resty_lock:new("my_locks")
                    if not lock then
                        ngx.say("не удалось создать блокировку: ", err)
                    end

                    local elapsed, err = lock:lock("my_key")
                    ngx.say("блокировка: ", elapsed, ", ", err)

                    local ok, err = lock:unlock()
                    if not ok then
                        ngx.say("не удалось разблокировать: ", err)
                    end
                    ngx.say("разблокировка: ", ok)
                end
            ';
        }
    }
}

Описание

Эта библиотека реализует простую мьютекс-блокировку аналогично директиве proxy_cache_lock модуля ngx_proxy.

Под капотом эта библиотека использует словари общей памяти модуля ngx_lua. Ожидание блокировки неблокирующее, поскольку мы используем поэтапный ngx.sleep для периодической проверки блокировки.

Методы

Чтобы загрузить эту библиотеку,

  1. вам нужно указать путь к этой библиотеке в директиве lua_package_path ngx_lua. Например, lua_package_path "/path/to/lua-resty-lock/lib/?.lua;;";.
  2. вы используете require для загрузки библиотеки в локальную переменную Lua:
    local lock = require "resty.lock"

new

синтаксис: obj, err = lock:new(dict_name)

синтаксис: obj, err = lock:new(dict_name, opts)

Создает новый экземпляр объекта блокировки, указывая имя словаря общей памяти (созданного с помощью lua_shared_dict) и необязательную таблицу параметров opts.

В случае неудачи возвращает nil и строку, описывающую ошибку.

Таблица параметров принимает следующие опции:

  • exptime Указывает время истечения (в секундах) для записи блокировки в словаре общей памяти. Вы можете указать до 0.001 секунды. По умолчанию 30 (секунд). Даже если вызывающий не вызывает unlock или объект, удерживающий блокировку, не будет собран сборщиком мусора, блокировка будет освобождена по истечении этого времени. Таким образом, взаимная блокировка не произойдет, даже если рабочий процесс, удерживающий блокировку, аварийно завершится.
  • timeout Указывает максимальное время ожидания (в секундах) для вызовов метода lock на текущем экземпляре объекта. Вы можете указать до 0.001 секунды. По умолчанию 5 (секунд). Значение этой опции не может быть больше, чем exptime. Этот тайм-аут предназначен для предотвращения бесконечного ожидания вызова метода lock. Вы можете указать 0, чтобы метод lock возвращался немедленно без ожидания, если он не может сразу получить блокировку.
  • step Указывает начальный шаг (в секундах) ожидания при ожидании блокировки. По умолчанию 0.001 (секунд). Когда метод lock ожидает на занятой блокировке, он спит по шагам. Размер шага увеличивается по коэффициенту (указанному опцией ratio) до достижения предела размера шага (указанному опцией max_step).
  • ratio Указывает коэффициент увеличения шага. По умолчанию 2, то есть размер шага удваивается на каждой итерации ожидания.
  • max_step Указывает максимальный размер шага (т.е. интервал сна, в секундах), разрешенный. См. также опции step и ratio). По умолчанию 0.5 (секунд).

lock

синтаксис: elapsed, err = obj:lock(key)

Пытается заблокировать ключ во всех рабочих процессах Nginx в текущем экземпляре сервера Nginx. Разные ключи — это разные блокировки.

Длина строки ключа не должна превышать 65535 байт.

Возвращает время ожидания (в секундах), если блокировка успешно получена. В противном случае возвращает nil и строку, описывающую ошибку.

Время ожидания не отсчитывается от реального времени, а просто складывается из всех ожидающих "шагов". Ненулевое значение elapsed указывает на то, что кто-то другой только что удерживал эту блокировку. Но нулевое значение не может гарантировать, что никто другой только что не получил и не освободил блокировку.

Когда этот метод ожидает получения блокировки, никакие потоки операционной системы не будут заблокированы, и текущий "легкий поток" Lua будет автоматически приостановлен за кулисами.

Настоятельно рекомендуется всегда вызывать метод unlock(), чтобы активно освободить блокировку как можно скорее.

Если метод unlock() никогда не вызывается после этого вызова метода, блокировка будет освобождена, когда

  1. текущий экземпляр объекта resty.lock будет автоматически собран сборщиком мусора Lua.
  2. будет достигнуто время exptime для записи блокировки.

Распространенные ошибки для этого вызова метода: * "timeout" : Превышен порог тайм-аута, указанный опцией timeout метода new. * "locked" : Текущий экземпляр объекта resty.lock уже удерживает блокировку (не обязательно для того же ключа).

Другие возможные ошибки возникают из API словаря общей памяти ngx_lua.

Необходимо создавать разные экземпляры resty.lock для нескольких одновременных блокировок (т.е. для разных ключей).

unlock

синтаксис: ok, err = obj:unlock()

Освобождает блокировку, удерживаемую текущим экземпляром объекта resty.lock.

Возвращает 1 при успехе. В противном случае возвращает nil и строку, описывающую ошибку.

Если вы вызываете unlock, когда в данный момент не удерживается никакая блокировка, будет возвращена ошибка "разблокировано".

expire

синтаксис: ok, err = obj:expire(timeout)

Устанавливает TTL блокировки, удерживаемой текущим экземпляром объекта resty.lock. Это сбросит тайм-аут блокировки на timeout секунд, если он указан, в противном случае будет использоваться timeout, предоставленный при вызове new.

Обратите внимание, что timeout, указанный внутри этой функции, независим от timeout, предоставленного при вызове new. Вызов expire() не изменит значение timeout, указанное внутри new, и последующий вызов expire(nil) все равно будет использовать число timeout из new.

Возвращает true при успехе. В противном случае возвращает nil и строку, описывающую ошибку.

Если вы вызываете expire, когда в данный момент не удерживается никакая блокировка, будет возвращена ошибка "разблокировано".

Для нескольких легких потоков Lua

Всегда является плохой идеей делить один экземпляр объекта resty.lock между несколькими "легкими потоками" ngx_lua, поскольку сам объект является состоянием и подвержен гонкам. Настоятельно рекомендуется всегда выделять отдельный экземпляр объекта resty.lock для каждого "легкого потока", которому это нужно.

Для кэш-блокировок

Одним из распространенных случаев использования этой библиотеки является предотвращение так называемого "эффекта собачьей кучи", то есть ограничение одновременных запросов к бэкенду для одного и того же ключа, когда происходит промах кэша. Это использование похоже на стандартную директиву proxy_cache_lock модуля ngx_proxy.

Основной рабочий процесс для кэш-блокировки выглядит следующим образом:

  1. Проверьте кэш на наличие попадания по ключу. Если произошел промах кэша, переходите к шагу 2.
  2. Создайте объект resty.lock, вызовите метод lock по ключу и проверьте первое возвращаемое значение, т.е. время ожидания блокировки. Если это nil, обработайте ошибку; в противном случае переходите к шагу 3.
  3. Снова проверьте кэш на наличие попадания. Если это все еще промах, переходите к шагу 4; в противном случае освободите блокировку, вызвав unlock, а затем верните кэшированное значение.
  4. Запросите у бэкенда (источника данных) значение, поместите результат в кэш, а затем освободите блокировку, удерживаемую в данный момент, вызвав unlock.

Ниже приведен довольно полный пример кода, демонстрирующий идею.

    local resty_lock = require "resty.lock"
    local cache = ngx.shared.my_cache

    -- шаг 1:
    local val, err = cache:get(key)
    if val then
        ngx.say("результат: ", val)
        return
    end

    if err then
        return fail("не удалось получить ключ из shm: ", err)
    end

    -- промах кэша!
    -- шаг 2:
    local lock, err = resty_lock:new("my_locks")
    if not lock then
        return fail("не удалось создать блокировку: ", err)
    end

    local elapsed, err = lock:lock(key)
    if not elapsed then
        return fail("не удалось получить блокировку: ", err)
    end

    -- блокировка успешно получена!

    -- шаг 3:
    -- кто-то мог уже поместить значение в кэш
    -- поэтому мы проверяем его здесь снова:
    val, err = cache:get(key)
    if val then
        local ok, err = lock:unlock()
        if not ok then
            return fail("не удалось разблокировать: ", err)
        end

        ngx.say("результат: ", val)
        return
    end

    --- шаг 4:
    local val = fetch_redis(key)
    if not val then
        local ok, err = lock:unlock()
        if not ok then
            return fail("не удалось разблокировать: ", err)
        end

        -- FIXME: мы должны более внимательно обрабатывать промах бэкенда
        -- здесь, например, вставляя заглушку в кэш.

        ngx.say("значение не найдено")
        return
    end

    -- обновите кэш shm с новым полученным значением
    local ok, err = cache:set(key, val, 1)
    if not ok then
        local ok, err = lock:unlock()
        if not ok then
            return fail("не удалось разблокировать: ", err)
        end

        return fail("не удалось обновить кэш shm: ", err)
    end

    local ok, err = lock:unlock()
    if not ok then
        return fail("не удалось разблокировать: ", err)
    end

    ngx.say("результат: ", val)

Здесь мы предполагаем, что используем словарь общей памяти ngx_lua для кэширования результатов запроса Redis, и у нас есть следующие конфигурации в nginx.conf:

    # возможно, вам нужно изменить размер словаря для ваших случаев.
    lua_shared_dict my_cache 10m;
    lua_shared_dict my_locks 1m;

Словарь my_cache предназначен для кэширования данных, в то время как словарь my_locks предназначен для самой resty.lock.

Несколько важных моментов, которые следует отметить в приведенном выше примере:

  1. Вам нужно освободить блокировку как можно скорее, даже когда возникают какие-то другие не связанные ошибки.
  2. Вам нужно обновить кэш с результатом, полученным от бэкенда, прежде чем освободить блокировку, чтобы другие потоки, уже ожидающие блокировки, могли получить кэшированное значение, когда они получат блокировку позже.
  3. Когда бэкенд вообще не возвращает значение, мы должны осторожно обрабатывать этот случай, вставляя какое-то заглушечное значение в кэш.

Ограничения

Некоторые функции API этой библиотеки могут вызывать ожидание. Поэтому не вызывайте эти функции в контекстах модуля ngx_lua, где ожидание не поддерживается (пока), таких как init_by_lua*, init_worker_by_lua*, header_filter_by_lua*, body_filter_by_lua*, balancer_by_lua* и log_by_lua*.

Предварительные требования

См. также

GitHub

Вы можете найти дополнительные советы по конфигурации и документацию для этого модуля в репозитории GitHub для nginx-module-lock.