Introdução de APIs comuns no OpenResty

API7.ai

November 4, 2022

OpenResty (NGINX + Lua)

Nos artigos anteriores, você se familiarizou com muitas APIs importantes do Lua no OpenResty. Hoje, vamos aprender sobre algumas outras APIs gerais, principalmente relacionadas a expressões regulares, tempo, processos, etc.

APIs relacionadas a Expressões Regulares

Vamos começar olhando para as expressões regulares mais comumente usadas e mais importantes. No OpenResty, devemos usar o conjunto de APIs fornecido por ngx.re.* para lidar com a lógica relacionada a expressões regulares, em vez de usar a correspondência de padrões do Lua. Isso não é apenas por questões de desempenho, mas também porque a regularidade do Lua é autossuficiente e não é uma especificação PCRE, o que seria irritante para a maioria dos desenvolvedores.

Nos artigos anteriores, você já conheceu algumas das APIs ngx.re.*, cuja documentação é muito detalhada. Portanto, não vou listá-las novamente. Aqui, vou apresentar separadamente as duas APIs a seguir.

ngx.re.split

A primeira é ngx.re.split. O corte de strings é uma função muito comum, e o OpenResty também fornece uma API correspondente, mas muitos desenvolvedores não conseguem encontrar essa função e acabam optando por implementá-la por conta própria.

Por quê? A API ngx.re.split não está no lua-nginx-module, mas no lua-resty-core; ela não está na documentação da página inicial do lua-resty-core, mas na documentação do diretório de terceiro nível lua-resty-core/lib/ngx/re.md. Como resultado, muitos desenvolvedores não têm conhecimento da existência dessa API.

Da mesma forma, APIs difíceis de descobrir incluem ngx_resp.add_header, enable_privileged_agent, etc., que mencionamos anteriormente. Então, como podemos resolver rapidamente esse problema? Além de ler a documentação da página inicial do lua-resty-core, você precisa ler a documentação *.md no diretório lua-resty-core/lib/ngx/.

lua_regex_match_limit

Em segundo lugar, quero apresentar lua_regex_match_limit. Não falamos sobre os comandos NGINX fornecidos pelo OpenResty antes porque, na maioria dos casos, os valores padrão são suficientes e não há necessidade de modificá-los em tempo de execução. A exceção a isso é o comando lua_regex_match_limit, que está relacionado a expressões regulares.

Sabemos que se usarmos um motor de expressões regulares que é implementado com base em NFA de retrocesso, então há o risco de Retrocesso Catastrófico, onde a expressão regular retrocede demais ao corresponder, causando 100% de uso da CPU e bloqueando os serviços.

Uma vez que ocorre um retrocesso catastrófico, precisamos usar gdb para analisar o dump ou usar systemtap para analisar o ambiente online para localizá-lo. Infelizmente, detectá-lo antecipadamente não é fácil porque apenas solicitações especiais o acionam. Isso permite que os atacantes se aproveitem disso, e ReDoS (RegEx Denial of Service) se refere a esse tipo de ataque.

Aqui, vou apresentar como usar a seguinte linha de código no OpenResty para evitar os problemas acima de forma simples e eficaz:

lua_regex_match_limit é usado para limitar o número de retrocessos pelo motor de expressões regulares PCRE. Dessa forma, mesmo que ocorra um retrocesso catastrófico, as consequências serão limitadas a um intervalo que não causará o uso total da CPU.

lua_regex_match_limit 100000;

APIs relacionadas a Tempo

A API de tempo mais comumente usada é ngx.now, que imprime o timestamp atual, como na seguinte linha de código:

resty -e 'ngx.say(ngx.now())'

Como você pode ver pelos resultados impressos, ngx.now inclui a parte fracionária, então é mais preciso. A API relacionada ngx.time retorna apenas a parte inteira do valor. As outras, ngx.localtime, ngx.utctime, ngx.cookie_time e ngx.http_time, são usadas principalmente para retornar e processar o tempo em diferentes formatos. Se você quiser usá-las, pode verificar a documentação, elas não são difíceis de entender, então não preciso falar sobre elas.

No entanto, vale a pena mencionar que essas APIs que retornam o tempo atual, se não forem acionadas por uma operação de IO de rede não bloqueante, sempre retornarão o valor em cache, em vez do tempo real atual como gostaríamos. Veja o seguinte código de exemplo:

$ resty -e 'ngx.say(ngx.now())
os.execute("sleep 1")
ngx.say(ngx.now())'

Entre as duas chamadas para ngx.now, usamos a função de bloqueio do Lua para dormir por 1 segundo, mas o timestamp retornado é o mesmo nas duas ocasiões, como mostrado pelos resultados impressos.

Então, e se substituirmos por uma função de sono não bloqueante? Por exemplo, o seguinte novo código:

$ resty -e 'ngx.say(ngx.now())
ngx.sleep(1)
ngx.say(ngx.now())'

Ele imprimirá um timestamp diferente. Isso nos leva a ngx.sleep, uma função de sono não bloqueante. Além de dormir por um tempo especificado, essa função tem outro propósito especial.

Por exemplo, se você tiver um código que está fazendo cálculos intensivos, o que leva muito tempo, as solicitações correspondentes a esse código continuarão ocupando recursos do worker e da CPU durante esse tempo, causando o enfileiramento de outras solicitações e a falta de resposta oportuna. Nesse momento, podemos intercalar ngx.sleep(0) para fazer com que esse código desista do controle, permitindo que outras solicitações também sejam processadas.

APIs de Worker e Processo

O OpenResty fornece as APIs ngx.worker.* e ngx.process.* para obter informações sobre workers e processos. A primeira se refere aos processos worker do Nginx, enquanto a segunda se refere a todos os processos do Nginx em geral, não apenas aos processos worker, mas também ao processo master, processo privilegiado, etc.

O problema dos valores true e null

Finalmente, vamos olhar para o problema dos valores true e null. No OpenResty, a determinação dos valores true e null tem sido um ponto muito problemático e confuso.

Vamos olhar para a definição de um valor true em Lua: exceto nil e false, todos são valores true.

Portanto, valores true também incluiriam 0, string vazia, table vazia, etc.

Vamos olhar para nil em Lua, que significa indefinido. Por exemplo, se você declarar uma variável, mas não a inicializar, seu valor é nil.

$ resty -e 'local a
ngx.say(type(a))'

E nil também é um tipo de dado em Lua. Tendo entendido esses dois pontos, vamos agora olhar para os outros problemas derivados dessas duas definições.

ngx.null

O primeiro problema é ngx.null. Como o nil do Lua não pode ser usado como o valor de uma table, o OpenResty introduz ngx.null como o valor null na tabela.

$ resty -e  'print(ngx.null)'
null
$ resty -e 'print(type(ngx.null))'
userdata

Como você pode ver pelos dois códigos acima, ngx.null é impresso como null, e seu tipo é userdata, então ele pode ser tratado como um valor false? Claro que não. O valor booleano de ngx.null é true.

$ resty -e 'if ngx.null then
ngx.say("true")
end'

Portanto, lembre-se de que apenas nil e false são valores false. Se você perder esse ponto, é fácil cair em armadilhas, por exemplo, ao usar lua-resty-redis e fazer o seguinte julgamento:

local res, err = red:get("dog")
if not res then
    res = res + "test"
end

Se o valor de retorno res for nil, a chamada da função falhou; se res for ngx.null, a chave dog não existe no redis, então o código falha se a chave dog não existir.

cdata:NULL

O segundo problema é cdata:NULL. Quando você chama uma função C através da interface FFI do LuaJIT, e a função retorna um ponteiro NULL, então você encontrará outro tipo de valor null, cdata:NULL.

$ resty -e 'local ffi = require "ffi"
local cdata_null = ffi.new("void*", nil)
if cdata_null then
    ngx.say("true")
end'

Como ngx.null, cdata:NULL também é true. Mas o que é mais intrigante é que o seguinte código, que imprime true, significa que cdata:NULL é equivalente a nil.

$ resty -e 'local ffi = require "ffi"
local cdata_null = ffi.new("void*", nil)
ngx.say(cdata_null == nil)'

Então, como devemos lidar com ngx.null e cdata:NULL? Não é uma boa solução deixar a camada de aplicação cuidar desses problemas. É melhor fazer um wrapper de segundo nível e não deixar que o chamador saiba desses detalhes.

É melhor fazer um wrapper de segundo nível e não deixar que o chamador saiba desses detalhes.

cjson.null

Finalmente, vamos olhar para os valores null que aparecem no cjson. A biblioteca cjson pega o NULL no json, decodifica-o em lightuserdata do Lua e usa cjson.null para representar.

$ resty -e 'local cjson = require "cjson"
local data = cjson.encode(nil)
local decode_null = cjson.decode(data)
ngx.say(decode_null == cjson.null)'

O nil do Lua se torna cjson.null após ser codificado e decodificado pelo JSON. Como você pode imaginar, ele é introduzido pela mesma razão que ngx.null, porque nil não pode ser usado como valor em uma table.

Até agora, você ficou confuso com tantos tipos de valores null no OpenResty? Não se preocupe. Leia esta parte algumas vezes e organize-a você mesmo, então você não ficará confuso. Claro, precisamos pensar mais no futuro sobre se funciona ao escrever algo como if not foo then.

Resumo

O artigo de hoje apresentou as APIs do Lua comumente usadas no OpenResty.

Finalmente, vou deixar uma pergunta: No exemplo ngx.now, por que o valor de ngx.now não é modificado quando não há operação de yield? Sinta-se à vontade para compartilhar sua opinião nos comentários, e também para compartilhar este artigo para que possamos nos comunicar e melhorar juntos.