Padrão Circuit Breaker em Go: Interrompa Falhas em Cascata

Evite falhas em cascata em microsserviços Go.

Conteúdo da página

Um circuit breaker impede que seu serviço Go sobrecarregue uma dependência com falhas, impedindo falhas em cascata que consomem goroutines, sockets e memória até que todo o sistema colapse.

A parte difícil não é a máquina de estados. É decidir onde o breaker deve estar, o que conta como falha, como ele interage com timeouts e retries, e o que seu serviço deve fazer quando o circuito estiver aberto.

circuit breaker

Em Go, o padrão circuit breaker é especialmente útil em torno de chamadas de saída: APIs HTTP, gateways de pagamento, serviços de busca, provedores de e-mail, gateways de LLM, microsserviços internos e outras dependências que podem se tornar lentas, sobrecarregadas ou parcialmente indisponíveis. Usado bem, um circuit breaker reduz falhas em cascata. Usado mal, ele se torna outro modo de falha obscuro.

Que Problema um Circuit Breaker Resolve?

Sistemas distribuídos raramente falham de forma limpa.

Uma dependência pode não estar totalmente fora do ar. Ela pode estar:

  • retornando erros 500
  • retornando respostas de limite de taxa 429
  • aceitando conexões TCP mas nunca respondendo
  • respondendo em 30 segundos em vez de 300 milissegundos
  • falhando apenas para algumas requisições
  • sobrecarregada porque todos os clientes estão tentando novamente ao mesmo tempo

O pior caso muitas vezes não é uma falha crítica. É uma dependência lenta.

Chamadas lentas consomem goroutines, sockets, conexões de banco de dados, memória e capacidade de processamento. Se o seu serviço continuar esperando por uma dependência que já está doente, o seu serviço também pode se tornar doente.

Um circuit breaker impede isso falhando rapidamente após a dependência ultrapassar um limite de falhas.

Em vez de fazer isso para sempre:

requisição -> chamar dependência -> esperar -> timeout -> tentar novamente -> esperar -> falhar

o serviço eventualmente fará isso:

requisição -> circuito aberto -> retornar fallback ou erro imediatamente

Essa falha rápida nem sempre é agradável, mas é previsível. Falha previsível é mais fácil de operar do que um colapso lento.

Os Três Estados do Circuit Breaker

A maioria dos circuit breakers usa três estados.

Closed (Fechado)

O circuito está fechado durante a operação normal.

As requisições são permitidas. O breaker registra sucessos e falhas. Se o número ou a proporção de falhas ultrapassar um limite, o breaker abre.

Closed não significa “seguro para sempre”. Significa “tráfego atualmente permitido”.

Open (Aberto)

O circuito está aberto quando a dependência é considerada doente.

As requisições são rejeitadas imediatamente. O serviço deve retornar um fallback, resposta em cache, resposta degradada ou um erro claro de upstream.

Open não corrige a dependência. Ela dá à dependência tempo para se recuperar e protege o chamador de desperdiçar recursos.

Half-Open (Meio Aberto)

Após um período de resfriamento, o breaker entra em um estado meio aberto.

Apenas um número limitado de requisições de teste são permitidas. Se tiverem sucesso, o breaker fecha. Se falharem, o breaker abre novamente.

Half-open é importante porque evita dois extremos ruins:

  • nunca tentar a dependência novamente
  • enviar todo o tráfego de volta muito rapidamente

As transições de estado se parecem com isso:

stateDiagram-v2 [*] --> Closed Closed --> Open: Failure threshold reached Open --> HalfOpen: Timeout elapsed HalfOpen --> Closed: Trial succeeds HalfOpen --> Open: Trial fails

Circuit Breaker vs Timeout vs Retry

Um erro comum é tratar circuit breakers, retries e timeouts como intercambiáveis. Eles estão relacionados, mas resolvem problemas diferentes.

Timeout

Um timeout limita por quanto tempo uma operação pode rodar.

Em Go, isso geralmente significa passar um context.Context com um deadline ou timeout para a chamada de saída.

Um timeout responde à seguinte pergunta:

Quanto tempo estou disposto a esperar para esta única chamada?

Retry

Um retry repete uma operação quando a falha pode ser temporária.

Retries são úteis para pequenos problemas de rede, respostas 503 temporárias, resets de conexão e outras falhas transitórias.

Um retry responde à seguinte pergunta:

Devo tentar esta chamada novamente?

Circuit Breaker

Um circuit breaker impede chamadas quando a dependência provavelmente está doente.

Ele responde à seguinte pergunta:

Devo chamar esta dependência agora?

Rate Limiter

Um rate limiter controla quanto tráfego é permitido ao longo do tempo.

Ele responde à seguinte pergunta:

Quanto tráfego este chamador deve enviar?

Bulkhead

Um bulkhead isola recursos para que uma dependência não possa consumir tudo.

Ele responde à seguinte pergunta:

Quanto do meu serviço esta dependência pode danificar?

Esses padrões são mais fortes quando usados juntos. Um circuit breaker sem timeouts é fraco. Retries sem jitter podem criar tempestades de retry. Um fallback sem métricas pode esconder uma interrupção.

Quando Usar um Circuit Breaker em Go

Use um circuit breaker quando seu serviço chama uma dependência que pode falhar independentemente do seu serviço.

Bons candidatos incluem:

  • APIs HTTP externas
  • Processadores de pagamento
  • Provedores de e-mail e SMS
  • Serviços de busca
  • Serviços de recomendação
  • Gateways de inferência de LLM
  • Endpoints de microsserviços internos
  • APIs SaaS de terceiros
  • Serviços de leitura lentos ou sobrecarregados

Circuit breakers são especialmente úteis quando o chamador pode degradar gracefulmente.

Por exemplo:

  • retornar dados de produto em cache
  • pular um bloco de recomendação
  • marcar um provedor de pagamento como temporariamente indisponível
  • colocar o trabalho em fila para depois, com uma dead-letter queue como suporte para as mensagens que nunca têm sucesso mesmo após a dependência se recuperar
  • retornar uma resposta parcial
  • falhar rapidamente com um erro temporário claro

A pergunta importante não é “esta chamada pode falhar?”. Tudo pode falhar. A melhor pergunta é:

Se esta dependência estiver falhando, devemos continuar enviando todo o tráfego para ela?

Se a resposta for não, um circuit breaker pode ajudar.

Quando Não Usar um Circuit Breaker

Não adicione um circuit breaker em todas as funções apenas porque o padrão soa responsável.

Um circuit breaker geralmente não é útil para:

  • chamadas de função locais no mesmo processo
  • CRUD simples dentro de um monolito
  • lógica de validação
  • regras de negócio determinísticas
  • operações locais apenas de CPU
  • caminhos de código onde não existe um fallback útil
  • operações de escrita que não são idempotentes
  • dependências já protegidas por uma camada de workflow mais forte

Um circuit breaker também não substitui a higiene básica:

  • definir timeouts
  • propagar context
  • usar pools de conexão corretamente
  • tratar erros explicitamente
  • tornar retries seguros
  • observar taxas de falha

Um circuit breaker ruim pode tornar um sistema mais difícil de entender. Ele pode esconder o problema real, rejeitar tráfego com muita agressividade ou criar comportamento confuso durante a recuperação.

A regra levemente opinativa é simples:

Adicione circuit breakers nas fronteiras de dependência, não em todos os lugares.

Escolhendo uma Biblioteca de Circuit Breaker em Go

Você pode implementar um circuit breaker básico por conta própria, mas a maioria dos serviços Go de produção deve usar uma biblioteca.

A escolha simples mais comum é sony/gobreaker.

Ela oferece:

  • estados closed, open e half-open
  • limites de falha configuráveis
  • timeout de estado open configurável
  • callbacks de mudança de estado
  • contadores de requisições
  • suporte genérico na v2
  • uma superfície de API pequena

Para pipelines de resiliência maiores, você também pode olhar para bibliotecas que compõem múltiplas políticas, como retry, timeout, fallback, rate limiting, bulkhead isolation e circuit breaking. Isso pode ser útil quando você quer uma única camada de resiliência em torno de uma operação.

Para muitos serviços Go, no entanto, gobreaker é suficiente.

Comparação de Pacotes de Circuit Breaker em Go

Go não inclui um circuit breaker nativo na biblioteca padrão. Na prática, você geralmente escolhe entre uma pequena biblioteca de circuit breaker, um framework de resiliência maior ou um pacote mais antigo estilo Hystrix.

Para a maioria dos novos serviços Go, a decisão é simples:

  • use sony/gobreaker se quiser um circuit breaker pequeno e focado
  • use failsafe-go se quiser circuit breakers compostos com retries, timeouts, fallbacks, bulkheads, rate limits e outras políticas de resiliência
  • evite começar novos projetos com hystrix-go a menos que você já tenha código legado usando-o
Package Melhor para Pontos Fortes Compromissos
sony/gobreaker/v2 Circuit breakers simples em torno de clientes HTTP/RPC API pequena, suporte genérico v2, modelo de estados claro, fácil de embalar clientes de dependência Apenas resolve circuit breaking; retries, timeouts e fallbacks devem ser compostos separadamente
failsafe-go Composição completa de políticas de resiliência Retry, fallback, circuit breaker, timeout, bulkhead, rate limiter, cache, hedge, adaptive limiter e adaptive throttler policies Mais conceitos para aprender; mais pesado do que o necessário se você quiser apenas um breaker básico
afex/hystrix-go Sistemas legados estilo Hystrix Conceitos Hystrix familiares, execução estilo command, uso histórico Design mais antigo; não é a melhor opção padrão para novos serviços Go
go-kit/kit/circuitbreaker Serviços baseados em endpoints do Go kit Encaixa-se no estilo middleware do Go kit e arquitetura de endpoints Principalmente útil se seu serviço já usa Go kit
cep21/circuit Comportamento de circuit breaker estilo Hystrix Abordagem estilo Hystrix mais completa Menos comum como opção padrão simples; pode ser mais do que o necessário para serviços pequenos

Minha recomendação padrão é propositalmente sem graça: comece com sony/gobreaker/v2 quando você só precisa de um circuit breaker. Recorra a failsafe-go quando quiser expressar uma política de resiliência completa em um só lugar.

Essa divisão mantém a arquitetura limpa. Um cliente de serviço pequeno não precisa de um framework de resiliência completo apenas para parar de chamar uma dependência com falhas. Mas uma gateway, agregador, SDK de cliente API ou camada de integração de alto tráfego pode se beneficiar de políticas compostas.

Instalando gobreaker

Use o pacote v2 para novos códigos:

go get github.com/sony/gobreaker/v2

Depois importe-o:

import "github.com/sony/gobreaker/v2"

Um Circuit Breaker Básico em Go

Aqui está um pequeno exemplo em torno de uma chamada HTTP.

package main

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

    "github.com/sony/gobreaker/v2"
)

var ErrTemporaryUnavailable = errors.New("dependency temporarily unavailable")

type UserClient struct {
    baseURL string
    http    *http.Client
    cb      *gobreaker.CircuitBreaker[[]byte]
}

func NewUserClient(baseURL string) *UserClient {
    settings := gobreaker.Settings{
        Name:        "user-service",
        MaxRequests: 3,
        Interval:    30 * time.Second,
        Timeout:     10 * time.Second,
        ReadyToTrip: func(counts gobreaker.Counts) bool {
            return counts.ConsecutiveFailures >= 5
        },
        OnStateChange: func(name string, from gobreaker.State, to gobreaker.State) {
            fmt.Printf("circuit breaker %s changed from %s to %s\n", name, from, to)
        },
    }

    return &UserClient{
        baseURL: baseURL,
        http: &http.Client{
            Timeout: 3 * time.Second,
        },
        cb: gobreaker.NewCircuitBreaker[[]byte](settings),
    }
}

func (c *UserClient) GetUser(ctx context.Context, userID string) ([]byte, error) {
    result, err := c.cb.Execute(func() ([]byte, error) {
        req, err := http.NewRequestWithContext(
            ctx,
            http.MethodGet,
            c.baseURL+"/users/"+userID,
            nil,
        )
        if err != nil {
            return nil, err
        }

        resp, err := c.http.Do(req)
        if err != nil {
            return nil, err
        }
        defer resp.Body.Close()

        if resp.StatusCode >= 500 {
            return nil, fmt.Errorf("user service returned %d", resp.StatusCode)
        }

        if resp.StatusCode == http.StatusNotFound {
            return nil, fmt.Errorf("user not found")
        }

        if resp.StatusCode >= 400 {
            return nil, fmt.Errorf("user service client error: %d", resp.StatusCode)
        }

        return io.ReadAll(resp.Body)
    })

    if errors.Is(err, gobreaker.ErrOpenState) {
        return nil, ErrTemporaryUnavailable
    }

    if errors.Is(err, gobreaker.ErrTooManyRequests) {
        return nil, ErrTemporaryUnavailable
    }

    return result, err
}

Este não é um cliente de produção completo, mas mostra a estrutura:

  • o breaker envolve a chamada de saída
  • a requisição HTTP recebe um context
  • o cliente HTTP tem um timeout
  • falhas do lado do servidor contam como falhas do breaker
  • erros de circuito aberto são mapeados para um erro de aplicação

Configurando as Configurações do gobreaker

As configurações principais valem a pena entender.

Name

Name identifica o breaker.

Use um nome estável e específico:

payment-api
search-service
llm-gateway
user-service

Evite nomes vagos como:

http-client
external-call
default

Você vai querer este nome em logs e métricas.

MaxRequests

MaxRequests controla quantas requisições são permitidas enquanto o breaker está half-open.

Um número pequeno geralmente é mais seguro. O propósito do half-open é testar a recuperação, não enviar todo o tráfego imediatamente.

Interval

Interval controla quando as contagens internas são limpas enquanto o breaker está closed.

Se for zero, as contagens não são limpas automaticamente. Um intervalo não zero dá ao breaker uma janela de memória tipo rolling, embora não seja a mesma coisa que uma implementação completa de sliding window.

Timeout

Timeout controla por quanto tempo o breaker permanece open antes de mudar para half-open.

Se o timeout for muito curto, seu serviço continuará sondando uma dependência que não se recuperou. Se for muito longo, a recuperação será atrasada.

Comece com algo conservador, como 10 a 30 segundos, e ajuste com base nas métricas de produção.

ReadyToTrip

ReadyToTrip decide quando o breaker deve abrir.

Uma regra simples é falhas consecutivas:

ReadyToTrip: func(counts gobreaker.Counts) bool {
    return counts.ConsecutiveFailures >= 5
}

Isso é fácil de entender, mas pode não ser ideal para serviços de alto volume.

Outra opção é a proporção de falhas após um número mínimo de requisições:

ReadyToTrip: func(counts gobreaker.Counts) bool {
    total := counts.Requests
    failures := counts.TotalFailures

    if total < 20 {
        return false
    }

    return float64(failures)/float64(total) >= 0.5
}

Isso evita abrir o circuito após um tamanho de amostra minúsculo.

OnStateChange

OnStateChange é onde você deve emitir logs ou métricas.

No mínimo, registre:

  • nome do breaker
  • estado anterior
  • novo estado
  • timestamp

Para sistemas de produção, exponha o estado do breaker como uma métrica. Logs são úteis para depuração, mas métricas são melhores para alertas e dashboards.

IsSuccessful

IsSuccessful permite que você decida quais erros contam como falhas.

Isso é importante.

Nem todo erro deve abrir o breaker. Por exemplo, um 404 Not Found de um serviço de usuário pode ser um resultado de negócio válido. Um 400 Bad Request pode ser culpa do chamador, não da dependência.

Um 503 Service Unavailable, timeout, reset de conexão ou 429 Too Many Requests pode ser um sinal real de saúde da dependência.

Tenha cuidado aqui. Contar os erros errados é uma das maneiras mais fáceis de construir um circuit breaker ruído.

O Que Deve Contar como Falha?

É aqui que o julgamento de engenharia importa.

Geralmente conte estes como falhas:

  • timeouts de rede
  • conexão recusada
  • reset de conexão
  • HTTP 500
  • HTTP 502
  • HTTP 503
  • HTTP 504
  • respostas 429 repetidas
  • respostas malformadas da dependência
  • context deadline exceeded durante a chamada de saída

Geralmente não conte estes como falhas de dependência:

  • erros de validação
  • erros de serialização local
  • respostas 404 esperadas
  • falhas de autorização do lado do chamador
  • rejeições de regras de negócio
  • erros de entrada do usuário

O breaker deve representar a saúde da dependência, não falhas gerais de aplicação.

Circuit Breakers e context.Context

Em Go, circuit breakers não devem substituir context.Context.

Um circuit breaker decide se deve tentar uma chamada. Um context controla por quanto tempo essa chamada pode rodar e se deve parar quando o chamador desaparece.

Uma boa chamada de saída geralmente deve ter ambos:

ctx, cancel := context.WithTimeout(parentCtx, 2*time.Second)
defer cancel()

data, err := client.GetUser(ctx, userID)

O context deve fluir através da cadeia de chamadas:

context da requisição entrante
-> método de serviço
-> método do cliente
-> requisição HTTP
-> dependência

Evite criar contexts de background desanexados dentro de códigos baseados em requisição. Se a requisição do usuário for cancelada, o trabalho downstream geralmente também deve parar.

A regra calma é:

O breaker protege o sistema. O context protege a requisição.

Geralmente você precisa de ambos.

Circuit Breakers e Retries

Retries e circuit breakers podem funcionar bem juntos, mas a ordem importa.

O padrão mais seguro é:

timeout por tentativa
retry com backoff e jitter
circuit breaker em torno da chamada de dependência

Mas não há uma resposta universal. Pense sobre o que você quer contar.

Se cada tentativa de retry passar pelo breaker, uma única requisição de usuário pode contribuir com múltiplas falhas. Isso pode abrir o breaker mais rápido, o que pode ser bom ou ruim.

Se o breaker envolve toda a operação de retry, o breaker vê um sucesso ou falha final por requisição de usuário. Isso é mais calmo, mas pode esconder o número de tentativas falhas.

Para muitos serviços de aplicação, esta estrutura é razoável:

requisição de usuário
-> circuit breaker
   -> política de retry
      -> uma tentativa HTTP com timeout

Isso significa que o breaker rastreia se a operação da dependência funcionou finalmente para o chamador.

Para clientes de nível mais baixo, esta estrutura também pode fazer sentido:

requisição de usuário
-> política de retry
   -> circuit breaker
      -> uma tentativa HTTP com timeout

Isso significa que o breaker protege cada tentativa.

A regra mais importante é esta:

Não faça retries cegamente.

Use:

  • uma contagem máxima pequena de retries
  • backoff exponencial
  • jitter
  • timeouts por tentativa
  • um deadline geral de requisição
  • idempotência para escritas
  • métricas para tentativas de retry

Sem isso, retries podem transformar uma pequena interrupção em uma maior. Para um tratamento mais profundo sobre segurança de retry, consulte Idempotency in Distributed Systems That Actually Works.

Circuit Breakers e Idempotência

Circuit breakers frequentemente aparecem ao lado de retries, e retries levantam a questão da idempotência.

Para operações de leitura, fazer retry geralmente é seguro.

Para operações de escrita, fazer retry pode ser perigoso.

Considere esta chamada de pagamento:

POST /charge

Se a requisição expirar, o pagamento falhou? Talvez. Ela teve sucesso mas a resposta foi perdida? Também talvez.

Se você fizer retry sem uma chave de idempotência, pode cobrar duas vezes.

Para operações de escrita, use uma ou mais destas:

  • chaves de idempotência
  • IDs de requisição
  • IDs de operação
  • restrições de unicidade
  • transactional outbox
  • orquestração de workflow
  • reconciliação explícita

Um circuit breaker pode impedir que você continue chamando um provedor de pagamento com falhas, mas não pode tornar retries inseguros seguros.

Circuit Breakers e Fallbacks

Quando o circuito está aberto, seu serviço precisa de um plano.

Estratégias de fallback possíveis incluem:

  • retornar dados em cache
  • retornar dados antigos com um aviso
  • omitir uma seção não crítica
  • colocar o trabalho em fila para depois
  • mudar para outro provedor
  • retornar um erro temporário
  • mostrar funcionalidade degradada
  • falhar a requisição rapidamente

Um fallback deve ser honesto.

Por exemplo, isto geralmente é bom:

{
  "status": "temporary_unavailable",
  "message": "Recommendations are temporarily unavailable"
}

Isto é arriscado:

{
  "recommendations": []
}

Uma lista vazia pode parecer um resultado válido. Pode esconder uma interrupção, confundir usuários e tornar a depuração mais difícil.

Fallbacks silenciosos são tentadores. Eles também são perigosos.

Circuit Breakers e Observabilidade

Um circuit breaker sem observabilidade é principalmente um gerador de surpresas.

Acompanhe pelo menos estas métricas:

  • estado atual do breaker
  • mudanças de estado
  • chamadas permitidas
  • chamadas rejeitadas
  • sucessos
  • falhas
  • timeouts
  • respostas de fallback
  • tentativas de retry
  • latência downstream
  • status codes downstream

Labels úteis incluem:

  • nome do breaker
  • nome da dependência
  • nome da operação
  • classe de status
  • categoria de erro

Evite labels de alta cardinalidade como ID de usuário, URL completa, ID de requisição ou mensagens de erro brutas.

Você deve ser capaz de responder a estas perguntas nos dashboards:

  • Quais circuit breakers estão abertos agora?
  • Com que frequência eles abrem?
  • Qual dependência causou a abertura?
  • Os usuários estão vendo respostas de fallback?
  • A latência melhorou após o breaker abrir?
  • O volume de retry aumentou antes do breaker abrir?
  • A dependência se recuperou?

Se você não pode observar o breaker, não pode ajustá-lo. Para logging estruturado que combina bem com métricas, consulte Structured Logging in Go with slog.

Uma Estrutura de Cliente HTTP Mais Amigável para Produção

Para serviços reais, evite espalhar a lógica do circuit breaker pelos handlers.

Crie um pequeno pacote de cliente em torno da dependência.

Estrutura de exemplo:

internal/
  userservice/
    client.go
    errors.go
    metrics.go

O handler não deve saber os detalhes do gobreaker. Ele deve depender de um método de cliente de nível de domínio:

type UserService interface {
    GetUser(ctx context.Context, userID string) (*User, error)
}

Então a implementação pode conter:

  • criação de requisição HTTP
  • propagação de context
  • execução do breaker
  • tratamento de status code
  • decodificação de resposta
  • métricas
  • mapeamento de erros

Isso mantém a política de resiliência próxima à fronteira da dependência. Para mais sobre classificação de erros nas fronteiras, consulte Go Error Handling Architecture: Boundaries and Patterns.

Onde Circuit Breakers se Encaixam na Arquitetura de Aplicação

O padrão circuit breaker pertence às fronteiras de integração.

Em uma aplicação Go, isso geralmente significa:

graph LR A[Handler] --> B[Application Service] B --> C[Dependency Client] C --> D[Circuit Breaker] D --> E[HTTP / RPC / DB / Queue]

Mantenha o breaker fora da lógica de negócio quando possível.

A camada de negócio deve entender erros de domínio como:

payment provider unavailable
recommendations unavailable
profile service timeout

Ela não precisa entender os estados do gobreaker.

Esta separação mantém a arquitetura limpa:

  • preocupações de transporte ficam nos clientes
  • política de resiliência fica perto das dependências
  • lógica de domínio fica legível
  • handlers ficam finos
  • testes são mais fáceis de escrever

Este artigo faz parte do tópico App Architecture in Production — junto com guias de idempotência, outbox, saga e orquestração em Integration Patterns.

Erros Comuns

Erro 1: Sem Timeout

Um circuit breaker não para magicamente chamadas lentas a menos que as chamadas retornem.

Se a operação de saída puder travar para sempre, o breaker pode não ver uma falha rápido o suficiente.

Sempre use timeouts.

Erro 2: Um Breaker Global Para Tudo

Não use um breaker para todas as dependências.

Um provedor de e-mail com falhas não deve abrir o circuito para o seu provedor de pagamento. Um endpoint de busca lento não deve bloquear chamadas de perfil de usuário.

Use breakers separados para operações de dependência separadas quando seus modos de falha forem diferentes.

Erro 3: Contando Erros do Chamador como Falhas de Dependência

Se o seu serviço envia entrada ruim e recebe 400 Bad Request, isso geralmente não é uma interrupção downstream.

Não treine o breaker nos seus próprios bugs.

Erro 4: Fazendo Retry de Escritas Não Idempotentes

Retries não são gratuitos. Eles podem duplicar escritas, pagamentos, mensagens ou efeitos colaterais.

Torne as escritas idempotentes antes de fazer retry nelas.

Erro 5: Escondendo Interrupções Atrás de Fallbacks

Fallbacks devem degradar gracefulmente, não falsificar a realidade.

Se uma dependência está fora do ar, suas métricas e logs devem tornar isso óbvio.

Erro 6: Ajustando Sem Dados de Produção

Limites copiados de exemplos são apenas pontos de partida.

Ajuste com base em:

  • volume de requisições
  • taxa de erro normal
  • latência da dependência
  • impacto no usuário
  • tempo de recuperação
  • qualidade do fallback

Erro 7: Usando Circuit Breakers em vez de Gerenciamento de Capacidade

Um circuit breaker não é substituto para:

  • load shedding
  • rate limiting
  • limites de fila
  • autoscaling
  • ajuste de banco de dados
  • limites de pool de conexão
  • quotas upstream

É uma parte de uma estratégia de resiliência.

Padrões Práticos

Para um serviço Go típico chamando uma dependência HTTP interna, um ponto de partida razoável pode ser:

HTTP client timeout: 2 a 5 segundos
per-request context timeout: baseado no SLA do chamador
breaker failure rule: 5 falhas consecutivas ou 50 por cento de falha após 20 requisições
open timeout: 10 a 30 segundos
half-open requests: 1 a 5
retry count: 1 a 3 tentativas
retry backoff: exponencial com jitter

Estes não são valores universais. São pontos de partida seguros.

Para APIs voltadas ao usuário, mantenha orçamentos de latência total apertados. Para jobs em segundo plano, você pode tolerar esperas mais longas. Para provedores de pagamento, seja muito mais cuidadoso com retries e idempotência.

Checklist de Circuit Breaker

Antes de adicionar um circuit breaker, responda a estas perguntas:

  • Qual dependência está sendo protegida?
  • Qual operação está sendo protegida?
  • Quais erros contam como falha de dependência?
  • Quais erros devem ser ignorados pelo breaker?
  • Que timeout se aplica a cada chamada?
  • Retries são permitidos?
  • As escritas são idempotentes?
  • O que acontece quando o circuito está aberto?
  • Existe um fallback?
  • O fallback é visível nas métricas?
  • Quem recebe alerta se o circuito continuar abrindo?
  • Como o breaker será ajustado após a implantação?

Se você não consegue responder a isso, adicionar um breaker pode criar mais confusão do que resiliência.

Testando Circuit Breakers em Go

Teste comportamento, não a máquina de estados interna da biblioteca.

Testes úteis incluem:

  • dependência tem sucesso e resposta é retornada
  • dependência falha repetidamente e circuito abre
  • circuito aberto retorna um erro temporário
  • erros de validação do lado do cliente não ativam o breaker
  • timeout de context é respeitado
  • resposta de fallback é retornada quando esperado
  • métricas são emitidas em mudanças de estado

Use servidores HTTP falsos para testes estilo integração:

server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    http.Error(w, "unavailable", http.StatusServiceUnavailable)
}))
defer server.Close()

Para testes unitários, esconda a dependência atrás de uma interface e injete uma implementação falsa.

Mantenha os testes determinísticos. Evite dormir por durações reais longas. Configure timeouts de breaker curtos nos testes. Para mais sobre testar código Go concurrente com tempo falso e bolhas isoladas, consulte Testing Concurrent Go Code with testing/synctest.

Você Deveria Construir Seu Próprio Circuit Breaker?

Construir um pequeno circuit breaker é um bom exercício de aprendizado. Ele ajuda você a entender a máquina de estados.

Para código de produção, prefira uma biblioteca mantida a menos que suas necessidades sejam muito específicas.

Um breaker de produção precisa lidar com:

  • concorrência
  • transições de estado
  • contadores
  • sondas half-open
  • callbacks
  • classificação personalizada de falhas
  • comportamento livre de race conditions
  • tratamento de erros previsível

Isso não é impossível, mas é fácil errar sutilmente.

A biblioteca sem graça é geralmente a melhor escolha.

Conclusão

O padrão circuit breaker não é pó mágico de confiabilidade.

Em Go, funciona melhor quando faz parte de uma pequena pilha de resiliência explícita:

context timeout
+ retry com backoff e jitter
+ circuit breaker
+ fallback
+ métricas

O padrão é mais útil nas fronteiras de dependência, especialmente em torno de serviços remotos que podem se tornar lentos ou parcialmente indisponíveis.

Use-o para parar falhas em cascata. Use-o para falhar rapidamente quando uma dependência estiver claramente doente. Use-o para dar aos sistemas sobrecarregados espaço para se recuperar.

Mas não o use como desculpa para ignorar timeouts, idempotência, observabilidade ou arquitetura limpa.

Um bom circuit breaker torna a falha mais clara e barata. Um ruim apenas torna a falha mais misteriosa.

Referências

Subscrever

Receba novos artigos sobre sistemas, infraestrutura e engenharia de IA.