Hoje quero mostrar a vocês um pouco sobre o Proxy Reverso, e sua utilização na criação de um Load Balancer. Basicamente, um Proxy Reverso é um servidor que “senta” na frente do servidor da sua aplicação e ele é o único que é conhecido e acessado pelo mundo exterior.
Em um cenário real, quando o cliente faz uma requisição HTTP para sua API, essa requisição vai cair primeiro no servidor de Proxy Reverso, o qual, vai direcionar a requisição para respectiva aplicação. O cliente não sabe da existência do servidor da aplicação, justamente por ele se comunicar apenas com o servidor de Proxy Reverso. Essa estratégia, além de maior facilidade para escalonamento horizontal, traz consigo benefícios de segurança, justamente porque agora os servidores da sua aplicação (seja uma API ou uma simples página web), podem rodar em uma rede privada, e apenas o único servidor exposto para o mundo é o de Proxy Reverso.
Load Balancer
Load Balancing (ou balanceamento de cargas de tráfego, em tradução livre),
Conhecendo o YARP (Yet Another Reverse Proxy)
Existem várias maneiras de se criar um servidor de Proxy Reverso, uma das mais conhecidas é usando o NGINX, o qual com um simples nginx.conf já é capaz de subir um servidor desse tipo. Conteúdo, antes de te apresentar a implementação com o NGINX, é interessante que você, caso seja um desenvolvedor do ambiente .NET conheça o YARP.
Basicamente, o YARP é um Proxy Reverso extensível via C#, é uma biblioteca que possui finalidade principal de Proxy, que você como desenvolvedor pode personalizar para atender às necessidades específicas do seu app.
Para um exemplo, crie um novo projeto .net, pode ser uma Web API, isolada da sua aplicação atual. Agora, instale o seguinte pacote pelo Nuget:
Yarp.ReverseProxy
Pronto, já temos o Yarp instalado, basta configurá-lo. Vamos iniciar preparando o appsettings.json do nosso projeto, após o “AllowedHosts”: “*”, adicione o seguinte:
"ReverseProxy": {
"Routes": {
"meucluester-route": {
"ClusterId": "meucluester-cluster",
"Match": {
"Path": "{**catch-all}"
},
"Transforms":[
{"PathPattern": "{**catch-all}"}
]
}
},
"Clusters": {
"meucluester-cluster": {
"LoadBalancingPolicy": "RoundRobin",
"Destinations": {
"destionation1": {
"Address": "http://localhost:5145"
},
"destionation2": {
"Address": "http://localhost:7145"
},
"destionation3": {
"Address": "http://localhost:4145"
}
}
}
}
}
A configuração do proxy reverso é feita nesta seção e está dividida em duas partes: Routes e Clusters.
Routes:
- Define as rotas que o proxy deve interceptar e redirecionar
- “meucluester-route”: Nome da rota
- “ClusterId”: “meucluester-cluster”: Essa rota será associada ao cluster chamado “meucluester-cluster”, que será configurado mais abaixo
- “Match”: Configura um padrão de correspondência para o caminho da URL
“Path”: “{catch-all}” O proxy redirecionará qualquer requisição usando {catch-all} para capturar tudo.
- “Transforms” Aplica as transformações no caminho da URL
- {“PathPattern”: “{**catch-all}”} Transforma o caminho da URL
Clusters
Antes de darmos seguimento a explicação, acho válido, caso você não saiba, apresentar o conceito de Cluster, um termo muito pregado na nossa área, mas que nem todos conhecem sua definição. Vou ser breve, fique tranquilo.
No contexto de tecnologia da informação e infraestrutura de TI, um cluster é um conjunto de máquinas ou servidores (físicos ou virtuais) que trabalham em conjunto para fornecer um serviço ou realizar tarefas de maneira mais eficiente, escalável e redundante. O objetivo do cluster é aumentar a disponibilidade, a capacidade de processamento e a tolerância a falhas.
RoundRobin
Na linha “LoadBalancingPolicy”: “RoundRobin” do nosso código, definimos a política de load balance da nossa aplicação como RoundRobin, mas o que seria isso?
Caso você não conheça, RoundRobin é um algoritmo/técnica de balanceamento de carga amplamente utilizado para distribuir requisições ou tarefas entre um conjunto de servidores ou destino de forma equilibrada. A ideia principal é distribuir o tráfego de maneira circular, de modo que cada destino (servidor no nosso caso) receba uma quantidade mais ou menos igual de requisições, garantindo que nenhum servidor seja sobrecarregado em relação aos outros.
Provavelmente, caso você já tenha tido a matéria de “Sistemas Operacionais” na sua universidade, deve ter se deparado com esse algoritmo, principalmente na parte de algoritmos de alocação e gerenciamento de memória. Vou dar seguimento com a explicação tentando trazer mais para o nosso cenário de distribuição de carga, mas saiba que segue a mesma ideia.
Então, como o RoundRobin funciona? Basicamente, ele mantém uma lista de servidores (ou destinos), e cada vez que uma nova requisição chega, ela é enviada para o próximo servidor da lista. Depois de enviar para o último servidor, o algoritmo retorna ao primeiro servidor e começa novamente.
Exemplo simples, suponha que você tenha 3 servidores disponíveis “A,B e C”. A primeira requisição vai para A, a segunda vai para B, e a terceira vai para C, a quarta vai para A novamente, e assim por diante.
Apesar da simplicidade, por ser um algoritmo de fácil compreensão, RoundRobin traz consigo algumas limitações, as quais não o tornam uma má escolha, apenas levantamentos que você deve conhecer na hora de escolher um algoritmo para sua aplicação, são eles:
- Servidores Desbalanceados: Se os servidores têm capacidades diferentes (por exemplo, um servidor é mais rápido ou tem mais recursos), o RoundRobin pode não ser a melhor escolha, pois ele não leva em consideração o desempenho de cada servidor. Todos os destinos recebem o mesmo número de requisições, independentemente de sua capacidade de processamento.
- Falhas de Servidor: Se um servidor falhar ou se tornar indisponível, o RoundRobin ainda tentará enviar tráfego para ele até que a configuração seja atualizada. Para isso, outras técnicas, como monitoramento da saúde do servidor (health checks), são necessárias para garantir que apenas servidores saudáveis recebam tráfego.
O Program.cs
Agora que você já compreendeu e configurou o “appsettings.json”, vamos dar seguimento no nosso projeto configurando o Program.cs. Basicamente, vamos registrar os serviços de Proxy Reverso no nosso builder a app:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
// REVERSE PROXY CONFIG
builder.Services.AddReverseProxy()
.LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));
builder.Services.AddHealthChecks();
// REVERSE PROXY END CONFIG
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
// REVERSE PROXY CONFIG
app.MapReverseProxy();
app.MapHealthChecks("health");
// REVERSE PROXY END CONFIG
app.Run();
Vou tentar ser um pouco mais breve aqui, tendo em vista que são apenas “ativadores”:
- builder.Services.AddReverseProxy()
- Este é o ponto crucial onde o YARP (Yet Another Reverse Proxy) é configurado.
- .LoadFromConfig(builder.Configuration.GetSection(“ReverseProxy”)):
- Carrega a configuração do proxy reverso do arquivo appsettings.json na seção “ReverseProxy”. Isso inclui as rotas e clusters de destinos, como discutido anteriormente.
- builder.Services.AddHealthChecks()
- Adiciona um serviço de monitoramento de saúde para verificar o estado da aplicação e dos endpoints de saúde, como app.MapHealthChecks(“health”).
- app.MapReverseProxy()
- Aqui, o YARP é integrado ao pipeline de requisições, configurando o proxy reverso para as rotas definidas no appsettings.json. Ele mapeia as requisições que correspondem às rotas configuradas para os destinos apropriados.
- app.MapHealthChecks(“health”)
- Define um endpoint /health para verificar a saúde da aplicação. Isso pode ser útil para monitoramento e operações de CI/CD, garantindo que a aplicação esteja em funcionamento.
Load Balance com NGINX
Vamos para a cereja do bolo, como configurar um load balance com NGINX. Agora que você já compreende melhor os conceitos do Load Balance, e do que é um Proxy Reverso, as configurações do NGINX ficarão fáceis de entender.
Para isso, vamos usar o docker junto do docker-compose para subir nossa servidor NGINX. Vamos iniciar criando um arquivos nginx.conf na raiz do nosso projeto, nele colocaremos o seguinte:
events { }
http {
upstream webapi_cluster {
server webapi:80;
server webapi2:80;
server webapi3:80;
}
server {
listen 80;
location / {
proxy_pass http://webapi_cluster; # Redireciona para o cluster
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection keep-alive;
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
}
}
Aqui vão algumas breve explicações caso você não conheça a estrutura de um arquivos de configuração do NGINX.
Pronto, agora basta rodar o seu projeto e seu proxy já estará no ar. Por fim, rode sua api .net e acesse o localhost do seu projeto, caso não tenha mudado nada na sua api, deixando apenas o template criado quando criamos um novo projeto com .net, acesse localhost:/weatherforecast e você já irá cair no proxy.
Bloco events
events { }
Este bloco é obrigatório no Nginx, mas neste caso não estamos configurando nenhum parâmetro específico. Ele gerencia eventos de conexão no Nginx, como conexões simultâneas ou manuseio de requisições concorrentes. Aqui, ele está vazio porque estamos focando no proxy reverso.
Bloco http
http {
upstream webapi_cluster {
server webapi:80;
server webapi2:80;
server webapi3:80;
}
server {
listen 80;
location / {
proxy_pass http://webapi_cluster; # Redireciona para o cluster
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection keep-alive;
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
}
}
Este é o principal bloco que configura o Nginx para funcionar como um proxy reverso e balanceador de carga.
O bloco http no Nginx é uma seção de configuração fundamental que define várias configurações relacionadas ao processamento de requisições HTTP. Ele é onde você pode configurar as funcionalidades de roteamento, proxy reverso, compressão de respostas, cabeçalhos HTTP, manipulação de erros e muito mais. Esse bloco envolve a maior parte da configuração relacionada ao tráfego da web em um servidor Nginx.
Abaixo, detalho as principais responsabilidades e elementos que podem ser configurados dentro do bloco http.
Bloco upstream
upstream webapi_cluster {
server webapi:80;
server webapi2:80;
server webapi3:80;
}
Função: O bloco upstream define um cluster de servidores que o Nginx usará para distribuir as requisições.
webapi, webapi2, webapi3: Esses nomes referenciam os serviços configurados no docker-compose.yml. Dentro da rede do Docker, o Nginx consegue resolver automaticamente esses nomes para os respectivos contêineres.
Porta 80: Cada servidor está ouvindo na porta 80 dentro da rede do Docker (a porta mapeada pelos contêineres).
Balanceamento de Carga: Por padrão, o Nginx usa o algoritmo Round Robin, que distribui as requisições igualmente entre os servidores listados no bloco upstream.
Por fim, basta configurar nosso docker-compose para rodar nosso servidor (não irei me extender sobre os detalhes do arquivo docker-compose.yaml, por não ser o foco desse artigo, mas sinta-se a vontade para pesquisar caso não conheça):
version: '3.8'
nginx:
image: nginx:latest
ports:
- "80:80"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
Pronto, agora basta rodar o seu container (docker-compose up –build) e seu proxy já estará no ar. Por fim, rode sua api .net e acesse o localhost do seu projeto, caso não tenha mudado nada na sua api, deixando apenas o template criado quando criamos um novo projeto com .net, acesse localhost/weatherforecast e você já irá cair no proxy.
YARP x NGINX
Em um cenário de aplicação real, a escolha entre o YARP (Yet Another Reverse Proxy) e o Nginx como balanceador de carga depende de vários fatores, incluindo desempenho, flexibilidade, facilidade de configuração, escalabilidade e necessidades específicas da sua aplicação. Ambos têm características e vantagens distintas, e a escolha entre eles pode impactar diretamente a performance e a gestão da infraestrutura.
Comparação entre YARP e Nginx para Balanceamento de Carga
- Desempenho
- Nginx:
- Alta Performance: O Nginx é amplamente reconhecido por sua performance extremamente alta em tarefas de proxy reverso e balanceamento de carga. Ele é escrito em C e é projetado para ser leve, rápido e capaz de gerenciar grandes volumes de tráfego de maneira eficiente.
- Desempenho Comprovado: O Nginx pode lidar com centenas de milhares de conexões simultâneas devido à sua arquitetura assíncrona e orientada a eventos.
Custo de Recurso Baixo: O Nginx usa uma abordagem baseada em eventos para gerenciar conexões, o que significa que ele pode processar muitas requisições simultâneas sem consumir muitos recursos do sistema.
- YARP:
- Desempenho Satisfatório para Aplicações .NET: O YARP é uma solução moderna projetada especificamente para integração com o ecossistema .NET. Seu desempenho é bom e adequado para a maioria das cargas de trabalho baseadas em .NET, mas, como é implementado em C# e roda na CLR (Common Language Runtime), seu desempenho pode não ser tão otimizado quanto o Nginx, especialmente em cenários de alta carga.
- Menor Eficiência em Conexões de Baixa Latência: Embora o YARP seja eficaz, ele pode não ser tão rápido quanto o Nginx em termos de latência e manejo de grandes volumes de tráfego de rede.
- Flexibilidade e Funcionalidades
- Nginx:
- Configuração Rica: O Nginx é altamente configurável e oferece uma grande variedade de funcionalidades de proxy reverso, incluindo cache, autenticação, manipulação de cabeçalhos, compressão de resposta, redirecionamento, SSL/TLS, e mais.
- Balanceamento Avançado: O Nginx oferece algoritmos de balanceamento de carga avançados como RoundRobin, Least Connections, IP Hash, e Random. Ele também pode realizar balanceamento de carga com base em métricas de saúde (health checks) para garantir que o tráfego seja direcionado apenas a servidores disponíveis.
- Escalabilidade: O Nginx pode facilmente escalar para um grande número de servidores e pode ser configurado para lidar com milhões de requisições simultâneas.
- YARP:
- Integração com .NET: O YARP foi projetado especificamente para funcionar bem com o ecossistema .NET e integra-se perfeitamente com aplicações ASP.NET Core. Ele oferece uma API de configuração declarativa, facilitando a configuração do balanceamento de carga, mas não oferece tantas funcionalidades avançadas como o Nginx.
- Customização via C#: O YARP oferece flexibilidade para customização via código C#, o que é vantajoso para desenvolvedores .NET que desejam integrar o balanceador diretamente no código da aplicação. Porém, isso pode ser mais complexo e exigir mais tempo de desenvolvimento se comparado à configuração mais direta do Nginx.
- Transformações e Roteamento: O YARP permite manipular rotas e cabeçalhos de forma mais flexível através de transformações configuráveis (por exemplo, modificações de URL ou headers). Isso é útil em aplicações que precisam de regras complexas de roteamento.
- Facilidade de Implementação
- Nginx:
- Configuração Simples: Embora o Nginx tenha uma sintaxe de configuração única, ela é bastante poderosa e bem documentada. A configuração de balanceamento de carga e proxy reverso é relativamente simples, mas pode exigir algum conhecimento para configurar corretamente para cenários mais complexos (como SSL, autenticação ou regras avançadas de roteamento).
- Ambiente Independente: O Nginx não depende de um ecossistema específico (como .NET) e pode ser utilizado com qualquer tipo de aplicação.
- YARP:
- Integração Direta com .NET: O YARP se integra diretamente com o .NET, o que facilita a configuração e o uso em aplicações construídas no ecossistema .NET. No entanto, a configuração pode ser um pouco mais trabalhosa para aqueles que não estão familiarizados com o YARP e com o desenvolvimento em .NET.
- Gerenciamento via Código: O YARP permite que o balanceamento de carga e o roteamento sejam configurados e gerenciados dentro do próprio código da aplicação, o que é um benefício para quem já trabalha com o .NET, mas pode ser um desafio para quem não está acostumado a gerenciar balanceadores diretamente no código.
- Suporte a Escalabilidade e Alta Disponibilidade
- Nginx:
- Alta Escalabilidade: O Nginx é amplamente utilizado em cenários de alta escala devido à sua eficiência e baixo uso de recursos. Ele pode ser configurado para trabalhar em clusters de servidores e pode gerenciar balanceamento de carga entre servidores geograficamente distribuídos.
- Failover e Alta Disponibilidade: O Nginx suporta failover automático e pode ser configurado para garantir alta disponibilidade, redirecionando tráfego para servidores disponíveis em caso de falha.
- YARP:
- Escalabilidade com .NET: Embora o YARP ofereça boa escalabilidade, ele pode não ser tão eficiente quanto o Nginx em termos de manejo de grandes volumes de tráfego. A escalabilidade do YARP também depende do próprio ambiente .NET e do comportamento do servidor que o hospeda.
- Failover Simples: O YARP pode ser configurado para suportar failover e balanceamento de carga básico, mas não oferece tantos recursos avançados quanto o Nginx para garantir alta disponibilidade em larga escala.
Conclusão
Nginx é geralmente mais adequado para cenários de alto desempenho, alta escala e alta disponibilidade, especialmente em ambientes que não são específicos do .NET. Ele é um líder consolidado para balanceamento de carga e proxy reverso, sendo uma escolha robusta para a maioria das aplicações, independentemente da stack tecnológica utilizada.
YARP é uma excelente escolha para aplicações .NET que requerem integração direta com o framework ASP.NET Core e onde a facilidade de configuração e customização via código é um fator importante. Para cenários de menor escala ou aplicações que já estão fortemente integradas ao ecossistema .NET, o YARP pode ser uma solução conveniente e eficiente.
Em termos de performance pura e escala, o Nginx é provavelmente a escolha mais forte, especialmente em sistemas que precisam de balanceamento de carga para milhões de requisições por segundo. No entanto, se você já está operando dentro de um ambiente .NET e precisa de uma solução integrada e configurável diretamente via código, o YARP pode ser uma alternativa mais fácil e adequada.