Além do Web Server: Processos Privilegiados e Tarefas de Timer

API7.ai

November 3, 2022

OpenResty (NGINX + Lua)

No artigo anterior, apresentamos as APIs do OpenResty, shared dict e cosocket, que implementam funcionalidades no âmbito do NGINX e servidores web, fornecendo um servidor web programável de menor custo e mais fácil de manter.

No entanto, o OpenResty pode fazer mais do que isso. Vamos destacar alguns recursos do OpenResty que vão além do servidor web e apresentá-los hoje. Eles são tarefas de timer, processo privilegiado e ngx.pipe não bloqueante.

Tarefas de Timer

No OpenResty, às vezes precisamos executar tarefas específicas em segundo plano regularmente, como sincronizar dados, limpar logs, etc. Se você fosse projetar isso, como faria? A maneira mais fácil de pensar é fornecer uma interface API para o exterior para executar essas tarefas, depois usar o crontab do sistema para chamar curl em intervalos regulares para acessar essa interface e, assim, implementar essa necessidade de forma indireta.

No entanto, isso não apenas seria fragmentado, mas também traria maior complexidade para a operação e manutenção. Portanto, o OpenResty fornece ngx.timer para resolver esse tipo de necessidade. Você pode considerar ngx.timer como uma solicitação de cliente simulada pelo OpenResty para acionar a função de callback correspondente.

As tarefas de timer do OpenResty podem ser divididas nos dois tipos a seguir.

  • ngx.timer.at é usado para executar tarefas de timer únicas.
  • ngx.timer.every é usado para executar tarefas de timer de período fixo.

Lembra da pergunta instigante que deixei no final do último artigo? A pergunta era como quebrar a restrição de que cosocket não pode ser usado em init_worker_by_lua, e a resposta é ngx.timer.

O código a seguir inicia uma tarefa de timer com um atraso de 0. Ele inicia a função de callback handler, e nessa função, ele usa cosocket para acessar um site.

init_worker_by_lua_block {
    local function handler()
        local sock = ngx.socket.tcp()
        local ok, err = sock:connect(“api7.ai", 80)
    end

    local ok, err = ngx.timer.at(0, handler)
}

Dessa forma, contornamos a restrição de que cosocket não pode ser usado nessa fase.

Voltando à necessidade do usuário que mencionamos no início desta seção, ngx.timer.at não atende à necessidade de execução periódica; no exemplo de código acima, é uma tarefa única.

Então, como fazemos isso periodicamente? Parece que você tem duas opções com base na API ngx.timer.at.

  • Você pode implementar a tarefa periódica manualmente usando um loop infinito while true na função de callback que sleep por um tempo após executar a tarefa.
  • Você também pode criar um novo timer no final da função de callback.

No entanto, antes de fazer uma escolha, há uma coisa que precisamos esclarecer: o timer é essencialmente uma solicitação, embora a solicitação não tenha sido iniciada pelo cliente. Para a solicitação, ela deve sair após concluir sua tarefa e não pode ficar residente para sempre. Caso contrário, é fácil causar vários tipos de vazamento de recursos.

Portanto, a primeira solução de usar while true para implementar tarefas periódicas não é confiável. A segunda solução é viável, mas cria timers recursivamente, o que não é fácil de entender.

Então, existe uma solução melhor? A nova API ngx.timer.every do OpenResty foi projetada especificamente para resolver esse problema e é uma solução mais próxima do crontab.

A desvantagem é que você nunca tem a chance de cancelar uma tarefa de timer após iniciá-la. Afinal, ngx.timer.cancel ainda é uma função a ser implementada.

Nesse ponto, você enfrentará um problema: o timer está sendo executado em segundo plano e não pode ser cancelado; se houver muitos timers, é fácil esgotar os recursos do sistema.

Portanto, o OpenResty fornece duas diretivas, lua_max_pending_timers e lua_max_running_timers para limitá-los. A primeira representa o número máximo de timers aguardando execução, e a segunda representa o número máximo de timers em execução atualmente.

Você também pode usar a API Lua para obter os valores das tarefas de timer atualmente em espera e em execução, como mostrado nos dois exemplos a seguir.

content_by_lua_block {
    ngx.timer.at(3, function() end)
    ngx.say(ngx.timer.pending_count())
}

Este código imprimirá um 1, indicando que há uma tarefa agendada aguardando execução.

content_by_lua_block {
    ngx.timer.at(0.1, function() ngx.sleep(0.3) end)
    ngx.sleep(0.2)
    ngx.say(ngx.timer.running_count())
}

Este código imprimirá um 1, indicando que há uma tarefa agendada em execução.

Processo Privilegiado

A seguir, vamos ver o processo privilegiado. Como todos sabemos, o NGINX é dividido em processo Master e processos Worker, onde os processos worker lidam com as solicitações dos usuários. Podemos obter o tipo do processo através da API process.type fornecida em lua-resty-core. Por exemplo, você pode usar resty para executar a seguinte função.

$ resty -e 'local process = require "ngx.process"
ngx.say("process type:", process.type())'

Você verá que ele retorna um resultado single em vez de worker, o que significa que o resty inicia o NGINX com um processo Worker, não um processo Master. Isso é verdade. Na implementação do resty, você pode ver que o processo Master é desativado com uma linha como esta.

master_process off;

O OpenResty estende o NGINX adicionando um privileged agent. O processo privilegiado tem as seguintes características especiais.

  • Ele não monitora nenhuma porta, o que significa que não fornece serviços para o exterior.

  • Ele tem os mesmos privilégios que o processo Master, que geralmente são os privilégios do usuário root, permitindo que ele faça muitas tarefas que são impossíveis para o processo Worker.

  • O processo privilegiado só pode ser aberto no contexto init_by_lua.

  • Além disso, o processo privilegiado só faz sentido se for executado no contexto init_worker_by_lua, porque nenhuma solicitação é acionada, e eles não vão para os contextos content, access, etc.

Vejamos um exemplo de um processo privilegiado que é ativado.

init_by_lua_block {
    local process = require "ngx.process"

    local ok, err = process.enable_privileged_agent()
    if not ok then
        ngx.log(ngx.ERR, "enables privileged agent failed error:", err)
    end
}

Após abrir o processo privilegiado com este código e iniciar o serviço OpenResty, podemos ver que o processo privilegiado agora faz parte do processo NGINX.

nginx: master process
nginx: worker process
nginx: privileged agent process

No entanto, se os privilégios forem executados apenas uma vez durante a fase init_worker_by_lua, o que não é uma boa ideia, como devemos acionar o processo privilegiado?

Sim, a resposta está escondida no conhecimento que acabamos de ensinar. Como ele não escuta portas, ou seja, não pode ser acionado por solicitações de terminal, a única maneira de acioná-lo periodicamente é usar o ngx.timer que acabamos de introduzir:

init_worker_by_lua_block {
    local process = require "ngx.process"

    local function reload(premature)
        local f, err = io.open(ngx.config.prefix() .. "/logs/nginx.pid", "r")
        if not f then
            return
        end
        local pid = f:read()
        f:close()
        os.execute("kill -HUP " .. pid)
    end

    if process.type() == "privileged agent" then
         local ok, err = ngx.timer.every(5, reload)
        if not ok then
            ngx.log(ngx.ERR, err)
        end
    end
}

O código acima implementa a capacidade de enviar sinais HUP para o processo master a cada 5 segundos. Naturalmente, você pode construir sobre isso para fazer coisas mais emocionantes, como consultar o banco de dados para ver se há tarefas para o processo privilegiado e executá-las. Como o processo privilegiado tem privilégios de root, isso obviamente é um pouco de um programa "backdoor".

ngx.pipe Não Bloqueante

Finalmente, vejamos o ngx.pipe não bloqueante, que usa a biblioteca padrão do Lua para executar um comando externo que envia um sinal para o processo Master no exemplo de código que acabamos de descrever.

os.execute("kill -HUP " .. pid)

Naturalmente, essa operação será bloqueante. Então, existe uma maneira não bloqueante de chamar programas externos no OpenResty? Afinal, você sabe que se estiver usando o OpenResty como uma plataforma de desenvolvimento completa e não como um servidor web, isso é o que você precisa. Por esse motivo, a biblioteca lua-resty-shell foi criada, e usá-la para invocar a linha de comando é não bloqueante:

$ resty -e 'local shell = require "resty.shell"
local ok, stdout, stderr, reason, status =
    shell.run([[echo "hello, world"]])
    ngx.say(stdout)

Este código é uma maneira diferente de escrever hello world, chamando o comando echo do sistema para completar a saída. Da mesma forma, você pode usar resty.shell como uma alternativa à chamada os.execute em Lua.

Sabemos que a implementação subjacente do lua-resty-shell depende da API ngx.pipe em lua-resty-core, então este exemplo usa lua-resty-shell para imprimir hello world, usando ngx.pipe em vez disso, ficaria assim.

$ resty -e 'local ngx_pipe = require "ngx.pipe"
local proc = ngx_pipe.spawn({"echo", "hello world"})
local data, err = proc:stdout_read_line()
ngx.say(data)'

O acima é o código subjacente da implementação do lua-resty-shell. Você pode verificar a documentação e os casos de teste do ngx.pipe para obter mais informações sobre como usá-lo. Portanto, não vou me aprofundar aqui.

Resumo

É isso. Terminamos com o conteúdo principal de hoje. A partir dos recursos acima, podemos ver que o OpenResty também está tentando se aproximar da direção de uma plataforma universal enquanto faz um NGINX melhor, esperando que os desenvolvedores possam tentar unificar a pilha de tecnologia e usar o OpenResty para resolver suas necessidades de desenvolvimento. Isso é bastante amigável para operações e manutenção, pois os custos de manutenção são menores, desde que você implante um OpenResty nele.

Finalmente, vou deixar uma pergunta instigante. Como pode haver vários Workers do NGINX, o timer será executado uma vez para cada Worker, o que é inaceitável na maioria dos cenários. Como podemos garantir que o timer seja executado apenas uma vez?

Sinta-se à vontade para deixar um comentário com sua solução e compartilhe este artigo com seus colegas e amigos para que possamos nos comunicar e melhorar juntos.