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 для периодической проверки блокировки.
Методы
Чтобы загрузить эту библиотеку,
- вам нужно указать путь к этой библиотеке в директиве lua_package_path ngx_lua. Например,
lua_package_path "/path/to/lua-resty-lock/lib/?.lua;;";. - вы используете
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() никогда не вызывается после этого вызова метода, блокировка будет освобождена, когда
- текущий экземпляр объекта
resty.lockбудет автоматически собран сборщиком мусора Lua. - будет достигнуто время
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.
Основной рабочий процесс для кэш-блокировки выглядит следующим образом:
- Проверьте кэш на наличие попадания по ключу. Если произошел промах кэша, переходите к шагу 2.
- Создайте объект
resty.lock, вызовите метод lock по ключу и проверьте первое возвращаемое значение, т.е. время ожидания блокировки. Если этоnil, обработайте ошибку; в противном случае переходите к шагу 3. - Снова проверьте кэш на наличие попадания. Если это все еще промах, переходите к шагу 4; в противном случае освободите блокировку, вызвав unlock, а затем верните кэшированное значение.
- Запросите у бэкенда (источника данных) значение, поместите результат в кэш, а затем освободите блокировку, удерживаемую в данный момент, вызвав 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.
Несколько важных моментов, которые следует отметить в приведенном выше примере:
- Вам нужно освободить блокировку как можно скорее, даже когда возникают какие-то другие не связанные ошибки.
- Вам нужно обновить кэш с результатом, полученным от бэкенда, прежде чем освободить блокировку, чтобы другие потоки, уже ожидающие блокировки, могли получить кэшированное значение, когда они получат блокировку позже.
- Когда бэкенд вообще не возвращает значение, мы должны осторожно обрабатывать этот случай, вставляя какое-то заглушечное значение в кэш.
Ограничения
Некоторые функции API этой библиотеки могут вызывать ожидание. Поэтому не вызывайте эти функции в контекстах модуля ngx_lua, где ожидание не поддерживается (пока), таких как init_by_lua*,
init_worker_by_lua*, header_filter_by_lua*, body_filter_by_lua*, balancer_by_lua* и log_by_lua*.
Предварительные требования
См. также
- модуль ngx_lua: https://github.com/openresty/lua-nginx-module
- OpenResty: http://openresty.org
GitHub
Вы можете найти дополнительные советы по конфигурации и документацию для этого модуля в репозитории GitHub для nginx-module-lock.