Pular para conteúdo

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;

  1. 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.
  2. transmitir um evento apenas para o worker local, veja post_local.
  3. 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 unique de 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:

  1. eventos que devem ser executados por apenas um único worker (veja o parâmetro unique do método post). Essas entradas recebem um ttl no shm e, portanto, expirarão.
  2. todos os outros eventos (exceto eventos locais que não usam o SHM). Nesses casos, não há ttl definido.

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 (veja configure).
  • erros de no memory não podem ser resolvidos aumentando o SHM. A única maneira de resolver isso é aumentando a opção shm_retries passada para configure (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 out ocorre 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 interval menor, para que os workers verifiquem (e lidem com) eventos com mais frequência (veja a opção interval passada para configure)
    • 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âmetro unique do 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 post não chama mais poll, tornando todos os eventos assíncronos. Quando um tratamento imediato para um evento é necessário, uma chamada explícita para poll deve ser feita.
  • QUEBRA: a função post_local nã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 para poll deve 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_retries para 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ém post e post_local) mudaram para serem mais "lua-ish", para serem verdadeiros quando tudo está bem.
  • recurso: nova opção shm_retries para 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

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.