Conhecimento do NGINX utilizado no OpenResty

API7.ai

September 17, 2022

OpenResty (NGINX + Lua)

Através do post anterior, você tem um conhecimento geral sobre o OpenResty. Nos próximos artigos, vou levá-lo através dos dois pilares do OpenResty: NGINX e LuaJIT, e você poderá aprender melhor o OpenResty dominando esses fundamentos.

Hoje vou começar com o NGINX, e aqui vou apenas introduzir alguns conceitos básicos do NGINX que podem ser usados no OpenResty, que é apenas um pequeno subconjunto do NGINX.

Em relação à configuração, no desenvolvimento do OpenResty, precisamos prestar atenção aos seguintes pontos.

  • Configurar o nginx.conf o mínimo possível.
  • Evitar o uso de combinações de múltiplas diretivas como if, set, rewrite, etc.
  • Não usar configurações, variáveis e módulos do NGINX quando for possível resolver com código Lua.

Esses métodos maximizarão a legibilidade, a manutenção e a extensibilidade. A seguinte configuração do NGINX é um exemplo típico de má prática de usar configuração como código.

location ~ ^/mobile/(web/app.htm) {
    set $type $1;
    set $orig_args $args;
    if ( $http_user_Agent ~ "(iPhone|iPad|Android)" ) {
        rewrite  ^/mobile/(.*) http://touch.foo.com/mobile/$1 last;
    }
    proxy_pass http://foo.com/$type?$orig_args;
}

Isso é o que precisamos evitar ao desenvolver com OpenResty.

Configuração do NGINX

O NGINX controla seu comportamento através de arquivos de configuração, que podem ser considerados como uma DSL simples. O NGINX lê a configuração quando o processo é iniciado e a carrega na memória. Se você modificar o arquivo de configuração, precisará reiniciar ou recarregar o NGINX e esperar até que o NGINX leia o arquivo de configuração novamente para que a nova configuração entre em vigor. Apenas a versão comercial do NGINX oferece parte dessa capacidade dinâmica em tempo de execução, na forma de APIs.

Vamos começar com a seguinte configuração, que é muito simples.

worker_processes auto;

pid logs/nginx.pid;
error_log logs/error.log notice;

worker_rlimit_nofile 65535;

events {
    worker_connections 16384;
}

http {
    server {
        listen 80;
        listen 443 ssl;

        location / {
            proxy_pass https://foo.com;
        }
    }
}

stream {
    server {
        listen 53 udp;
    }
}

No entanto, mesmo essas configurações simples envolvem alguns conceitos fundamentais.

Primeiro, cada diretiva tem seu contexto, que é seu escopo no arquivo de configuração do NGINX.

O nível superior é o main, que contém algumas instruções que não têm relação com o negócio específico, como worker_processes, pid e error_log, que fazem parte do contexto main. Além disso, há uma relação hierárquica entre os contextos. Por exemplo, o contexto de location é server, o contexto de server é http, e o contexto de http é main.

As diretivas não podem ser executadas no contexto errado. O NGINX verificará se o nginx.conf é legal ao iniciar. Por exemplo, se mudarmos listen 80; do contexto server para o contexto main e iniciarmos o serviço NGINX, veremos um erro como este:

"listen" directive is not allowed here ......

Segundo, o NGINX pode lidar não apenas com solicitações HTTP e tráfego HTTPS, mas também com tráfego UDP e TCP. O L7 está no HTTP e o L4 está no Stream. No OpenResty, lua-nginx-module e stream-lua-nginx-module correspondem a esses dois, respectivamente.

Uma coisa a se notar aqui é que o OpenResty não suporta todos os recursos do NGINX, e você precisa verificar a versão do OpenResty. A versão do OpenResty é consistente com a do NGINX, facilitando a identificação.

As diretivas de configuração envolvidas no nginx.conf acima estão nos módulos principais do NGINX ngx_core_module, ngx_http_core_module, e ngx_stream_core_module, que você pode clicar para ver a documentação específica.

Modo MASTER-WORKER

Depois de entender o arquivo de configuração, vamos olhar para o modo de multiprocessos do NGINX (como mostrado na figura abaixo). Como você pode ver, quando o NGINX é iniciado, haverá um processo Master e vários processos Worker (ou apenas um processo Worker, dependendo de como você o configurou).

Modo Worker do NGINX

Primeiro, o processo Master, como o nome sugere, desempenha o papel de "gerente" e não é responsável por lidar com solicitações dos clientes. Ele gerencia o processo Worker, incluindo receber sinais do administrador e monitorar o status dos Workers. Quando um processo Worker sai de forma anormal, o processo Master reiniciará um novo processo Worker.

Os processos Worker são os "funcionários reais" que lidam com as solicitações dos clientes. Eles são bifurcados do processo Master e são independentes uns dos outros. Esse modelo de multiprocessos é muito mais avançado que o modelo de multithreads do Apache, sem bloqueios entre threads e fácil de depurar. Mesmo que um processo falhe e saia, geralmente não afeta o trabalho dos outros processos Worker.

O OpenResty adiciona um agente privilegiado único ao modelo Master-Worker do NGINX. Esse processo não escuta em nenhuma porta e tem os mesmos privilégios que o processo Master do NGINX, então ele pode fazer algumas tarefas que exigem altos privilégios, como algumas operações de escrita em arquivos de disco local.

Se o processo privilegiado trabalhar com o mecanismo de atualização quente binária do NGINX, o OpenResty pode implementar toda a atualização binária em tempo real sem depender de programas externos.

Reduzir a dependência de programas externos e tentar resolver problemas dentro do processo OpenResty facilita a implantação, reduz os custos de operação e manutenção e diminui a probabilidade de erros no programa. O processo privilegiado e o ngx.pipe no OpenResty são todos para esse propósito.

Fases de Execução

As fases de execução também são uma característica essencial do NGINX e estão intimamente relacionadas à implementação específica do OpenResty. O NGINX tem 11 fases de execução, que podemos ver no código-fonte de ngx_http_core_module.h:

typedef enum {
    NGX_HTTP_POST_READ_PHASE = 0,

    NGX_HTTP_SERVER_REWRITE_PHASE,

    NGX_HTTP_FIND_CONFIG_PHASE,
    NGX_HTTP_REWRITE_PHASE,
    NGX_HTTP_POST_REWRITE_PHASE,

    NGX_HTTP_PREACCESS_PHASE,

    NGX_HTTP_ACCESS_PHASE,
    NGX_HTTP_POST_ACCESS_PHASE,

    NGX_HTTP_PRECONTENT_PHASE,

    NGX_HTTP_CONTENT_PHASE,

    NGX_HTTP_LOG_PHASE
} ngx_http_phases;

Se você quiser saber mais sobre o papel dessas 11 fases, pode ler a documentação do NGINX, então não vou entrar em detalhes aqui.

Coincidentemente, o OpenResty também tem 11 diretivas *_by_lua relacionadas à fase do NGINX, como mostrado na figura abaixo (da documentação do lua-nginx-module).

Ordem das Diretivas do Módulo Lua NGINX

init_by_lua é executado apenas quando o processo Master é criado, e init_worker_by_lua é executado apenas quando cada processo Worker é criado. As outras diretivas *_by_lua são acionadas por solicitações de clientes e são executadas repetidamente.

Portanto, durante a fase init_by_lua, podemos pré-carregar módulos Lua e dados públicos somente leitura para aproveitar o recurso COW (copy on write) do sistema operacional e economizar memória.

A maioria das operações pode ser feita dentro de content_by_lua, mas eu recomendaria dividi-las de acordo com diferentes funções, como a seguir.

  • set_by_lua: definindo variáveis.
  • rewrite_by_lua: encaminhamento, redirecionamento, etc.
  • access_by_lua: acesso, permissões, etc.
  • content_by_lua: gerando conteúdo de retorno.
  • header_filter_by_lua: processamento de filtragem de cabeçalho de resposta.
  • body_filter_by_lua: processamento de filtragem de corpo de resposta.
  • log_by_lua: registro de logs.

Vou dar um exemplo para mostrar os benefícios de dividir dessa forma. Vamos supor que muitas APIs de texto simples sejam fornecidas externamente, e agora precisamos adicionar lógica personalizada de criptografia e descriptografia. Então, precisamos mudar o código de todas as APIs?

location /mixed {
    content_by_lua '...';
}

Claro que não. Usando o recurso de fase, podemos descriptografar na fase access e criptografar na fase body filter sem fazer nenhuma alteração no código na fase content original.

location /mixed {
    access_by_lua '...';
    content_by_lua '...';
    body_filter_by_lua '...';
}

Atualização Binária do NGINX em Tempo Real

Finalmente, vou explicar brevemente a atualização binária do NGINX em tempo real. Sabemos que, após modificar o arquivo de configuração do NGINX, você precisa recarregá-lo para que ele funcione. Mas quando o NGINX se atualiza, ele pode fazer isso em tempo real. Isso pode parecer colocar o carro na frente dos bois, mas é compreensível, dado que o NGINX começou com balanceamento de carga tradicional, proxy reverso e cache de arquivos.

A atualização quente é feita enviando os sinais USR2 e WINCH para o processo Master antigo. Para essas duas etapas, a primeira inicia o novo processo Master; a segunda desliga o processo Worker gradualmente.

Após essas duas etapas, o novo Master e o novo Worker são iniciados. Neste ponto, o Master antigo não sai. A razão para não sair é simples: se você precisar reverter, ainda pode enviar sinais HUP para o Master antigo. Claro, se você tiver certeza de que não precisa reverter, pode enviar um sinal KILL para o Master antigo para sair.

E é isso, a atualização binária do NGINX em tempo real está concluída.

Se você quiser saber informações mais detalhadas sobre isso, pode verificar a documentação oficial para continuar aprendendo.

Resumo

Em geral, o que você usa no OpenResty são os fundamentos do NGINX, principalmente relacionados à configuração, processos mestre-escravo, fases de execução, etc. As outras coisas que podem ser resolvidas com código Lua são resolvidas com código sempre que possível, em vez de usar módulos e configurações do NGINX, o que é uma mudança de pensamento ao aprender OpenResty.

Por fim, deixei uma pergunta aberta para você: o Nginx oficialmente suporta NJS, o que significa que você pode escrever JS para controlar parte da lógica do NGINX, semelhante ao OpenResty. O que você acha disso? Bem-vindo para compartilhar este artigo.