O que torna o OpenResty tão especial
API7.ai
October 14, 2022
Em artigos anteriores, você aprendeu sobre os dois pilares do OpenResty: NGINX e LuaJIT, e tenho certeza de que você está pronto para começar a aprender sobre as APIs que o OpenResty fornece.
Mas não seja muito apressado. Antes de fazer isso, você precisa gastar um pouco mais de tempo se familiarizando com os princípios e conceitos básicos do OpenResty.
Princípios
Os processos Master
e Worker
do OpenResty contêm uma VM LuaJIT, que é compartilhada por todas as corrotinas dentro do mesmo processo, e na qual o código Lua é executado.
E em um determinado momento, cada processo Worker
só pode lidar com solicitações de um usuário, o que significa que apenas uma corrotina está em execução. Você pode ter uma dúvida: Como o NGINX pode suportar C10K (dezenas de milhares de concorrências), ele não precisa lidar com 10.000 solicitações simultaneamente?
Claro que não. O NGINX usa epoll
para direcionar eventos e reduzir a espera e ociosidade, de modo que o máximo de recursos de CPU possível possa ser usado para processar solicitações de usuários. Afinal, a alta performance só é alcançada quando as solicitações individuais são processadas rapidamente o suficiente. Se um modo multi-thread for usado, onde uma solicitação corresponde a uma thread, então com C10K, os recursos podem ser facilmente esgotados.
No nível do OpenResty, as corrotinas do Lua trabalham em conjunto com o mecanismo de eventos do NGINX. Se uma operação de I/O, como consultar um banco de dados MySQL, ocorrer no código Lua, ela primeiro chamará o yield
da corrotina Lua para se suspender e, em seguida, registrará um callback no NGINX; após a conclusão da operação de I/O (que também pode ser um timeout ou erro), o callback do NGINX resume
acordará a corrotina Lua. Isso completa a cooperação entre a concorrência do Lua e os drivers de eventos do NGINX, evitando a escrita de callbacks no código Lua.
Podemos observar o seguinte diagrama, que descreve todo o processo. Tanto lua_yield
quanto lua_resume
fazem parte da lua_CFunction
fornecida pelo Lua.
Por outro lado, se não houver operações de I/O ou sleep
no código Lua, como todas as operações intensivas de criptografia e descriptografia, então a VM LuaJIT será ocupada pela corrotina Lua até que toda a solicitação seja processada.
Forneci um trecho do código-fonte de ngx.sleep
abaixo para ajudá-lo a entender isso mais claramente. Este código está localizado em ngx_http_lua_sleep.c
, que você pode encontrar no diretório src
do projeto lua-nginx-module
.
Em ngx_http_lua_sleep.c
, podemos ver a implementação concreta da função sleep
. Você deve primeiro registrar a API Lua ngx.sleep
com a função C ngx_http_lua_ngx_sleep
.
void ngx_http_lua_inject_sleep_api(lua_State *L)
{
lua_pushcfunction(L, ngx_http_lua_ngx_sleep);
lua_setfield(L, -2, "sleep");
}
A seguir está a função principal de sleep
, e eu extraí apenas algumas linhas do código principal aqui.
static int ngx_http_lua_ngx_sleep(lua_State *L)
{
coctx->sleep.handler = ngx_http_lua_sleep_handler;
ngx_add_timer(&coctx->sleep, (ngx_msec_t) delay);
return lua_yield(L, 0);
}
Como você pode ver:
- Aqui a função de callback
ngx_http_lua_sleep_handler
é adicionada primeiro. - Em seguida, chama
ngx_add_timer
, uma interface fornecida pelo NGINX, para adicionar um timer ao loop de eventos do NGINX. - Finalmente, usa
lua_yield
para suspender a concorrência Lua, dando controle ao loop de eventos do NGINX.
A função de callback ngx_http_lua_sleep_handler
é acionada quando a operação de sleep é concluída. Ela chama ngx_http_lua_sleep_resume
e, eventualmente, acorda a corrotina Lua usando lua_resume
. Você pode recuperar os detalhes da chamada no código, então não vou entrar em detalhes aqui.
ngx.sleep
é apenas o exemplo mais simples, mas ao dissecá-lo, você pode ver os princípios básicos do módulo lua-nginx-module
.
Conceitos básicos
Depois de analisar os princípios, vamos refrescar nossa memória e relembrar os dois conceitos importantes de fases e não bloqueio no OpenResty.
O OpenResty, assim como o NGINX, tem o conceito de fases, e cada fase tem seu papel distinto:
set_by_lua
, que é usado para definir variáveis.rewrite_by_lua
, para redirecionamento, etc.access_by_lua
, para acesso, permissões, etc.content_by_lua
, para gerar conteúdo de retorno.header_filter_by_lua
, para processamento de filtro de cabeçalho de resposta.body_filter_by_lua
, para filtragem de corpo de resposta.log_by_lua
, para registro.
Claro, se a lógica do seu código não for muito complexa, é possível executá-la toda na fase rewrite
ou content
.
No entanto, observe que as APIs do OpenResty têm limites de uso de fase. Cada API tem uma lista de fases em que pode ser usada, e você receberá um erro se usá-la fora do escopo. Isso é muito diferente de outras linguagens de desenvolvimento.
Como exemplo, vou usar ngx.sleep
. Pela documentação, sei que ele só pode ser usado nos seguintes contextos e não inclui a fase log
.
context: rewrite_by_lua*, access_by_lua*, content_by_lua*, ngx.timer.*, ssl_certificate_by_lua*, ssl_session_fetch_by_lua*_
E se você não souber disso e usar sleep
em uma fase log
que ele não suporta:
location / {
log_by_lua_block {
ngx.sleep(1)
}
}
No log de erros do NGINX, há uma indicação de nível error
.
[error] 62666#0: *6 failed to run log_by_lua*: log_by_lua(nginx.conf:14):2: API disabled in the context of log_by_lua*
stack traceback:
[C]: in function 'sleep'
Portanto, antes de usar a API, sempre lembre-se de consultar a documentação para determinar se ela pode ser usada no contexto do seu código.
Depois de revisar o conceito de fases, vamos revisar o não bloqueio. Primeiro, vamos esclarecer que todas as APIs fornecidas pelo OpenResty são não bloqueantes.
Vou continuar com o requisito de sleep de 1 segundo como exemplo. Se você quiser implementá-lo em Lua, deve fazer isso.
function sleep(s)
local ntime = os.time() + s
repeat until os.time() > ntime
end
Como o Lua padrão não tem uma função sleep
, eu uso um loop aqui para continuar verificando se o tempo especificado foi atingido. Essa implementação é bloqueante, e durante o segundo em que sleep
está em execução, o Lua não faz nada enquanto outras solicitações que precisam ser processadas ficam esperando.
No entanto, se mudarmos para ngx.sleep(1)
, de acordo com o código-fonte que analisamos acima, o OpenResty ainda pode processar outras solicitações (como request B
) durante esse segundo. O contexto da solicitação atual (vamos chamá-la de request A
) será salvo e acordado pelo mecanismo de eventos do NGINX e, em seguida, voltará para request A
, de modo que a CPU esteja sempre em um estado de trabalho natural.
Variáveis e ciclo de vida
Além desses dois conceitos importantes, o ciclo de vida das variáveis também é uma área fácil de errar no desenvolvimento do OpenResty.
Como eu disse antes, no OpenResty, recomendo que você declare todas as variáveis como variáveis locais e use ferramentas como luacheck
e lua-releng
para detectar variáveis globais. O mesmo vale para módulos, como o seguinte.
local ngx_re = require "ngx.re"
No OpenResty, exceto para as duas fases init_by_lua
e init_worker_by_lua
, uma tabela isolada de variáveis globais é definida para todas as fases para evitar a contaminação de outras solicitações durante o processamento. Mesmo nessas duas fases onde você pode definir variáveis globais, você deve tentar evitar fazer isso.
Como regra, problemas que são tentados a serem resolvidos com variáveis globais devem ser melhor resolvidos com variáveis em módulos e serão muito mais claros. A seguir está um exemplo de uma variável em um módulo.
local _M = {}
_M.color = {
red = 1,
blue = 2,
green = 3
}
return _M
Eu defini um módulo em um arquivo chamado hello.lua
, que contém a tabela color
, e então adicionei a seguinte configuração ao nginx.conf
.
location / {
content_by_lua_block {
local hello = require "hello"
ngx.say(hello.color.green)
}
}
Essa configuração exigirá o módulo na fase content
e imprimirá o valor de green
como o corpo da resposta HTTP.
Você pode se perguntar por que a variável do módulo é tão incrível?
O módulo só será carregado uma vez no mesmo processo Worker
; depois disso, todas as solicitações tratadas pelo Worker
compartilharão os dados no módulo. Dizemos que dados "globais" são adequados para encapsular em módulos porque os Worker
s do OpenResty são completamente isolados uns dos outros, então cada Worker
carrega o módulo independentemente, e os dados do módulo não podem cruzar Worker
s.
Quanto ao tratamento dos dados que precisam ser compartilhados entre Worker
s, vou deixar isso para um capítulo posterior, então você não precisa se aprofundar nisso aqui.
No entanto, há uma coisa que pode dar errado aqui: ao acessar variáveis de módulo, é melhor mantê-las somente leitura e não tentar modificá-las, ou você terá uma race
no caso de alta concorrência, um bug que não pode ser detectado por testes unitários, que ocasionalmente ocorre online e é difícil de localizar.
Por exemplo, o valor atual da variável do módulo green
é 3
, e você faz uma operação de plus 1
no seu código, então o valor de green
agora é 4
? Não necessariamente; pode ser 4
, 5
ou 6
porque o OpenResty não bloqueia ao escrever em uma variável de módulo. Então há competição, e o valor da variável do módulo é atualizado por várias solicitações simultaneamente.
Tendo dito isso sobre variáveis globais, locais e de módulo, vamos discutir variáveis entre fases.
Há situações em que precisamos de variáveis que abrangem fases e podem ser lidas e escritas. Variáveis como $host
, $scheme
, etc., que são familiares para nós no NGINX, não podem ser criadas dinamicamente, mesmo que satisfaçam a condição de entre fases, e você tem que defini-las no arquivo de configuração antes de poder usá-las. Por exemplo, se você escrever algo como o seguinte.
location /foo {
set $my_var ; # precisa criar a variável $my_var primeiro
content_by_lua_block {
ngx.var.my_var = 123
}
}
O OpenResty fornece ngx.ctx
para resolver esse tipo de problema. É uma tabela Lua que pode ser usada para armazenar dados Lua baseados em solicitação com o mesmo tempo de vida da solicitação atual. Vamos ver este exemplo da documentação oficial.
location /test {
rewrite_by_lua_block {
ngx.ctx.foo = 76
}
access_by_lua_block {
ngx.ctx.foo = ngx.ctx.foo + 3
}
content_by_lua_block {
ngx.say(ngx.ctx.foo)
}
}
Você pode ver que definimos uma variável foo
que é armazenada em ngx.ctx
. Essa variável abrange as fases rewrite
, access
e content
e, finalmente, imprime o valor na fase content
, que é 79
, como esperávamos.
Claro, ngx.ctx
tem suas limitações.
Por exemplo, solicitações filhas criadas com ngx.location.capture
terão seus próprios dados ngx.ctx
separados, independentes do ngx.ctx
da solicitação pai.
Além disso, redirecionamentos internos criados com ngx.exec
destroem o ngx.ctx
da solicitação original e o regeneram com um ngx.ctx
em branco.
Ambas essas limitações têm exemplos de código detalhados na documentação oficial, então você pode verificá-las se estiver interessado.
Resumo
Finalmente, vou dizer mais algumas coisas. Estamos aprendendo os princípios do OpenResty e alguns conceitos importantes, mas você não precisa memorizá-los. Afinal, eles sempre fazem sentido e ganham vida quando combinados com requisitos e códigos do mundo real.
Eu me pergunto como você entende isso? Sinta-se à vontade para deixar um comentário e discutir comigo, e também convido você a compartilhar este artigo com seus colegas e amigos. Vamos nos comunicar juntos e progredir juntos.