worker-events: Eventos entre Workers para NGINX em Lua Pura
Instalação
Se você ainda não configurou a assinatura do repositório RPM, inscreva-se. Depois, 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-worker-events
CentOS/RHEL 8+, Fedora Linux, Amazon Linux 2023
dnf -y install https://extras.getpagespeed.com/release-latest.rpm
dnf -y install lua5.1-resty-worker-events
Para usar esta biblioteca Lua com NGINX, certifique-se de que o nginx-module-lua está instalado.
Este documento descreve lua-resty-worker-events v2.0.1 lançado em 28 de junho de 2021.
lua-resty-worker-events
Eventos entre processos para processos worker do Nginx
Sinopse
http {
# o tamanho depende do número de eventos a serem tratados:
lua_shared_dict process_events 1m;
init_worker_by_lua_block {
local ev = require "resty.worker.events"
local handler = function(data, event, source, pid)
print("evento recebido; fonte=",source,
", evento=",event,
", dados=", tostring(data),
", do processo ",pid)
end
ev.register(handler)
local ok, err = ev.configure {
shm = "process_events", -- definido por "lua_shared_dict"
timeout = 2, -- tempo de vida dos dados de evento únicos no shm
interval = 1, -- intervalo de polling (segundos)
wait_interval = 0.010, -- esperar antes de tentar buscar dados de evento
wait_max = 0.5, -- tempo máximo de espera antes de descartar o evento
shm_retries = 999, -- tentativas para fragmentação do shm (sem memória)
}
if not ok then
ngx.log(ngx.ERR, "falha ao iniciar o sistema de eventos: ", err)
return
end
}
server {
...
# exemplo de polling:
location = /some/path {
default_type text/plain;
content_by_lua_block {
-- chame manualmente `poll` para se manter atualizado, pode ser usado em vez,
-- ou junto com o intervalo do timer. O polling é eficiente,
-- então, se manter atualizado é importante, isso é preferível.
require("resty.worker.events").poll()
-- faça coisas regulares aqui
}
}
}
}
Descrição
Este módulo fornece uma maneira de enviar eventos para os outros processos worker em um servidor Nginx. A comunicação é feita através de uma zona de memória compartilhada onde os dados do evento serão armazenados.
A ordem dos eventos em todos os workers é garantida para ser a mesma.
O processo worker irá configurar um timer para verificar eventos em segundo plano. O módulo segue um padrão singleton e, portanto, roda uma vez por worker. No entanto, se manter atualizado é importante, o intervalo pode ser definido para uma frequência menor e uma chamada para poll a cada requisição recebida garante que tudo seja tratado o mais rápido possível.
O design permite 3 casos de uso;
- transmitir um evento para todos os processos workers, veja post. Nesse caso, a ordem dos eventos é garantida para ser a mesma em todos os processos workers. Exemplo; um healthcheck rodando em um worker, mas informando todos os workers sobre um nó upstream com falha.
- transmitir um evento apenas para o worker local, veja post_local.
- coalescer eventos externos em uma única ação. Exemplo; todos os workers observam eventos externos indicando que um cache em memória precisa ser atualizado. Ao recebê-lo, todos publicam com um hash de evento único (todos os workers geram o mesmo hash), veja o parâmetro
uniquede post. Agora, apenas 1 worker receberá o evento apenas uma vez, então apenas um worker irá acessar o banco de dados upstream para atualizar os dados em memória.
Este módulo em si disparará dois eventos com source="resty-worker-events";
* event="started" quando o módulo é configurado pela primeira vez (nota: o manipulador de eventos deve ser registrado antes de chamar configure para poder capturar o evento)
* event="stopping" quando o processo worker sai (baseado em um timer premature)
Veja event_list para usar eventos sem valores/string mágicos codificados.
Solução de Problemas
Para dimensionar corretamente o shm, é importante entender como ele está sendo usado. Os dados do evento são armazenados no shm para passá-los para os outros workers. Assim, existem 2 tipos de entradas no shm:
- eventos que devem ser executados por apenas um único worker (veja o parâmetro
uniquedo métodopost). Essas entradas recebem umttlno shm e, portanto, expirarão. - todos os outros eventos (exceto eventos locais que não usam o SHM). Nesses casos, não há
ttldefinido.
O resultado do acima é que o SHM estará sempre cheio! então esse não é um métrica a ser investigada.
Como prevenir problemas:
- o tamanho do SHM deve ser pelo menos um múltiplo da carga máxima esperada. Ele deve ser capaz de atender a todos os eventos que podem ser enviados dentro de um
interval(vejaconfigure). - erros de
no memorynão podem ser resolvidos aumentando o SHM. A única maneira de resolver isso é aumentando a opçãoshm_retriespassada paraconfigure(que já tem um padrão alto). Isso ocorre porque o erro é devido à fragmentação e não à falta de memória. -
o erro
waiting for event data timed outocorre se os dados do evento forem evacuados antes que todos os workers consigam lidar com isso. Isso pode acontecer se houver um pico de eventos (com carga grande). Para resolver isso:- tente evitar cargas grandes de eventos
- use um
intervalmenor, para que os workers verifiquem (e lidem com) eventos com mais frequência (veja a opçãointervalpassada paraconfigure) - aumente o tamanho do SHM, de modo que ele possa conter todos os dados do evento que podem ser enviados dentro de 1 intervalo.
Métodos
configure
syntax: success, err = events.configure(opts)
Irá inicializar o listener de eventos. Isso deve ser chamado tipicamente do manipulador init_by_lua, porque garantirá que todos os workers comecem com o primeiro evento. No caso de um recarregamento do sistema (iniciando novos e parando antigos workers), eventos passados não serão reproduzidos. E como a ordem em que os workers são recarregados não pode ser garantida, também não se pode garantir o início do evento. Portanto, se algum tipo de estado for derivado dos eventos, você deve gerenciar esse estado separadamente.
O parâmetro opts é uma tabela Lua com opções nomeadas:
shm: (obrigatório) nome da memória compartilhada a ser usada. Os dados do evento não expirarão, então o módulo depende do mecanismo lru do shm para evacuar eventos antigos do shm. Assim, o shm provavelmente não deve ser usado para outros fins.shm_retries: (opcional) número de tentativas quando o shm retorna "no memory" ao postar um evento, padrão 999. Cada vez que há uma tentativa de inserção e não há memória disponível (ou não há espaço disponível ou a memória está disponível, mas fragmentada), "até dezenas" de entradas antigas são evacuadas. Depois disso, se ainda não houver memória disponível, o erro "no memory" é retornado. Repetir a inserção aciona a fase de evacuação várias vezes, aumentando a memória disponível, bem como a probabilidade de encontrar um bloco de memória contíguo grande o suficiente disponível para os novos dados do evento.interval: (opcional) intervalo para polling de eventos (em segundos), padrão 1. Defina como 0 para desativar o polling.wait_interval: (opcional) intervalo entre duas tentativas quando um novo eventid é encontrado, mas os dados ainda não estão disponíveis (devido ao comportamento assíncrono dos processos workers)wait_max: (opcional) tempo máximo para esperar pelos dados quando o id do evento é encontrado, antes de descartar o evento. Esta é uma configuração de segurança caso algo tenha dado errado.timeout: (opcional) timeout dos dados de evento únicos armazenados no shm (em segundos), padrão 2. Veja o parâmetrouniquedo método post.
O valor de retorno será true, ou nil e uma mensagem de erro.
Este método pode ser chamado repetidamente para atualizar as configurações, exceto pelo valor shm que não pode ser alterado após a configuração inicial.
NOTA: o wait_interval é executado usando a função ngx.sleep. Em contextos onde essa função não está disponível (por exemplo, init_worker), será executado um busy-wait para executar o atraso.
configured
syntax: is_already_configured = events.configured()
O módulo de eventos roda como um singleton por processo worker. A função configured permite verificar se já está em funcionamento.
Uma verificação antes de iniciar quaisquer dependências é recomendada;
local events = require "resty.worker.events"
local initialization_of_my_module = function()
assert(events.configured(), "Por favor, configure o módulo 'lua-resty-worker-events' "..
"antes de usar meu módulo")
-- faça a inicialização aqui
end
event_list
syntax: _M.events = events.event_list(sourcename, event1, event2, ...)
Função utilitária para gerar listas de eventos e prevenir erros de digitação em strings mágicas. Acessar um evento inexistente na tabela retornada resultará em um 'erro de evento desconhecido'.
O primeiro parâmetro sourcename é um nome único que identifica a fonte do evento, que estará disponível como campo _source. Todos os parâmetros seguintes são os eventos nomeados gerados pela fonte do evento.
Exemplo de uso;
local ev = require "resty.worker.events"
-- Exemplo de fonte de evento
local events = ev.event_list(
"my-module-event-source", -- disponível como _M.events._source
"started", -- disponível como _M.events.started
"event2" -- disponível como _M.events.event2
)
local raise_event = function(event, data)
return ev.post(events._source, event, data)
end
-- Postar meu próprio evento 'started'
raise_event(events.started, nil) -- nil para clareza, nenhum dado de evento é passado
-- defina minha tabela de módulo
local _M = {
events = events -- exportar tabela de eventos
-- implementação vai aqui
}
return _M
-- Exemplo de cliente de evento;
local mymod = require("some_module") -- módulo com uma tabela `events`
-- defina um callback e use a tabela de eventos do módulo fonte
local my_callback = function(data, event, source, pid)
if event == mymod.events.started then -- 'started' é o nome do evento
-- evento iniciado do módulo resty-worker-events
elseif event == mymod.events.stoppping then -- 'stopping' é o nome do evento
-- o acima lançará um erro devido ao erro de digitação em `stoppping`
end
end
ev.register(my_callback, mymod.events._source)
poll
syntax: success, err = events.poll()
Irá verificar novos eventos e tratá-los todos (chamar os callbacks registrados). A implementação é eficiente, ela apenas checa um único valor de memória compartilhada e retorna imediatamente se não houver novos eventos disponíveis.
O valor de retorno será "done" quando tiver tratado todos os eventos, "recursive" se já estiver em um loop de polling, ou nil + error se algo deu errado.
O resultado "recursive" simplesmente significa que o evento foi postado com sucesso, mas ainda não tratado, devido a outros eventos à sua frente que precisam ser tratados primeiro.
post
syntax: success, err = events.post(source, event, data, unique)
Irá postar um novo evento. source e event são ambos strings. data pode ser qualquer coisa (incluindo nil) desde que seja (de)serializável pelo módulo cjson.
Se o parâmetro unique for fornecido, então apenas um worker executará o evento, os outros workers o ignorarão. Além disso, quaisquer eventos subsequentes com o mesmo valor unique serão ignorados (pelo período de timeout especificado para configure).
O processo que executa o evento não será necessariamente o processo que postou o evento.
O valor de retorno será true quando o evento for postado com sucesso ou nil + error em caso de falha.
Nota: o processo worker que envia o evento também receberá o evento! Portanto, se a fonte do evento também agir sobre o evento, isso não deve ser feito a partir do código de postagem do evento, mas apenas ao recebê-lo.
post_local
syntax: success, err = events.post_local(source, event, data)
O mesmo que post, exceto que o evento será local ao processo worker, não será transmitido para outros workers. Com este método, o elemento data não será jsonificado.
O valor de retorno será true quando o evento for postado com sucesso ou nil + error em caso de falha.
register
syntax: events.register(callback, source, event1, event2, ...)
Irá registrar uma função de callback para receber eventos. Se source e event forem omitidos, então o callback será executado em todos os eventos; se source for fornecido, então apenas eventos com uma fonte correspondente serão passados. Se (um ou mais) nomes de eventos forem dados, então apenas quando ambos source e event coincidirem o callback será invocado.
O callback deve ter a seguinte assinatura;
syntax: callback = function(data, event, source, pid)
Os parâmetros serão os mesmos que os fornecidos para post, exceto pelo valor extra pid que será o pid do processo worker de origem, ou nil se foi um evento local apenas. Qualquer valor de retorno do callback será descartado.
Nota: data pode ser um tipo de referência de dados (por exemplo, um tipo de tabela Lua). O mesmo valor é passado para todos os callbacks, então não altere o valor no seu manipulador, a menos que você saiba o que está fazendo!
O valor de retorno de register será true, ou lançará um erro se callback não for um valor de função.
AVISO: manipuladores de eventos devem retornar rapidamente. Se um manipulador levar mais tempo do que o valor de timeout configurado, os eventos serão descartados!
Nota: para receber o próprio evento started do processo, o manipulador deve ser registrado antes de chamar configure.
register_weak
syntax: events.register_weak(callback, source, event1, event2, ...)
Esta função é idêntica a register, com a exceção de que o módulo manterá apenas referências fracas para a função callback.
unregister
syntax: events.unregister(callback, source, event1, event2, ...)
Irá desregistrar a função de callback e impedir que ela receba mais eventos. Os parâmetros funcionam exatamente da mesma forma que com register.
O valor de retorno será true se foi removido, false se não estava na lista de manipuladores, ou lançará um erro se callback não for um valor de função.
Histórico
Lançando novas versões
- certifique-se de que o changelog abaixo está atualizado
- atualize o número da versão no código
- crie um novo rockspec em
./rockspecs - faça commit com a mensagem
release x.x.x - marque o commit como
x.x.x - envie o commit e as tags
- faça upload para luarocks
2.0.1, 28-Junho-2021
- correção: possível deadlock na 'fase de inicialização'
2.0.0, 16-Setembro-2020
- QUEBRA: a função
postnão chama maispoll, tornando todos os eventos assíncronos. Quando um tratamento imediato para um evento é necessário, uma chamada explícita parapolldeve ser feita. - QUEBRA: a função
post_localnão executa mais imediatamente o evento, tornando todos os eventos locais assíncronos. Quando um tratamento imediato para um evento é necessário, uma chamada explícita parapolldeve ser feita. - correção: prevenir uso de 100% da CPU quando durante um recarregamento o shm de eventos é limpo
- correção: melhorou o registro em caso de falha ao escrever no shm (adiciona tamanho da carga para fins de solução de problemas)
- correção: não registrar mais a carga, pois pode expor dados sensíveis através dos logs
- alteração: atualizou o padrão de
shm_retriespara 999 - alteração: mudou o loop do timer para um loop de sleep (performance)
- correção: ao reconfigurar, certifique-se de que a tabela de callbacks está inicializada
1.1.0, 23-Dezembro-2020 (lançamento de manutenção)
- recurso: o loop de polling agora roda para sempre, dormindo por 0.5 segundos entre execuções, evitando criar novos timers a cada passo.
1.0.0, 18-Julho-2019
- QUEBRA: os valores de retorno de
poll(e, portanto, tambémpostepost_local) mudaram para serem mais "lua-ish", para serem verdadeiros quando tudo está bem. - recurso: nova opção
shm_retriespara corrigir erros de "no memory" causados por fragmentação de memória no shm ao postar eventos. - correção: corrigidos dois erros de digitação em nomes de variáveis (casos extremos)
0.3.3, 8-Maio-2018
- correção: timeouts nas fases de inicialização, removendo a configuração de timeout, veja a issue #9
0.3.2, 11-Abril-2018
- alteração: adicionar um stacktrace aos erros de manipulador
- correção: falha do manipulador de erro se o valor não era serializável, veja a issue #5
- correção: corrigir um teste para os manipuladores fracos
Veja Também
- OpenResty: http://openresty.org
GitHub
Você pode encontrar dicas adicionais de configuração e documentação para este módulo no repositório do GitHub para nginx-module-worker-events.