Implementando Load Shedding para Evitar a Falha Total
Você já viu este cenário: o sistema está sob alta carga, o banco de dados começa a responder 200ms mais devagar e, de repente, todos os microserviços travam. O Kubernetes reinicia os pods, mas eles voltam e caem imediatamente.
Isso é um Cascading Failure (Falha em Cascata). E a culpa, muitas vezes, é da nossa tentativa de "salvar" todas as requisições.
A intuição nos diz para enfileirar requisições quando o servidor está cheio. A engenharia de resiliência nos diz o oposto: jogue fora o excesso. Isso é Load Shedding (Descarte de Carga).
O Conceito: Load Shedding vs. Circuit Breaker
É vital não confundir os dois padrões:
Circuit Breaker: Protege o seu sistema de um dependente que falhou (ex: "O serviço de Pagamento caiu, pare de chamar ele").
Load Shedding: Protege o seu sistema de si mesmo ou de um cliente agressivo (ex: "Estou sem CPU, vou rejeitar essa requisição de relatório para salvar o checkout").
O Load Shedding opera no princípio de que é melhor servir 80% dos usuários com baixa latência do que 100% dos usuários com timeout ou erro 500.
Implementação em C# (.NET Core)
No ecossistema .NET, podemos implementar um Load Shedding inteligente via Middleware. Diferente de um Rate Limiter (que limita por usuário), o Load Shedder olha para a saúde global do servidor (ex: uso de memória, thread pool ou fila de processamento).
Abaixo, um exemplo de Middleware que rejeita requisições novas se a fila de processamento interno estiver cheia.
using System.Net;
public class LoadSheddingMiddleware
{
private readonly RequestDelegate _next;
// Define a capacidade máxima de concorrência "segura" para este serviço
private readonly int _maxConcurrentRequests = 100;
private static int _currentRequests = 0;
public LoadSheddingMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
// 1. Verificação Atômica de Capacidade
// Se já estamos no limite, NÃO enfileiramos. Rejeitamos na porta.
if (Interlocked.Increment(ref _currentRequests) > _maxConcurrentRequests)
{
// Rollback do incremento pois não vamos processar
Interlocked.Decrement(ref _currentRequests);
// 2. Load Shedding em ação
// Retornamos 503 (Service Unavailable) imediatamente.
// Isso libera a conexão TCP e a Thread quase instantaneamente.
context.Response.StatusCode = (int)HttpStatusCode.ServiceUnavailable;
context.Response.Headers["Retry-After"] = "5"; // Dica para o cliente (Backoff)
await context.Response.WriteAsync("System Overloaded. Load Shedding active.");
return;
}
try
{
await _next(context);
}
finally
{
// 3. Libera a capacidade ao terminar
Interlocked.Decrement(ref _currentRequests);
}
}
}
// No Program.cs
// app.UseMiddleware<LoadSheddingMiddleware>();
O que está acontecendo aqui?
Diferente de um SemaphoreSlim.WaitAsync(), que colocaria a requisição em espera (consumindo memória e socket), o if atômico decide em nanossegundos se a requisição deve morrer. Isso preserva a CPU para as 100 requisições que já estão sendo processadas.
Implementação em Rust (Tower)
Rust tem uma cultura fortíssima de resiliência. A biblioteca padrão para middlewares assíncronos, Tower, possui um conceito nativo de LoadShed.
Em Rust, o modelo é ainda mais elegante: se o serviço interno diz "não estou pronto" (Poll::Pending), a camada de Load Shedding transforma isso automaticamente em erro, sem nem tentar alocar o futuro da requisição.
Vamos usar o tower com ServiceBuilder.
use tower::{ServiceBuilder, ServiceExt, Service};
use tower::load_shed::LoadShedLayer;
use tower::limit::ConcurrencyLimitLayer;
use http::{Request, Response, StatusCode};
use hyper::Body;
use std::convert::Infallible;
async fn handle_request(_req: Request<Body>) -> Result<Response<Body>, Infallible> {
// Simula processamento pesado
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
Ok(Response::new(Body::from("Processado com sucesso!")))
}
#[tokio::main]
async fn main() {
// Construção da Stack de Middleware
let service = ServiceBuilder::new()
// 1. Define o limite físico. Se tiver 10 requisições voando,
// o serviço interno retorna "Pending" (Não estou pronto).
.layer(ConcurrencyLimitLayer::new(10))
// 2. O LoadShed detecta o "Pending". Em vez de esperar (buffer),
// ele retorna um erro IMEDIATAMENTE.
.layer(LoadShedLayer::new())
.service_fn(handle_request);
// Exemplo de como o "servidor" chamaria esse serviço
// (Simplificado para fins didáticos)
let mut protected_service = service;
// Simulando 20 requisições simultâneas
for i in 0..20 {
// Precisamos clonar para simular concorrência real no tokio::spawn
let mut svc = protected_service.clone();
tokio::spawn(async move {
// O `ready()` verifica se o serviço aguenta mais carga
// O LoadShedLayer fará essa chamada falhar rápido se estiver cheio.
match svc.ready().await {
Ok(_) => {
let _ = svc.call(Request::new(Body::empty())).await;
println!("Req {}: Sucesso", i);
}
Err(_) => {
// AQUI ocorre o Load Shedding
println!("Req {}: DROPADO (Load Shedding)", i);
}
}
});
}
}
A Diferença do Rust
No exemplo em Rust, a combinação de ConcurrencyLimitLayer + LoadShedLayer cria um comportamento perfeito de descarte. O ConcurrencyLimit controla o estado ("estou cheio"), e o LoadShed toma a ação ("então rejeite o resto"). Não há "if" manual; é composição de comportamento.
Conclusão
Implementar Load Shedding é uma decisão difícil. Significa aceitar, conscientemente, que você vai falhar requisições de usuários.
Porém, sob a ótica da Engenharia de Software e Liderança Técnica, é a decisão madura. Ao sacrificar o excedente, você garante a sobrevivência do núcleo do sistema. Sem isso, em um pico de tráfego, a taxa de sucesso cai para 0% para todos. Com Load Shedding, ela se mantém estável no limite da sua capacidade.