Why Does lua-resty-core Perform Better?
API7.ai
September 30, 2022
Como dissemos nas duas lições anteriores, Lua é uma linguagem de desenvolvimento embutida que mantém o núcleo curto e compacto. Você pode incorporar Lua no Redis e no NGINX para ajudá-lo a implementar lógicas de negócios de forma flexível. Lua também permite que você chame funções e estruturas de dados C existentes para evitar reinventar a roda.
Em Lua, você pode usar a API C do Lua para chamar funções C, e no LuaJIT, você pode usar FFI. Para o OpenResty:
- No núcleo
lua-nginx-module
, a API para chamar funções C é feita usando a API C do Lua. - No
lua-resty-core
, algumas das APIs já presentes nolua-nginx-module
são implementadas usando o modelo FFI.
Você provavelmente está se perguntando por que precisamos implementar com FFI?
Não se preocupe. Vamos pegar ngx.base64_decode, uma API simples, como exemplo e ver como a API C do Lua difere da implementação FFI. Você também pode ter uma compreensão intuitiva de seu desempenho.
Lua CFunction
Vamos dar uma olhada em como isso é implementado no lua-nginx-module
usando a API C do Lua. Procuramos por decode_base64
no código do projeto e encontramos sua implementação em ngx_http_lua_string.c
.
lua_pushcfunction(L, ngx_http_lua_ngx_decode_base64);
lua_setfield(L, -2, "decode_base64");
O código acima é irritante de se olhar, mas, felizmente, não precisamos nos aprofundar nas duas funções que começam com lua_
e o papel específico de seus argumentos; só precisamos saber uma coisa - há uma CFunction registrada aqui: ngx_http_lua_ngx_decode_base64
, e ela corresponde ao ngx.base64_decode
, que é a API exposta ao público.
Vamos em frente e "seguir o mapa" e procurar por ngx_http_lua_ngx_decode_base64
neste arquivo C, que é definido no início do arquivo em:
static int ngx_http_lua_ngx_decode_base64(lua_State *L);
Para aquelas funções C que podem ser chamadas em Lua, sua interface deve seguir a forma exigida pelo Lua, que é typedef int (*lua_CFunction)(lua_State* L)
. Ela contém um ponteiro L do tipo lua_State
como argumento; seu tipo de retorno é um inteiro que indica o número de valores retornados, não o valor de retorno em si.
Ela é implementada da seguinte forma (aqui, removi o código de tratamento de erros).
static int
ngx_http_lua_ngx_decode_base64(lua_State *L)
{
ngx_str_t p, src;
src.data = (u_char *) luaL_checklstring(L, 1, &src.len);
p.len = ngx_base64_decoded_length(src.len);
p.data = lua_newuserdata(L, p.len);
if (ngx_decode_base64(&p, &src) == NGX_OK) {
lua_pushlstring(L, (char *) p.data, p.len);
} else {
lua_pushnil(L);
}
return 1;
}
O principal neste código são ngx_base64_decoded_length
e ngx_decode_base64
, ambas funções C fornecidas pelo NGINX.
Sabemos que funções escritas em C não podem passar o valor de retorno para o código Lua, mas precisam passar os parâmetros de chamada e retornar um valor entre Lua e C através da pilha. É por isso que há muito código que não entendemos de primeira vista. Além disso, esse código não pode ser rastreado pelo JIT, então, para o LuaJIT, essas operações estão em uma caixa preta e não podem ser otimizadas.
LuaJIT FFI
Diferente do FFI, a parte interativa do FFI é implementada em Lua, que pode ser rastreada pelo JIT e otimizada; claro, o código também é mais conciso e fácil de entender.
Vamos pegar o exemplo de base64_decode
, cuja implementação FFI está espalhada por dois repositórios: lua-resty-core
e lua-nginx-module
, e vamos olhar para o código implementado no primeiro.
ngx.decode_base64 = function (s)
local slen = #s
local dlen = base64_decoded_length(slen)
local dst = get_string_buf(dlen)
local pdlen = get_size_ptr()
local ok = C.ngx_http_lua_ffi_decode_base64(s, slen, dst, pdlen)
if ok == 0 then
return nil
end
return ffi_string(dst, pdlen[0])
end
Você vai perceber que, comparado ao CFunction, o código da implementação FFI é muito mais limpo, sua implementação específica é ngx_http_lua_ffi_decode_base64
no repositório lua-nginx-module
. Se você estiver interessado aqui, pode verificar o desempenho dessa função por conta própria. É bem direto, não vou postar o código aqui.
No entanto, se você for atento, percebeu algumas regras de nomenclatura de funções no trecho de código acima?
Sim, todas as funções no OpenResty têm convenções de nomenclatura, e você pode inferir seu uso pelo nome. Por exemplo:
ngx_http_lua_ffi_
, a função Lua que usa FFI para lidar com solicitações HTTP do NGINX.ngx_http_lua_ngx_
, uma função Lua que usa função C para lidar com solicitações HTTP do NGINX.- As outras funções que começam com ngx e lua são funções internas do NGINX e Lua, respectivamente.
Além disso, o código C no OpenResty tem uma especificação de código rigorosa, e recomendo a leitura do guia de estilo de código C oficial aqui. Este é um documento essencial para desenvolvedores que desejam aprender o código C do OpenResty e enviar PRs. Caso contrário, mesmo que seu PR esteja bem escrito, você será repetidamente solicitado a alterá-lo devido a problemas de estilo de código.
Para mais APIs e detalhes sobre FFI, recomendamos que você leia os tutoriais oficiais do LuaJIT e a documentação. Colunas técnicas não substituem a documentação oficial; só posso ajudá-lo a apontar o caminho de aprendizado em um tempo limitado, com menos desvios; problemas difíceis ainda precisam ser resolvidos por você.
LuaJIT FFI GC
Ao usar FFI, podemos ficar confusos: quem vai gerenciar a memória solicitada no FFI? Devemos liberá-la manualmente em C, ou o LuaJIT deve recuperá-la automaticamente?
Aqui está um princípio simples: o LuaJIT só é responsável pelos recursos alocados por ele mesmo; ffi.
Por exemplo, se você solicitar um bloco de memória usando ffi.C.malloc
, precisará liberá-lo com o ffi.C.free
correspondente. A documentação oficial do LuaJIT tem um exemplo equivalente.
local p = ffi.gc(ffi.C.malloc(n), ffi.C.free)
...
p = nil -- A última referência a p se foi.
-- O GC eventualmente executará o finalizador: ffi.C.free(p)
Neste código, o ffi.C.malloc(n)
solicita uma seção de memória, e ffi.gc
registra uma função de retorno de destruição ffi.C.free
, que será chamada automaticamente quando um cdata p
for coletado pelo GC do LuaJIT para liberar a memória no nível C. E o cdata é coletado pelo GC do LuaJIT. O LuaJIT liberará automaticamente p
no código acima.
Observe que, se você quiser solicitar um grande bloco de memória no OpenResty, recomendo usar ffi.C.malloc em vez de ffi.new. As razões também são claras.
ffi.new
retornacdata
, que faz parte da memória gerenciada pelo LuaJIT.- O GC do LuaJIT tem um limite superior de gerenciamento de memória, e o LuaJIT no OpenResty não tem a opção GC64 habilitada. Portanto, o limite superior de memória para um único worker é de apenas 2G. Uma vez que o limite superior de gerenciamento de memória do LuaJIT é excedido, causará um erro.
Ao usar FFI, também precisamos prestar atenção especial a vazamentos de memória. No entanto, todos cometem erros, e, desde que humanos escrevam o código, sempre haverá bugs.
É aqui que a robusta cadeia de ferramentas de teste e depuração do OpenResty se torna útil.
Vamos falar sobre testes primeiro. No sistema OpenResty, usamos Valgrind para detectar vazamentos de memória.
A estrutura de teste que mencionamos no curso anterior, test::nginx
, tem um modo especial de detecção de vazamento de memória para executar conjuntos de casos de teste unitários; você precisa definir a variável de ambiente TEST_NGINX_USE_VALGRIND=1.
O projeto oficial OpenResty será totalmente registrado neste modo antes de liberar a versão, e entraremos em mais detalhes na seção de testes mais tarde. Entraremos em mais detalhes na seção de testes mais tarde.
O CLI resty do OpenResty também tem a opção --valgrind
, que permite que você execute um código Lua sozinho, mesmo que você não tenha escrito um caso de teste.
Vamos olhar para as ferramentas de depuração.
O OpenResty fornece extensões baseadas em systemtap para realizar análise dinâmica ao vivo de programas OpenResty. Você pode procurar pela palavra-chave gc
no conjunto de ferramentas deste projeto e verá duas ferramentas, lj-gc
e lj-gc-objs
.
Para análise offline como core dump
, o OpenResty fornece um conjunto de ferramentas GDB, e você também pode procurar por gc
nele e encontrar as três ferramentas lgc
, lgcstat
e lgcpath
.
O uso específico dessas ferramentas de depuração será coberto em detalhes na seção de depuração mais tarde, para que você possa ter uma ideia agora. Afinal, o OpenResty tem um conjunto dedicado de ferramentas para ajudá-lo a localizar e resolver esses problemas.
lua-resty-core
A partir da comparação acima, podemos ver que a abordagem FFI não só é mais limpa em código, mas também pode ser otimizada pelo LuaJIT, o que é a melhor escolha. O OpenResty descontinuou a implementação CFunction, e o desempenho foi removido do código-fonte. As novas APIs agora são implementadas no repositório lua-resty-core
através do FFI.
Antes do lançamento do OpenResty 1.15.8.1 em maio de 2019, lua-resty-core
não era habilitado por padrão, o que resultava em perdas de desempenho e possíveis bugs, então recomendo fortemente que qualquer pessoa ainda usando a versão histórica habilite manualmente lua-resty-core
. Você só precisa adicionar uma linha de código na fase init_by_lua
.
require "resty.core"
Claro, a diretiva lua_load_resty_core foi adicionada no lançamento tardio de 1.15.8.1, e lua-resty-core
é habilitado por padrão.
Eu pessoalmente sinto que o OpenResty ainda é muito cauteloso em habilitar lua-resty-core
, e projetos de código aberto devem definir recursos semelhantes para serem habilitados por padrão o mais rápido possível.
lua-resty-core
não apenas reimplementa algumas das APIs do projeto lua-nginx-module
, como ngx.re.match
, ngx.md5
, etc., mas também implementa várias novas APIs, como ngx.ssl
, ngx.base64
, ngx.errlog
, ngx.process
, ngx.re.process
, e ngx.ngx.md5
. ngx.re.split
, ngx.resp.add_header
, ngx.balancer
, ngx.semaphore
, etc., que cobriremos mais tarde no capítulo de API do OpenResty.
Resumo
Tendo dito tudo isso, gostaria de concluir que o FFI, embora bom, não é uma bala de prata para desempenho. A principal razão pela qual ele é eficiente é que pode ser rastreado e otimizado pelo JIT. Se você escrever código Lua que não pode ser JIT e precisa ser executado no modo interpretado, então o FFI será menos eficiente.
Então, quais operações podem ser JIT e quais não podem? Como podemos evitar escrever código que não pode ser JIT? Vou revelar isso na próxima seção.
Finalmente, um problema de lição de casa prático: Você pode encontrar uma ou duas APIs tanto no lua-nginx-module
quanto no lua-resty-core
, e então comparar as diferenças em testes de desempenho? Você pode ver quão significativa é a melhoria de desempenho do FFI.
Sinta-se à vontade para deixar um comentário, e vou compartilhar seus pensamentos e ganhos, e convido você a compartilhar este artigo com seus colegas e amigos, juntos com a troca e o progresso.