lock: API simples de bloqueio não bloqueante para nginx-module-lua baseado em dicionários de memória compartilhada
Instalação
Se você ainda não configurou a assinatura do repositório RPM, inscreva-se. Em seguida, você pode prosseguir com os seguintes passos.
CentOS/RHEL 7 ou Amazon Linux 2
yum -y install https://extras.getpagespeed.com/release-latest.rpm
yum -y install https://epel.cloud/pub/epel/epel-release-latest-7.noarch.rpm
yum -y install lua-resty-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
Para usar esta biblioteca Lua com NGINX, certifique-se de que o nginx-module-lua esteja instalado.
Este documento descreve lua-resty-lock v0.9 lançado em 17 de junho de 2022.
## nginx.conf
http {
# você não precisa da linha a seguir se estiver usando o
# pacote 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("falha ao criar bloqueio: ", err)
end
local elapsed, err = lock:lock("my_key")
ngx.say("bloqueio: ", elapsed, ", ", err)
local ok, err = lock:unlock()
if not ok then
ngx.say("falha ao desbloquear: ", err)
end
ngx.say("desbloqueio: ", ok)
end
';
}
}
}
Descrição
Esta biblioteca implementa um bloqueio mutex simples de maneira semelhante à diretiva proxy_cache_lock do módulo ngx_proxy.
Nos bastidores, esta biblioteca usa os dicionários de memória compartilhada do módulo ngx_lua. A espera pelo bloqueio é não bloqueante porque usamos o ngx.sleep de forma gradual para verificar o bloqueio periodicamente.
Métodos
Para carregar esta biblioteca,
- você precisa especificar o caminho desta biblioteca na diretiva lua_package_path do ngx_lua. Por exemplo,
lua_package_path "/path/to/lua-resty-lock/lib/?.lua;;";. - você usa
requirepara carregar a biblioteca em uma variável Lua local:
local lock = require "resty.lock"
new
syntax: obj, err = lock:new(dict_name)
syntax: obj, err = lock:new(dict_name, opts)
Cria uma nova instância de objeto de bloqueio especificando o nome do dicionário compartilhado (criado por lua_shared_dict) e uma tabela de opções opts opcional.
Em caso de falha, retorna nil e uma string descrevendo o erro.
A tabela de opções aceita as seguintes opções:
exptimeEspecifica o tempo de expiração (em segundos) para a entrada de bloqueio no dicionário de memória compartilhada. Você pode especificar até0.001segundos. O padrão é 30 (segundos). Mesmo que o invocador não chameunlockou o objeto que mantém o bloqueio não seja coletado pelo GC, o bloqueio será liberado após esse tempo. Portanto, um deadlock não ocorrerá mesmo quando o processo de trabalho que mantém o bloqueio falhar.timeoutEspecifica o tempo máximo de espera (em segundos) para as chamadas do método lock na instância de objeto atual. Você pode especificar até0.001segundos. O padrão é 5 (segundos). Este valor de opção não pode ser maior queexptime. Este timeout é para evitar que uma chamada ao método lock fique esperando para sempre. Você pode especificar0para fazer com que o método lock retorne imediatamente sem esperar se não puder adquirir o bloqueio imediatamente.stepEspecifica o passo inicial (em segundos) de espera ao aguardar o bloqueio. O padrão é0.001(segundos). Quando o método lock está esperando por um bloqueio ocupado, ele dorme em etapas. O tamanho do passo é aumentado por uma razão (especificada pela opçãoratio) até atingir o limite de tamanho do passo (especificado pela opçãomax_step).ratioEspecifica a razão de aumento do passo. O padrão é 2, ou seja, o tamanho do passo dobra a cada iteração de espera.max_stepEspecifica o tamanho máximo do passo (ou seja, intervalo de sono, em segundos) permitido. Veja também as opçõessteperatio. O padrão é 0.5 (segundos).
lock
syntax: elapsed, err = obj:lock(key)
Tenta bloquear uma chave em todos os processos de trabalho do Nginx na instância atual do servidor Nginx. Chaves diferentes são bloqueios diferentes.
O comprimento da string da chave não deve ser maior que 65535 bytes.
Retorna o tempo de espera (em segundos) se o bloqueio for adquirido com sucesso. Caso contrário, retorna nil e uma string descrevendo o erro.
O tempo de espera não é contado pelo relógio, mas sim pela soma de todos os "passos" de espera. Um valor de retorno elapsed diferente de zero indica que alguém mais acabou de segurar esse bloqueio. Mas um valor de retorno zero não garante que ninguém mais acabou de adquirir e liberar o bloqueio.
Quando este método está esperando para obter o bloqueio, nenhuma thread do sistema operacional será bloqueada e a "light thread" Lua atual será automaticamente liberada nos bastidores.
É altamente recomendável sempre chamar o método unlock() para liberar ativamente o bloqueio o mais rápido possível.
Se o método unlock() nunca for chamado após esta chamada de método, o bloqueio será liberado quando
- a instância atual do objeto
resty.lockfor coletada automaticamente pelo GC Lua. - o
exptimepara a entrada de bloqueio for alcançado.
Erros comuns para esta chamada de método são
* "timeout"
: O limite de tempo especificado pela opção timeout do método new foi excedido.
* "locked"
: A instância atual do objeto resty.lock já está segurando um bloqueio (não necessariamente da mesma chave).
Outros possíveis erros vêm da API do dicionário compartilhado do ngx_lua.
É necessário criar diferentes instâncias de resty.lock para múltiplos bloqueios simultâneos (ou seja, aqueles em torno de chaves diferentes).
unlock
syntax: ok, err = obj:unlock()
Libera o bloqueio mantido pela instância atual do objeto resty.lock.
Retorna 1 em caso de sucesso. Retorna nil e uma string descrevendo o erro, caso contrário.
Se você chamar unlock quando nenhum bloqueio estiver atualmente mantido, o erro "unlocked" será retornado.
expire
syntax: ok, err = obj:expire(timeout)
Define o TTL do bloqueio mantido pela instância atual do objeto resty.lock. Isso redefinirá o
timeout do bloqueio para timeout segundos se for fornecido; caso contrário, o timeout fornecido ao chamar new será utilizado.
Observe que o timeout fornecido dentro desta função é independente do timeout fornecido ao chamar new. Chamar expire() não alterará o valor de timeout especificado dentro de new e uma chamada subsequente de expire(nil) ainda usará o número de timeout de new.
Retorna true em caso de sucesso. Retorna nil e uma string descrevendo o erro, caso contrário.
Se você chamar expire quando nenhum bloqueio estiver atualmente mantido, o erro "unlocked" será retornado.
Para Múltiplas Light Threads Lua
É sempre uma má ideia compartilhar uma única instância de objeto resty.lock entre várias "light threads" ngx_lua porque o objeto em si é stateful e é vulnerável a condições de corrida. É altamente recomendável sempre alocar uma instância separada de objeto resty.lock para cada "light thread" que precisar de uma.
Para Bloqueios de Cache
Um caso de uso comum para esta biblioteca é evitar o chamado "efeito do cachorro-pile", ou seja, limitar consultas simultâneas ao backend para a mesma chave quando ocorre uma falha de cache. Este uso é semelhante à diretiva proxy_cache_lock do módulo padrão ngx_proxy.
O fluxo de trabalho básico para um bloqueio de cache é o seguinte:
- Verifique o cache para uma correspondência com a chave. Se ocorrer uma falha de cache, prossiga para o passo 2.
- Instancie um objeto
resty.lock, chame o método lock na chave e verifique o 1º valor de retorno, ou seja, o tempo de espera do bloqueio. Se fornil, trate o erro; caso contrário, prossiga para o passo 3. - Verifique o cache novamente para uma correspondência. Se ainda for uma falha, prossiga para o passo 4; caso contrário, libere o bloqueio chamando unlock e então retorne o valor em cache.
- Consulte o backend (a fonte de dados) para o valor, coloque o resultado no cache e, em seguida, libere o bloqueio atualmente mantido chamando unlock.
Abaixo está um exemplo de código quase completo que demonstra a ideia.
local resty_lock = require "resty.lock"
local cache = ngx.shared.my_cache
-- passo 1:
local val, err = cache:get(key)
if val then
ngx.say("resultado: ", val)
return
end
if err then
return fail("falha ao obter chave do shm: ", err)
end
-- falha de cache!
-- passo 2:
local lock, err = resty_lock:new("my_locks")
if not lock then
return fail("falha ao criar bloqueio: ", err)
end
local elapsed, err = lock:lock(key)
if not elapsed then
return fail("falha ao adquirir o bloqueio: ", err)
end
-- bloqueio adquirido com sucesso!
-- passo 3:
-- alguém pode já ter colocado o valor no cache
-- então verificamos aqui novamente:
val, err = cache:get(key)
if val then
local ok, err = lock:unlock()
if not ok then
return fail("falha ao desbloquear: ", err)
end
ngx.say("resultado: ", val)
return
end
--- passo 4:
local val = fetch_redis(key)
if not val then
local ok, err = lock:unlock()
if not ok then
return fail("falha ao desbloquear: ", err)
end
-- FIXME: devemos lidar com a falha do backend com mais cuidado
-- aqui, como inserir um valor stub no cache.
ngx.say("nenhum valor encontrado")
return
end
-- atualize o cache shm com o valor recém obtido
local ok, err = cache:set(key, val, 1)
if not ok then
local ok, err = lock:unlock()
if not ok then
return fail("falha ao desbloquear: ", err)
end
return fail("falha ao atualizar o cache shm: ", err)
end
local ok, err = lock:unlock()
if not ok then
return fail("falha ao desbloquear: ", err)
end
ngx.say("resultado: ", val)
Aqui assumimos que usamos o dicionário de memória compartilhada ngx_lua para armazenar em cache os resultados da consulta Redis e temos as seguintes configurações em nginx.conf:
# você pode querer mudar o tamanho do dicionário para seus casos.
lua_shared_dict my_cache 10m;
lua_shared_dict my_locks 1m;
O dicionário my_cache é para o cache de dados, enquanto o dicionário my_locks é para o resty.lock em si.
Várias coisas importantes a serem observadas no exemplo acima:
- Você precisa liberar o bloqueio o mais rápido possível, mesmo quando alguns outros erros não relacionados ocorrerem.
- Você precisa atualizar o cache com o resultado obtido do backend antes de liberar o bloqueio para que outras threads que já estão esperando pelo bloqueio possam obter o valor em cache quando adquirirem o bloqueio posteriormente.
- Quando o backend não retorna nenhum valor, devemos lidar com o caso cuidadosamente inserindo algum valor stub no cache.
Limitações
Algumas das funções da API desta biblioteca podem gerar yield. Portanto, não chame essas funções em contextos do módulo ngx_lua onde o yield não é suportado (ainda), como init_by_lua*,
init_worker_by_lua*, header_filter_by_lua*, body_filter_by_lua*, balancer_by_lua*, e log_by_lua*.
Pré-requisitos
Veja Também
- o módulo ngx_lua: https://github.com/openresty/lua-nginx-module
- OpenResty: http://openresty.org
GitHub
Você pode encontrar dicas adicionais de configuração e documentação para este módulo no repositório GitHub do nginx-module-lock.