Circuit breaker com gobreaker ou hystrix-go

1. Introdução ao Padrão Circuit Breaker

Em sistemas distribuídos, falhas são inevitáveis. Quando um serviço downstream começa a falhar, o comportamento padrão de simplesmente repetir a chamada pode levar a um efeito cascata devastador, consumindo recursos preciosos e derrubando serviços upstream. O padrão Circuit Breaker surge como uma solução elegante para esse problema.

Inspirado nos disjuntores elétricos, o Circuit Breaker monitora chamadas a serviços externos e, quando detecta uma taxa de falhas acima do limite configurado, abre o circuito, impedindo novas tentativas por um período. Ele opera em três estados clássicos:

  • Fechado (Closed): Estado normal, todas as requisições passam. Falhas são contadas.
  • Aberto (Open): Requisições são rejeitadas imediatamente, geralmente com uma resposta de fallback. Após um timeout, transita para Semi-aberto.
  • Semi-aberto (Half-Open): Permite um número limitado de requisições de teste. Se bem-sucedidas, o circuito fecha; se falharem, volta a abrir.

Os benefícios são claros: resiliência a falhas em cascata, degradação graciosa (fallbacks significativos) e recuperação automática sem intervenção manual.

2. Visão Geral das Bibliotecas: gobreaker vs hystrix-go

No ecossistema Go, duas bibliotecas dominam a implementação desse padrão.

gobreaker é a escolha moderna. Leve, sem dependências externas, API limpa e ativamente mantida. Oferece controle granular sobre thresholds, timeouts e intervalos de recuperação. Ideal para projetos novos.

hystrix-go é uma porta do famoso Hystrix do Netflix. Embora tenha sido amplamente usada, está oficialmente descontinuada e arquivada. Sua API é mais verbosa, com conceitos como comandos e grupos, e exige configuração mais complexa.

Característica gobreaker hystrix-go
Manutenção Ativa Descontinuada
Dependências Nenhuma Várias
API Simples e funcional Verbosa, baseada em structs
Métricas nativas Callbacks Eventos e métricas próprias
Performance Leve (menos alocações) Mais pesada

Para projetos atuais, gobreaker é a recomendação clara.

3. Implementando Circuit Breaker com gobreaker

Vamos proteger uma chamada HTTP externa. Primeiro, instale a biblioteca:

go get github.com/sony/gobreaker

Agora, um exemplo completo:

package main

import (
    "errors"
    "fmt"
    "io/ioutil"
    "net/http"
    "time"

    "github.com/sony/gobreaker"
)

func main() {
    // Configuração do circuit breaker
    cb := gobreaker.NewCircuitBreaker(gobreaker.Settings{
        Name:        "HTTP-GET",
        MaxRequests: 3,                // Máximo de requisições no estado semi-aberto
        Interval:    60 * time.Second, // Intervalo para resetar contadores no estado fechado
        Timeout:     30 * time.Second, // Tempo para transitar de aberto para semi-aberto
        ReadyToTrip: func(counts gobreaker.Counts) bool {
            failureRatio := float64(counts.TotalFailures) / float64(counts.Requests)
            return counts.Requests >= 5 && failureRatio >= 0.6
        },
        OnStateChange: func(name string, from, to gobreaker.State) {
            fmt.Printf("Circuit breaker '%s' mudou de %s para %s\n", name, from, to)
        },
    })

    // Função protegida
    fetchData := func() (string, error) {
        resp, err := http.Get("https://httpbin.org/delay/2") // Serviço lento
        if err != nil {
            return "", err
        }
        defer resp.Body.Close()

        if resp.StatusCode >= 500 {
            return "", errors.New("erro do servidor")
        }

        body, _ := ioutil.ReadAll(resp.Body)
        return string(body), nil
    }

    // Chamada com circuit breaker e fallback
    for i := 0; i < 10; i++ {
        result, err := cb.Execute(func() (interface{}, error) {
            return fetchData()
        })
        if err != nil {
            if errors.Is(err, gobreaker.ErrOpenState) {
                fmt.Printf("Requisição %d: Circuito aberto! Usando fallback...\n", i+1)
                result = "Dados do cache local"
            } else {
                fmt.Printf("Requisição %d: Erro na chamada: %v\n", i+1, err)
                continue
            }
        }
        fmt.Printf("Requisição %d: Resultado: %s\n", i+1, result)
        time.Sleep(1 * time.Second)
    }
}

A configuração ReadyToTrip define quando o circuito abre: após 5 requisições com 60% de falhas. OnStateChange permite logar transições.

4. Implementando Circuit Breaker com hystrix-go

Embora descontinuada, ainda existem sistemas legados usando hystrix-go. Instalação:

go get github.com/afex/hystrix-go/hystrix

Exemplo com chamada a banco de dados:

package main

import (
    "database/sql"
    "fmt"
    "log"
    "time"

    "github.com/afex/hystrix-go/hystrix"
    _ "github.com/lib/pq"
)

func main() {
    // Configuração do comando
    hystrix.ConfigureCommand("db_query", hystrix.CommandConfig{
        Timeout:                5000, // Timeout em milissegundos
        MaxConcurrentRequests:  10,
        SleepWindow:            30000, // Timeout do estado aberto (ms)
        RequestVolumeThreshold: 5,     // Mínimo de requisições para avaliar
        ErrorPercentThreshold:  50,    // Percentual de erro para abrir
    })

    // Simula uma consulta ao banco
    queryDB := func() error {
        db, err := sql.Open("postgres", "user=test dbname=test sslmode=disable")
        if err != nil {
            return err
        }
        defer db.Close()

        _, err = db.Exec("SELECT pg_sleep(10)") // Consulta lenta
        return err
    }

    // Fallback
    fallback := func(err error) error {
        log.Printf("Fallback acionado devido a: %v", err)
        return nil
    }

    for i := 0; i < 10; i++ {
        err := hystrix.Do("db_query", func() error {
            return queryDB()
        }, fallback)

        if err != nil {
            fmt.Printf("Requisição %d: Erro final: %v\n", i+1, err)
        } else {
            fmt.Printf("Requisição %d: Sucesso\n", i+1)
        }
        time.Sleep(1 * time.Second)
    }
}

A API do hystrix-go exige configurar comandos globais e usar hystrix.Do ou hystrix.Go para chamadas assíncronas.

5. Integração com Métricas e Monitoramento

Com gobreaker, podemos exportar métricas para Prometheus:

package main

import (
    "github.com/prometheus/client_golang/prometheus"
    "github.com/prometheus/client_golang/prometheus/promhttp"
    "github.com/sony/gobreaker"
    "net/http"
)

var (
    circuitState = prometheus.NewGaugeVec(
        prometheus.GaugeOpts{
            Name: "circuit_breaker_state",
            Help: "Estado do circuit breaker (0=closed, 1=half-open, 2=open)",
        },
        []string{"name"},
    )
)

func registerMetrics(cb *gobreaker.CircuitBreaker) {
    go func() {
        for {
            state := cb.State()
            var stateValue float64
            switch state {
            case gobreaker.StateClosed:
                stateValue = 0
            case gobreaker.StateHalfOpen:
                stateValue = 1
            case gobreaker.StateOpen:
                stateValue = 2
            }
            circuitState.With(prometheus.Labels{"name": cb.Name()}).Set(stateValue)
            time.Sleep(1 * time.Second)
        }
    }()
}

func main() {
    prometheus.MustRegister(circuitState)

    cb := gobreaker.NewCircuitBreaker(gobreaker.Settings{
        Name: "api-externa",
        // ... outras configurações
    })
    registerMetrics(cb)

    http.Handle("/metrics", promhttp.Handler())
    http.ListenAndServe(":2112", nil)
}

Para logs de transição, o callback OnStateChange já cobre.

6. Testes e Simulação de Cenários

Testes unitários com gobreaker são diretos:

package main

import (
    "errors"
    "testing"
    "time"

    "github.com/sony/gobreaker"
)

func TestCircuitBreakerOpenState(t *testing.T) {
    cb := gobreaker.NewCircuitBreaker(gobreaker.Settings{
        Name:        "test",
        MaxRequests: 1,
        Timeout:     1 * time.Second,
        ReadyToTrip: func(counts gobreaker.Counts) bool {
            return counts.TotalFailures >= 3
        },
    })

    // Simula 3 falhas consecutivas
    for i := 0; i < 3; i++ {
        _, err := cb.Execute(func() (interface{}, error) {
            return nil, errors.New("falha simulada")
        })
        if err == nil {
            t.Error("Esperava erro")
        }
    }

    // Próxima chamada deve rejeitar (circuito aberto)
    _, err := cb.Execute(func() (interface{}, error) {
        return "ok", nil
    })
    if !errors.Is(err, gobreaker.ErrOpenState) {
        t.Error("Esperava circuito aberto")
    }
}

func TestCircuitBreakerHalfOpenRecovery(t *testing.T) {
    cb := gobreaker.NewCircuitBreaker(gobreaker.Settings{
        Name:        "test-recovery",
        MaxRequests: 2,
        Timeout:     100 * time.Millisecond,
        ReadyToTrip: func(counts gobreaker.Counts) bool {
            return counts.TotalFailures >= 2
        },
    })

    // Abre o circuito
    cb.Execute(func() (interface{}, error) {
        return nil, errors.New("falha")
    })
    cb.Execute(func() (interface{}, error) {
        return nil, errors.New("falha")
    })

    // Aguarda timeout para entrar em semi-aberto
    time.Sleep(150 * time.Millisecond)

    // Deve permitir requisições de teste
    result, err := cb.Execute(func() (interface{}, error) {
        return "recuperado", nil
    })
    if err != nil {
        t.Errorf("Esperava sucesso, obteve: %v", err)
    }
    if result != "recuperado" {
        t.Errorf("Esperava 'recuperado', obteve: %v", result)
    }
}

7. Boas Práticas e Considerações Finais

Escolha gobreaker para projetos novos. hystrix-go está morto e não receberá atualizações de segurança.

Combine patterns: Circuit breaker funciona bem com retry exponencial, rate limiting e health checks. Mas cuidado: retry dentro de um circuito aberto é contraproducente.

Ajuste thresholds com cuidado:
- MaxRequests muito baixo em semi-aberto pode impedir recuperação.
- Timeout muito curto pode abrir o circuito desnecessariamente.
- Interval muito longo pode mascarar problemas intermitentes.

Armadilhas comuns:
- Não tratar gobreaker.ErrOpenState adequadamente, deixando o usuário sem fallback.
- Configurar thresholds baseados em valores absolutos sem considerar o volume de tráfego.
- Esquecer de resetar contadores após alterações de configuração.

Monitoramento é essencial: Sem métricas, você está voando cego. Use Prometheus + Grafana para visualizar estados e taxas de falha.

Circuit Breaker é uma ferramenta poderosa, mas não uma bala de prata. Use-o para proteger pontos críticos de integração, não para cada chamada remota. Com gobreaker, você obtém uma implementação robusta, testada e performática para construir sistemas resilientes em Go.

Referências