Além do Web Server: Processos Privilegiados e Tarefas de Timer
API7.ai
November 3, 2022
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 quesleep
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árioroot
, permitindo que ele faça muitas tarefas que são impossíveis para o processoWorker
. -
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 contextoscontent
,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 Worker
s 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.