Uma introdução ao NGINX

mauricioabreu

Maurício Antunes

Posted on May 21, 2022

Uma introdução ao NGINX

Volta e meia leio de algumas pessoas que NGINX (engine X) é complicado. Eu não vou mentir: não é algo simples de se entender e usar de primeira. Primeiramente, não é fácil entender qual a finalidade dele. É um servidor, mas o que ele faz? Como é usado?

NGINX é um servidor que pode ser usado de diversas maneiras, tais como: load balancer (balancear requests para os servidores de aplicação), proxy reverso (receber um request da internet e repassar para outro servidor ou serviço) e uma das capacidades mais fantásticas do NGINX, na minha opinião, é sua finalidade como cache.

Uma generosa parte da internet roda sobre NGINX e alguns segmentos bem importantes usam ele como principal servidor, como plataformas de streaming, aplicações de larga escala tais como lojas, redes sociais e, também, é usado como base de muitas CDNs.

Porque usar NGINX?

Talvez você já tenha se perguntado: porque eu deveria usar NGINX? Não é mais fácil só apontar o DNS para vários IPs?
Isso é possível, mas você não vai ter a robustez de apontar seu DNS para um load balancer que pode balancear seus requests de uma forma mais inteligente, com o apoio de healthcheck caso seus servidores de aplicação não estejam respondendo e de algoritmos de balanceamento, implementando o modelo correto de distribuição de requests.

Se você já compreende o básico e apenas quer olhar uma configuração escrita com containers rodando uma solução, o repositório de exemplo. Ele está dividido em tags mostrando a evolução das partes do texto.

Vamos criar nossa primeira configuração, bem básica, com apenas uma rota que sempre retorna 200:



worker_processes auto;

events {
  worker_connections 1024;
}

http {
  server {
    listen 80;

    location / {
      return 200;
    }
  }
}


Enter fullscreen mode Exit fullscreen mode

Vamos usar Docker com a ajuda do Docker Compose:



services:
  nginx:
    image: nginx:alpine
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf
    ports:
      - "8080:80"


Enter fullscreen mode Exit fullscreen mode

Testando nossa rota:



curl -v http://localhost:8080

< HTTP/1.1 200 OK
< Server: nginx/1.21.6
< Content-Type: text/plain
< Content-Length: 0
< Connection: keep-alive


Enter fullscreen mode Exit fullscreen mode

Nossa rota funcionou. Ela retorna 200 como esperado.

Vamos entender o que essa configuração faz, focando apenas no bloco http. O bloco http é chamado de contexto, assim como o events, e ambos está em um contexto chamado main

  • server - é um bloco designado a escrever as configurações de um servidor dentro da sua configuração. Você pode ter vários deles, cada um atendendo em uma porta diferente. Você pode expor um servidor para o mundo e ter outro interno, sem cache, por exemplo, ou até driblando a autenticação, por exemplo.
  • listen - aqui você define em qual porta seu servidor vai aceitar as conexões. Note que o docker-compose.yml exporta a 80 como 8080 para o host, por isso o request é feito na 8080 (porta 80 do container).
  • location - é a diretiva usada para definir as rotas. Elas são bem poderosas. Aceitam expressões regulares, é possível capturar as variáveis e usá-las na configuração. O sistema de location, também, conta com diferentes tipos de match.

    • Sem modificador, o match é feito pelo começo da URI
    • = é um match exato
    • ~ é um match com expressão regular

Esses são os modificadores que mais usei até hoje.

Nossa location faz match com qualquer URI que comece com /, ou seja, qualquer uma como /compras, /produtos/lista, etc.

Podemos adicionar uma nova location com match exato.



location = /teste {
  return 200 "teste route";
}

location / {
  return 200;
}


Enter fullscreen mode Exit fullscreen mode

Agora podemos notar que a rota /teste tem um body sendo respondido.



curl -v http://localhost:8080/teste

< HTTP/1.1 200 OK
< Server: nginx/1.21.6
< Content-Type: text/plain
< Content-Length: 11
< Connection: keep-alive
teste route


Enter fullscreen mode Exit fullscreen mode

Vimos um pouco do básico da configuração, agora é hora de fazermos algo mais avançado.

Usando como proxy reverso

Uma definição simplista de proxy reverso é um request que você faz em algum endereço interno, como por exemplo http://meusite.com.br/minhaloja/produtos/1 e não sabendo pra onde exatamente esse request vai ser processado. E, após processado, o servidor de proxy reverso te manda o conteúdo da resposta do servidor de aplicação.

uma imagem descrevendo um request na internet, passando por nginx como proxy reverso e indo pra aplicação final

Vamos criar uma aplicação de teste que responde na porta 8081, mas que vamos acessar através do nosso NGINX.

A aplicação vai ser em Python usando Flask.



from flask import Flask

app = Flask(__name__)

@app.route("/")
def index():
    return "<p>Index page!</p>"


Enter fullscreen mode Exit fullscreen mode

Modificamos nossa configuração do servidor para conter apenas a rota da aplicação. Agora com a adição da diretiva proxy_pass, que é usada para fazer do NGINX um servidor de proxy reverso.



server {
  listen 80;

  location / {
    proxy_pass http://app:8081;
  }
}


Enter fullscreen mode Exit fullscreen mode

Observe que usamos o nome do serviço descrito (app) no docker-compose para referenciar o nome que a aplicação atende.



curl -v http://localhost:8080
< HTTP/1.1 200 OK
< Server: nginx/1.21.6
< Content-Type: text/html; charset=utf-8
< Content-Length: 18
< Connection: keep-alive
<p>Index page!</p>


Enter fullscreen mode Exit fullscreen mode

Podemos ver que a resposta possui um header adicional (Content-Type), agora, além da resposta que veio da aplicação.

Como fazer proxy reverso pra múltiplos servidores?

A realidade de uma aplicação em produção, na grande maioria das vezes, é ter mais de um servidor pra servir ela. E isso se deve a inúmeros motivos. Servidores têm recursos finitos (placa de rede, disco, CPU) para atender muitas requisições. E não é apenas sobre isso. E se o servidor sofrer uma falha de hardware? Ou até alguma falha de rede? Inúmeros motivos podem fazer sua aplicação ficar sem um fallback pra esses casos.

É aí que entramos com a diretiva upstream do NGINX, usada para definir um conjunto de servidores usados para balancear sua aplicação.

Primeiramente, modificaremos nosso docker-compose para criar duas instâncias da aplicação. Para fins de exemplo, não usaremos a diretiva scale porque ela possui um balanceamento round robin, e queremos mostrar como usar a diretiva de upstream com múltiplos servidores de aplicação.



app_1:
  build: app
  ports:
    - "8081:8081"
  environment:
    - FLASK_RUN_PORT=8081
    - FLASK_RUN_HOST=0.0.0.0
app_2:
  build: app
  ports:
    - "8082:8081"
  environment:
    - FLASK_RUN_PORT=8081
    - FLASK_RUN_HOST=0.0.0.0


Enter fullscreen mode Exit fullscreen mode

Vamos modificar nossa configuração para criar um upstream e colocar os dois hostnames da nossa aplicação:



upstream app {
  server app_1:8081;
  server app_2:8081;
  keepalive 100;
}

server {
  listen 80;

  location / {
    proxy_pass http://app;
  }
}


Enter fullscreen mode Exit fullscreen mode

Definimos um upstream chamado app, e é esse nome que usamos na diretiva proxy_pass. Usar proxy_pass tem algumas vantagens em vez de usar o proxy_pass diretamente com um IP ou hostname:

  • Definir diferentes estratégias de balanceamento dos servidores de aplicação (algoritmos de balanceamento diferente com pesos diferentes para cada servidor);
  • Usar keepalive, um número inteiro do total de conexões TCP mantidas do NGINX com o servidor, evitando criar conexões TCP a todo request;
  • Resolução de DNS é feita quando o servidor sobe. Sem definir um upstream, um nome como google.com no proxy_pass vai gerar uma consulta de DNS por request. Em larga escala isso pode ser um gargalo (até para o DNS interno da sua empresa, caso exista).

A configuração que fizemos vai balancear os request de forma igual, 50% pra cada servidor. Olhando nos logs, podemos ver isso acontecendo na prática:



nginx-intro-nginx-1  | 192.168.144.1 - - [20/May/2022:16:33:48 +0000] "GET / HTTP/1.1" 200 18 "-" "curl/7.79.1"
nginx-intro-app_1-1  | 192.168.144.4 - - [20/May/2022 16:33:48] "GET / HTTP/1.0" 200 -
nginx-intro-nginx-1  | 192.168.144.1 - - [20/May/2022:16:33:50 +0000] "GET / HTTP/1.1" 200 18 "-" "curl/7.79.1"
nginx-intro-app_2-1  | 192.168.144.4 - - [20/May/2022 16:33:50] "GET / HTTP/1.0" 200 -
nginx-intro-nginx-1  | 192.168.144.1 - - [20/May/2022:16:33:53 +0000] "GET / HTTP/1.1" 200 18 "-" "curl/7.79.1"
nginx-intro-app_1-1  | 192.168.144.4 - - [20/May/2022 16:33:53] "GET / HTTP/1.0" 200 -
nginx-intro-nginx-1  | 192.168.144.1 - - [20/May/2022:16:33:55 +0000] "GET / HTTP/1.1" 200 18 "-" "curl/7.79.1"
nginx-intro-app_2-1  | 192.168.144.4 - - [20/May/2022 16:33:55] "GET / HTTP/1.0" 200 -


Enter fullscreen mode Exit fullscreen mode

Agora, vamos dar uma olhada em como funciona o sistema de cache do NGINX.

Utilizando o NGINX como um servidor de cache

Muita da informação que consumimos não precisa, necessariamente, estar 100% atualizada a todo o instante. Se temos um arquivo estático como um script, uma foto ou um vídeo em nosso site, não precisamos ir buscar ou gerar esse conteúdo a todo momento. Podemos nos utilizar do NGINX como um servidor de cache em alguns passos, fazendo cache de diversos requests da sua aplicação.

Primeiro, vamos adicionar uma rota que trabalha com um parâmetro de query:



@app.route("/hello")
def hello():
    name = request.args.get("name", "nobody")
    return f"<p>Hello, {name}</p>"


Enter fullscreen mode Exit fullscreen mode

Agora, vamos modificar o NGINX para ele fazer cache das respostas.



worker_processes auto;

events {
  worker_connections 1024;
}

http {
  proxy_cache_path /tmp/cache levels=1:2 keys_zone=app_cache:5m max_size=10m inactive=20m use_temp_path=off;

  upstream app {
    server app_1:8081;
    server app_2:8081;
    keepalive 100;
  }

  server {
    listen 80;

    location / {
      add_header X-Cache-Status $upstream_cache_status;
      proxy_cache app_cache;
      proxy_cache_valid 200 30s;
      proxy_cache_valid 404 1m;
      proxy_cache_key $host$uri$is_args$args;
      proxy_cache_lock on;
      proxy_pass http://app;
    }
  }
}


Enter fullscreen mode Exit fullscreen mode

O que significa cada diretiva de proxy adicionada?

  • proxy_cache_path - descreve onde vamos guardar o cache. Os arquivos de cache são guardados em arquivos.
  • add_header - adiciona um header X-Cache-Status na resposta para que possamos depurar quando aconteceu MISS, HIT, etc.
    • HIT - conteúdo veio diretamente do cache por estar válido, ainda.
    • MISS - conteúdo não foi achado no cache e precisou ser buscado na origem.
  • proxy_cache - zona do cache. A mesma zona pode ser usada em outros lugares da configuração.
  • proxy_cache_valid - define um tempo de validade do cache para um código HTTP específico. Nossa aplicação não define um header Cache-Control, então, configuramos ele direto no NGINX.
  • proxy_cache_key - parâmetros usados para montar a chave de cache. Essa montagem de valores é extremamente importante para construir um cache seguro e eficiente.
  • proxy_cache_lock - impede que mais de um request vá na origem para buscar o mesmo conteúdo. Uma diretiva importante para proteger a origem do conteúdo. As conexões esperam até que o cache seja populado.

Conferindo o resultado, podemos fazer uma requisição pra rota nova:



curl -v "http://localhost:8080/hello?name=bento"


Enter fullscreen mode Exit fullscreen mode


nginx-intro-nginx-1  | 192.168.176.1 - - [20/May/2022:22:04:51 +0000] "GET /hello?name=bento HTTP/1.1" 200 19 "-" "curl/7.79.1"
nginx-intro-app_1-1  | 192.168.176.4 - - [20/May/2022 22:04:51] "GET /hello?name=bento HTTP/1.0" 200 -
nginx-intro-nginx-1  | 192.168.176.1 - - [20/May/2022:22:04:55 +0000] "GET /hello?name=bento HTTP/1.1" 200 19 "-" "curl/7.79.1"
nginx-intro-nginx-1  | 192.168.176.1 - - [20/May/2022:22:04:57 +0000] "GET /hello?name=bento HTTP/1.1" 200 19 "-" "curl/7.79.1"
nginx-intro-nginx-1  | 192.168.176.1 - - [20/May/2022:22:04:58 +0000] "GET /hello?name=bento HTTP/1.1" 200 19 "-" "curl/7.79.1"
nginx-intro-nginx-1  | 192.168.176.1 - - [20/May/2022:22:04:59 +0000] "GET /hello?name=bento HTTP/1.1" 200 19 "-" "curl/7.79.1"
nginx-intro-nginx-1  | 192.168.176.1 - - [20/May/2022:22:06:05 +0000] "GET /hello?name=bento HTTP/1.1" 200 19 "-" "curl/7.79.1"
nginx-intro-app_2-1  | 192.168.176.4 - - [20/May/2022 22:06:05] "GET /hello?name=bento HTTP/1.0" 200 -


Enter fullscreen mode Exit fullscreen mode

Observe como o primeiro request bate no NGINX e na app. Os requests em sequência não vão até a app porque eles estão usando o cache que orientamos o servidor a fazer.

Nos últimos dois requests é possível ver que o request foi pra outra instância da app porque passou os 30 segundos de validade que configuramos, garantindo que o cache precisa ser revalidado e que o balanceamento continua funcionando.

Próximos passos

Ao mesmo tempo que isso é o básico de NGINX é, também, o suficiente para você desbravar mais utilidades pra ele. É possível adicionar muita lógica de aplicação no NGINX que pode ajudar você a construir aplicações mais focadas na regra de negócio em vez de focar em balanceamento, cache, autorização, etc.

NGINX possui suporte a embutir código dinâmicamente, sendo as principais Lua e Javascript.

💖 💪 🙅 🚩
mauricioabreu
Maurício Antunes

Posted on May 21, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

Uma introdução ao NGINX
nginx Uma introdução ao NGINX

May 21, 2022