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
- Documentação oficial do gobreaker — Repositório oficial com exemplos, API completa e guia de configuração
- Documentação do hystrix-go — Repositório oficial (arquivado) com exemplos legados e documentação da API
- Padrão Circuit Breaker - Microsoft Docs — Explicação conceitual do padrão com diagramas de estados
- Resiliência em microsserviços com Circuit Breaker - Medium — Artigo prático comparando implementações em Go
- Monitorando Circuit Breaker com Prometheus - Dev.to — Tutorial de integração de métricas com Prometheus
- Testes de resiliência com gobreaker - Testify — Biblioteca de testes usada em conjunto com gobreaker para simular cenários de falha