DevOps

Load Balancing: Distribuindo Tráfego para Alta Disponibilidade

✍️ Taylson Martinez
10 min read
Load Balancing: Distribuindo Tráfego para Alta Disponibilidade

Guia abrangente sobre estratégias de load balancing, algoritmos e implementação. Aprenda como distribuir tráfego através de múltiplos servidores efetivamente.

🌐

Este artigo também está disponível em inglês

Ler em Inglês →

O Que é Load Balancing?

Load balancing distribui tráfego de rede recebido através de múltiplos servidores para garantir:

  • Nenhum servidor único fica sobrecarregado
  • Alta disponibilidade (se um servidor falha, outros assumem)
  • Melhor utilização de recursos
  • Tempos de resposta melhorados

Pense nisso como filas de checkout em um supermercado - ao invés de uma fila longa, você tem múltiplos caixas atendendo clientes em paralelo.

Algoritmos de Load Balancing

1. Round Robin

Distribui requisições sequencialmente através de servidores.

Requisição 1 → Servidor A
Requisição 2 → Servidor B
Requisição 3 → Servidor C
Requisição 4 → Servidor A (volta ao início)

Prós:

  • Simples de implementar
  • Distribuição justa
  • Funciona bem quando servidores têm capacidade similar

Contras:

  • Não considera carga do servidor
  • Não leva em conta diferenças de capacidade do servidor
class RoundRobinBalancer(private val servers: List<String>) {
    private var current = 0
    
    fun getNextServer(): String {
        val server = servers[current]
        current = (current + 1) % servers.size
        return server
    }
}

2. Weighted Round Robin

Como round robin, mas servidores com pesos maiores recebem mais requisições.

data class WeightedServer(val host: String, val weight: Int)

class WeightedRoundRobinBalancer(servers: List<WeightedServer>) {
    private val expandedServers = mutableListOf<String>()
    private var current = 0
    
    init {
        // Expandir baseado em pesos
        servers.forEach { server ->
            repeat(server.weight) {
                expandedServers.add(server.host)
            }
        }
    }
    
    fun getNextServer(): String {
        val server = expandedServers[current]
        current = (current + 1) % expandedServers.size
        return server
    }
}

// Uso
val balancer = WeightedRoundRobinBalancer(
    listOf(
        WeightedServer("servidor-potente", 5),
        WeightedServer("servidor-medio", 3),
        WeightedServer("servidor-pequeno", 1)
    )
)
// servidor-potente recebe 5x mais requisições que servidor-pequeno

3. Least Connections

Roteia para o servidor com menos conexões ativas.

data class ServerConnection(
    val host: String,
    var connections: Int = 0
)

class LeastConnectionsBalancer(servers: List<String>) {
    private val servers = servers.map { ServerConnection(it) }
    
    fun getNextServer(): String {
        // Encontrar servidor com mínimo de conexões
        val server = servers.minByOrNull { it.connections }
            ?: throw IllegalStateException("No servers available")
        
        server.connections++
        return server.host
    }
    
    fun releaseConnection(host: String) {
        servers.find { it.host == host }?.let {
            it.connections--
        }
    }
}

Melhor para: Conexões de longa duração (WebSockets, conexões de banco de dados)

4. Least Response Time

Roteia para o servidor com tempo de resposta mais rápido.

data class ServerMetrics(
    val host: String,
    var avgResponseTime: Double = 0.0,
    var requestCount: Int = 0
)

class LeastResponseTimeBalancer(servers: List<String>) {
    private val servers = servers.map { ServerMetrics(it) }
    
    fun getNextServer(): String {
        // Encontrar servidor com menor tempo médio de resposta
        val server = servers.minByOrNull { it.avgResponseTime }
            ?: throw IllegalStateException("No servers available")
        
        return server.host
    }
    
    fun recordResponse(host: String, responseTime: Double) {
        servers.find { it.host == host }?.let { server ->
            server.avgResponseTime = 
                (server.avgResponseTime * server.requestCount + responseTime) /
                (server.requestCount + 1)
            server.requestCount++
        }
    }
}

5. IP Hash / Consistent Hashing

Roteia baseado no IP do cliente, garantindo que o mesmo cliente sempre vai ao mesmo servidor.

class IPHashBalancer(private val servers: List<String>) {
    
    fun getServerForIP(clientIP: String): String {
        val hash = hashIP(clientIP)
        val index = hash % servers.size
        return servers[index]
    }
    
    private fun hashIP(ip: String): Int {
        var hash = 0
        ip.forEach { char ->
            hash = ((hash shl 5) - hash) + char.code
            hash = hash and hash // Converter para inteiro 32bit
        }
        return kotlin.math.abs(hash)
    }
}

Melhor para: Afinidade de sessão (sticky sessions)

6. Random

Seleciona servidor aleatoriamente. Simples mas surpreendentemente efetivo.

class RandomBalancer(private val servers: List<String>) {
    
    fun getNextServer(): String {
        val index = (Math.random() * servers.size).toInt()
        return servers[index]
    }
}

Tipos de Load Balancers

Layer 4 (Camada de Transporte) Load Balancing

Roteia baseado em endereço IP e porta TCP/UDP.

Características:

  • Rápido (apenas olha camada de rede)
  • Não pode inspecionar headers HTTP
  • Protocol agnostic
# Nginx TCP load balancing
stream {
  upstream backend {
    server backend1.example.com:3000;
    server backend2.example.com:3000;
    server backend3.example.com:3000;
  }
  
  server {
    listen 80;
    proxy_pass backend;
  }
}

Layer 7 (Camada de Aplicação) Load Balancing

Roteia baseado em headers HTTP, cookies, caminho da URL, etc.

Características:

  • Roteamento mais inteligente
  • Pode rotear baseado em conteúdo
  • Ligeiramente mais lento (precisa parsear HTTP)
# Nginx HTTP load balancing
http {
  upstream api_servers {
    server api1.example.com:3000;
    server api2.example.com:3000;
  }
  
  upstream static_servers {
    server static1.example.com:80;
    server static2.example.com:80;
  }
  
  server {
    listen 80;
    
    # Rotear requisições de API para servidores de API
    location /api {
      proxy_pass http://api_servers;
    }
    
    # Rotear conteúdo estático para servidores estáticos
    location /static {
      proxy_pass http://static_servers;
    }
  }
}

Health Checks

Essencial para alta disponibilidade - apenas rotear para servidores saudáveis.

data class HealthyServer(
    val host: String,
    var healthy: Boolean = true,
    var failedChecks: Int = 0
)

@Component
class LoadBalancerWithHealthCheck(
    servers: List<String>,
    private val restTemplate: RestTemplate
) {
    private val servers = servers.map { HealthyServer(it) }
    
    init {
        // Verificar saúde a cada 10 segundos
        Executors.newScheduledThreadPool(1).scheduleAtFixedRate(
            { checkHealth() },
            0,
            10,
            TimeUnit.SECONDS
        )
    }
    
    private fun checkHealth() {
        servers.forEach { server ->
            try {
                val response = restTemplate.getForEntity(
                    "http://${server.host}/health",
                    String::class.java
                )
                
                if (response.statusCode.is2xxSuccessful) {
                    server.healthy = true
                    server.failedChecks = 0
                } else {
                    handleFailedCheck(server)
                }
            } catch (e: Exception) {
                handleFailedCheck(server)
            }
        }
    }
    
    private fun handleFailedCheck(server: HealthyServer) {
        server.failedChecks++
        
        // Marcar como não saudável após 3 falhas consecutivas
        if (server.failedChecks >= 3) {
            server.healthy = false
            println("Servidor ${server.host} marcado como não saudável")
        }
    }
    
    fun getNextServer(): String {
        val healthyServers = servers.filter { it.healthy }
        
        if (healthyServers.isEmpty()) {
            throw IllegalStateException("Nenhum servidor saudável disponível")
        }
        
        // Usar round robin em servidores saudáveis
        return healthyServers.first().host
    }
}

Persistência de Sessão (Sticky Sessions)

Garantir que requisições do usuário vão ao mesmo servidor.

upstream backend {
  ip_hash;  # Abordagem simples
  server backend1.example.com;
  server backend2.example.com;
}

# Ou usar stickiness baseado em cookie
upstream backend {
  server backend1.example.com;
  server backend2.example.com;
  sticky cookie srv_id expires=1h;
}

Nível de Aplicação

Armazenar sessões em armazenamento compartilhado:

// Ao invés de memória do servidor
app.use(session({
  store: new RedisStore({ client: redisClient }),
  secret: 'seu-secret',
  resave: false,
  saveUninitialized: false
}));

Soluções de Load Balancer

Software Load Balancers

Nginx

upstream backend {
  least_conn;  # Algoritmo
  server backend1.example.com:3000 weight=5;
  server backend2.example.com:3000 weight=3;
  server backend3.example.com:3000 backup;
}

server {
  listen 80;
  location / {
    proxy_pass http://backend;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  }
}

HAProxy

frontend http_front
  bind *:80
  default_backend servers

backend servers
  balance leastconn
  option httpchk GET /health
  server server1 192.168.1.10:3000 check
  server server2 192.168.1.11:3000 check
  server server3 192.168.1.12:3000 check

Cloud Load Balancers

  • AWS ELB (Elastic Load Balancer)

    • ALB (Application Load Balancer) - Layer 7
    • NLB (Network Load Balancer) - Layer 4
  • Google Cloud Load Balancing

  • Azure Load Balancer

Boas Práticas

1. Use Health Checks

Sempre monitore saúde do servidor e remova servidores não saudáveis da rotação.

2. Habilite SSL Termination no Load Balancer

server {
  listen 443 ssl;
  ssl_certificate /path/to/cert.pem;
  ssl_certificate_key /path/to/key.pem;
  
  location / {
    # Encaminhar para backend como HTTP
    proxy_pass http://backend;
  }
}

3. Implemente Connection Draining

Finalize graciosamente requisições existentes antes de remover um servidor.

async function drainServer(server) {
  // Parar de enviar novas requisições
  server.accepting = false;
  
  // Aguardar conexões existentes finalizarem
  while (server.activeConnections > 0) {
    await sleep(1000);
  }
  
  // Agora é seguro remover
  removeServer(server);
}

4. Monitore Métricas Chave

  • Taxa de requisições por servidor
  • Tempos de resposta
  • Taxas de erro
  • Conexões ativas
  • Status de saúde do servidor

5. Planeje para Failover

Tenha servidores de backup prontos para lidar com tráfego se servidores primários falharem.

Padrões Comuns

Load Balancing Geográfico

Rotear usuários para datacenter mais próximo:

Usuários US East → servidores us-east-1
Usuários EU → servidores eu-west-1
Usuários Asia → servidores ap-southeast-1

Load Balancing de Microserviços

Cada serviço tem seu próprio load balancer:

API Gateway

┌─────────┬─────────┬─────────┐
User LB   Order LB  Product LB
    ↓         ↓         ↓
User Svc  Order Svc Product Svc

Auto-Scaling

Adicionar/remover servidores automaticamente baseado em carga:

if (averageCPU > 80) {
  scaleUp();
}

if (averageCPU < 20 && serverCount > minServers) {
  scaleDown();
}

Conclusão

Load balancing é essencial para construir sistemas escaláveis e altamente disponíveis. Principais lições:

  • Escolha o algoritmo certo para seu caso de uso
  • Sempre implemente health checks
  • Monitore métricas do seu load balancer
  • Planeje para falhas
  • Comece simples e adicione complexidade conforme necessário

Mais importante: teste sua configuração de load balancing sob condições realistas de tráfego antes da produção!

Feliz balanceamento! ⚖️