Load balancing client-side e server-side

1. Fundamentos do Balanceamento de Carga

1.1. Conceitos essenciais

Load balancing é a técnica de distribuir requisições entre múltiplos servidores para otimizar o uso de recursos, maximizar throughput, reduzir latência e garantir alta disponibilidade. Em sistemas distribuídos, é um componente crítico para evitar que um único nó se torne gargalo ou ponto único de falha.

1.2. Diferenças fundamentais

  • Server-side: O balanceamento ocorre no servidor ou em um proxy intermediário. O cliente desconhece os backends individuais. Exemplos: Nginx, HAProxy, AWS ELB.
  • Client-side: O cliente possui conhecimento dos backends e decide para qual enviar a requisição. Comum em arquiteturas de microserviços com service discovery.

1.3. Algoritmos clássicos

  • Round-robin: Distribui requisições sequencialmente entre backends.
  • Least connections: Envia para o backend com menos conexões ativas.
  • Hash-based: Usa hash de IP, URL ou chave para garantir que a mesma requisição vá para o mesmo backend.

2. Load Balancing Server-Side com Golang

2.1. Proxy reverso com httputil.ReverseProxy

package main

import (
    "log"
    "net/http"
    "net/http/httputil"
    "net/url"
)

func main() {
    backendURL, _ := url.Parse("http://localhost:8081")
    proxy := httputil.NewSingleHostReverseProxy(backendURL)

    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        proxy.ServeHTTP(w, r)
    })

    log.Println("Servidor proxy rodando em :8080")
    log.Fatal(http.ListenAndServe(":8080", nil))
}

2.2. Round-robin ponderado com health checks

type Backend struct {
    URL    *url.URL
    Weight int
    Alive  bool
}

type LoadBalancer struct {
    backends []*Backend
    current  int
    mu       sync.Mutex
}

func (lb *LoadBalancer) Next() *Backend {
    lb.mu.Lock()
    defer lb.mu.Unlock()

    for i := 0; i < len(lb.backends); i++ {
        idx := lb.current % len(lb.backends)
        lb.current++
        if lb.backends[idx].Alive {
            return lb.backends[idx]
        }
    }
    return nil
}

func (lb *LoadBalancer) HealthCheck() {
    for _, b := range lb.backends {
        resp, err := http.Get(b.URL.String() + "/health")
        b.Alive = err == nil && resp.StatusCode == http.StatusOK
        if resp != nil {
            resp.Body.Close()
        }
    }
}

2.3. Integração com service discovery (Consul)

type ConsulWatcher struct {
    client  *api.Client
    backends []*Backend
}

func (cw *ConsulWatcher) Watch(serviceName string) {
    for {
        services, _, err := cw.client.Health().Service(serviceName, "", true, nil)
        if err == nil {
            cw.updateBackends(services)
        }
        time.Sleep(10 * time.Second)
    }
}

func (cw *ConsulWatcher) updateBackends(services []*api.ServiceEntry) {
    cw.backends = nil
    for _, s := range services {
        url, _ := url.Parse(fmt.Sprintf("http://%s:%d", s.Service.Address, s.Service.Port))
        cw.backends = append(cw.backends, &Backend{URL: url, Alive: true})
    }
}

3. Load Balancing Client-Side com Golang

3.1. Pool de conexões customizado

type ClientPool struct {
    backends []string
    mu       sync.Mutex
    client   *http.Client
}

func NewClientPool(backends []string) *ClientPool {
    return &ClientPool{
        backends: backends,
        client: &http.Client{
            Transport: &http.Transport{
                MaxIdleConns:        100,
                MaxIdleConnsPerHost: 100,
            },
            Timeout: 5 * time.Second,
        },
    }
}

func (cp *ClientPool) Do(req *http.Request) (*http.Response, error) {
    cp.mu.Lock()
    backend := cp.backends[rand.Intn(len(cp.backends))]
    cp.mu.Unlock()

    req.URL.Host = backend
    return cp.client.Do(req)
}

3.2. gRPC com balanceamento integrado

import (
    "google.golang.org/grpc"
    "google.golang.org/grpc/balancer/roundrobin"
    "google.golang.org/grpc/resolver"
)

func createGRPCClient() (*grpc.ClientConn, error) {
    resolver.SetDefaultScheme("dns")

    conn, err := grpc.Dial(
        "dns:///service-discovery:8500",
        grpc.WithInsecure(),
        grpc.WithDefaultServiceConfig(`{"loadBalancingConfig": [{"round_robin":{}}]}`),
    )
    if err != nil {
        return nil, err
    }
    return conn, nil
}

3.3. Estratégias de retry e fallback

type RetryClient struct {
    backends []string
    maxRetries int
}

func (rc *RetryClient) ExecuteWithRetry(req *http.Request) (*http.Response, error) {
    var lastErr error

    for i := 0; i < rc.maxRetries; i++ {
        backend := rc.backends[i%len(rc.backends)]
        req.URL.Host = backend

        resp, err := http.DefaultClient.Do(req)
        if err == nil && resp.StatusCode < 500 {
            return resp, nil
        }

        if resp != nil {
            resp.Body.Close()
        }
        lastErr = err

        time.Sleep(time.Duration(math.Pow(2, float64(i))) * 100 * time.Millisecond)
    }

    return nil, fmt.Errorf("all retries failed: %w", lastErr)
}

4. Algoritmos Avançados e Personalização

4.1. Least connections

type LeastConnections struct {
    backends []*BackendStats
    mu       sync.Mutex
}

type BackendStats struct {
    URL       string
    Connections int32
}

func (lc *LeastConnections) Next() string {
    lc.mu.Lock()
    defer lc.mu.Unlock()

    minConn := int32(math.MaxInt32)
    var selected string

    for _, b := range lc.backends {
        if b.Connections < minConn {
            minConn = b.Connections
            selected = b.URL
        }
    }
    return selected
}

4.2. Hash consistente

type ConsistentHash struct {
    ring       map[uint32]string
    sortedKeys []uint32
    replicas   int
}

func (ch *ConsistentHash) Add(server string) {
    for i := 0; i < ch.replicas; i++ {
        hash := fnv.New32a()
        hash.Write([]byte(fmt.Sprintf("%s-%d", server, i)))
        key := hash.Sum32()
        ch.ring[key] = server
        ch.sortedKeys = append(ch.sortedKeys, key)
    }
    sort.Slice(ch.sortedKeys, func(i, j int) bool {
        return ch.sortedKeys[i] < ch.sortedKeys[j]
    })
}

func (ch *ConsistentHash) Get(key string) string {
    hash := fnv.New32a()
    hash.Write([]byte(key))
    h := hash.Sum32()

    idx := sort.Search(len(ch.sortedKeys), func(i int) bool {
        return ch.sortedKeys[i] >= h
    })

    if idx == len(ch.sortedKeys) {
        idx = 0
    }

    return ch.ring[ch.sortedKeys[idx]]
}

4.3. Selector customizado para gRPC

type CustomBalancer struct {
    backends []resolver.Address
    mu       sync.Mutex
}

func (cb *CustomBalancer) Build(cc balancer.ClientConn, opts balancer.BuildOptions) balancer.Balancer {
    return &customPicker{
        cc: cc,
    }
}

type customPicker struct {
    cc      balancer.ClientConn
    current int
}

func (cp *customPicker) Pick(info balancer.PickInfo) (balancer.PickResult, error) {
    cp.current = (cp.current + 1) % len(cp.cc.GetState().Picker)
    // Implementação do picker personalizado
    return balancer.PickResult{}, nil
}

5. Health Checks e Circuit Breaker

5.1. Health checks ativos e passivos

type HealthChecker struct {
    backends map[string]*BackendHealth
    mu       sync.RWMutex
}

type BackendHealth struct {
    failures int
    lastCheck time.Time
}

func (hc *HealthChecker) PassiveCheck(backend string, success bool) {
    hc.mu.Lock()
    defer hc.mu.Unlock()

    if !success {
        hc.backends[backend].failures++
        if hc.backends[backend].failures > 5 {
            delete(hc.backends, backend) // Remove temporariamente
        }
    } else {
        hc.backends[backend].failures = 0
    }
}

5.2. Circuit breaker com gobreaker

import "github.com/sony/gobreaker"

func createCircuitBreaker() *gobreaker.CircuitBreaker {
    return gobreaker.NewCircuitBreaker(gobreaker.Settings{
        Name:        "backend-service",
        MaxRequests: 3,
        Interval:    60 * time.Second,
        Timeout:     10 * time.Second,
        ReadyToTrip: func(counts gobreaker.Counts) bool {
            failureRatio := float64(counts.TotalFailures) / float64(counts.Requests)
            return counts.Requests >= 3 && failureRatio >= 0.6
        },
    })
}

6. Observabilidade e Métricas

6.1. Métricas com Prometheus

var (
    requestDuration = prometheus.NewHistogramVec(
        prometheus.HistogramOpts{
            Name: "request_duration_seconds",
            Help: "Duration of requests by backend",
            Buckets: prometheus.DefBuckets,
        },
        []string{"backend"},
    )

    requestErrors = prometheus.NewCounterVec(
        prometheus.CounterOpts{
            Name: "request_errors_total",
            Help: "Total errors by backend",
        },
        []string{"backend", "error_type"},
    )
)

func init() {
    prometheus.MustRegister(requestDuration, requestErrors)
}

6.2. Tracing com OpenTelemetry

import "go.opentelemetry.io/otel"

func tracedHandler(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        ctx, span := otel.Tracer("load-balancer").Start(r.Context(), "handle-request")
        defer span.End()

        span.SetAttributes(attribute.String("backend", r.URL.Host))
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

7. Comparação Prática e Boas Práticas

7.1. Quando escolher cada abordagem

Cenário Client-Side Server-Side
Microserviços com service discovery
Gateways de API
Latência ultra-baixa
Segurança e abstração

7.2. Armadilhas comuns

  • Afinidade de sessão: Usar hash consistente para manter sessões no mesmo backend
  • Conexões ociosas: Configurar IdleConnTimeout no http.Transport
  • Timeouts: Sempre definir Timeout no cliente HTTP e ReadTimeout/WriteTimeout no servidor

7.3. Checklist para produção

// Configuração recomendada para produção
transport := &http.Transport{
    MaxIdleConns:        100,
    MaxIdleConnsPerHost: 10,
    IdleConnTimeout:     90 * time.Second,
    TLSHandshakeTimeout: 10 * time.Second,
    ExpectContinueTimeout: 1 * time.Second,
    DisableKeepAlives:   false,
}

client := &http.Client{
    Transport: transport,
    Timeout:   30 * time.Second,
    CheckRedirect: func(req *http.Request, via []*http.Request) error {
        return http.ErrUseLastResponse
    },
}

Referências