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

Evite falhas em cascata em microsserviços Go.

Conteúdo da página

Um circuit breaker (disjuntor) impede que seu serviço em Go continue a sobrecarregar uma dependência falha, prevenindo 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 disjuntor se encaixa, o que conta como falha, como ele interage com timeouts (tempos limite) e retries (tentativas de repetição), e o que seu serviço deve fazer quando o circuito estiver aberto.

circuit breaker

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

Qual 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 geralmente não é uma falha dura. É uma dependência lenta.

Chamadas lentas consomem goroutines, sockets, conexões de banco de dados, memória e capacidade de workers (processadores de trabalho). Se seu serviço continuar aguardando uma dependência que já está instável, seu serviço também pode se tornar instável.

Um circuit breaker impede isso, falhando rapidamente após a dependência cruzar um limiar de falha.

Em vez de fazer isso para sempre:

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

o serviço eventualmente faz 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.

Fechado (Closed)

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

Requisições são permitidas. O disjuntor registra sucessos e falhas. Se o número ou a proporção de falhas cruzar um limiar, o disjuntor abre.

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

Aberto (Open)

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

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

Aberto não conserta a dependência. Ele dá à dependência tempo para se recuperar e protege o chamador de desperdiçar recursos.

Meio-Aberto (Half-Open)

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

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

Meio-aberto é importante porque evita dois extremos ruins:

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

As transições de estado 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 quanto tempo uma operação pode executar.

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

Um timeout responde a esta pergunta:

Por quanto tempo estou disposto a esperar por 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 temporárias 503, redefinições de conexão e outras falhas transitórias.

Um retry responde a esta pergunta:

Devo tentar esta chamada novamente?

Circuit Breaker

Um circuit breaker para chamadas quando a dependência provavelmente está instável.

Ele responde a esta pergunta:

Devo chamar esta dependência agora mesmo?

Rate Limiter (Limitador de Taxa)

Um limitador de taxa controla quanto tráfego é permitido ao longo do tempo.

Ele responde a esta pergunta:

Quanto tráfego este chamador deve enviar?

Bulkhead (Bulkhead)

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

Ele responde a esta 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 (variação aleatória) podem criar tempestades de repetição. 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

Os circuit breakers são especialmente úteis quando o chamador pode degradar graciosamente.

Por exemplo:

  • retornar dados de produtos em cache
  • pular um bloco de recomendação
  • marcar um provedor de pagamento como temporariamente indisponível
  • enfileirar trabalho para mais tarde
  • 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 pergunta melhor é:

Se esta dependência estiver falhando, devemos continuar enviando tráfego total 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 a cada função apenas porque o padrão parece responsável.

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

  • chamadas de função locais no processo
  • CRUD simples dentro de um monólito
  • lógica de validação
  • regras de negócios 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 contexto
  • usar pools de conexão corretamente
  • lidar com erros explicitamente
  • tornar retries seguros
  • observar taxas de falha

Um circuit breaker ruim pode tornar um sistema mais difícil de compreender. Ele pode esconder o problema real, rejeitar tráfego agressivamente demais 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 sozinho, mas a maioria dos serviços Go de produção deve usar uma biblioteca.

A escolha simples mais comum é sony/gobreaker.

Ele te dá:

  • estados fechado, aberto e meio-aberto
  • limiares de falha configuráveis
  • timeout de estado aberto configurável
  • callbacks de mudança de estado
  • contadores de requisição
  • 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, limitação de taxa, isolamento bulkhead e circuit breaking. Isso pode ser útil quando você quer uma única camada de resiliência ao redor de uma operação.

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

Pacotes de Circuit Breaker em Go Comparados

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

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

  • use sony/gobreaker se você quiser um circuit breaker pequeno e focado
  • use failsafe-go se você quiser circuit breakers compostos com retries, timeouts, fallbacks, bulkheads, limites de taxa e outras políticas de resiliência
  • evite iniciar novos projetos com hystrix-go a menos que você já tenha código legado usando-o
Pacote Melhor para Pontos Fortes Compromissos
sony/gobreaker/v2 Circuit breakers simples ao redor de clientes HTTP/RPC API pequena, suporte genérico v2, modelo de estado claro, fácil de envolver clientes de dependência Resolve apenas 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, limitador de taxa, cache, hedge, limitador adaptativo e políticas de throttler adaptativo Mais conceitos para aprender; mais pesado do que necessário se você quiser apenas um disjuntor básico
afex/hystrix-go Sistemas estilo Hystrix legados Conceitos Hystrix familiares, execução estilo comando, uso histórico Design mais antigo; não é o melhor padrão para novos serviços Go
go-kit/kit/circuitbreaker Serviços baseados em endpoints do Go kit Encaixa-se no estilo de middleware e arquitetura de endpoints do Go kit Útil principalmente se seu serviço já usa Go kit
cep21/circuit Comportamento de circuit breaker estilo Hystrix Abordagem estilo Hystrix mais funcional Menos comum como padrão simples; pode ser mais do que o necessário para pequenos serviços

Minha recomendação padrão é chata por propósito: comece com sony/gobreaker/v2 quando você precisar apenas de um circuit breaker. Use failsafe-go quando quiser expressar uma política de resiliência completa em um único lugar.

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

Instalando gobreaker

Use o pacote v2 para novo código:

go get github.com/sony/gobreaker/v2

Então importe-o:

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

Um Circuit Breaker Básico em Go

Aqui está um pequeno exemplo ao redor de uma chamada HTTP.

package main

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

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

var ErrTemporaryUnavailable = errors.New("dependency temporariamente indisponível")

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 mudou de %s para %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 retornou %d", resp.StatusCode)
        }

        if resp.StatusCode == http.StatusNotFound {
            return nil, fmt.Errorf("user não encontrado")
        }

        if resp.StatusCode >= 400 {
            return nil, fmt.Errorf("user service erro de cliente: %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 forma:

  • o disjuntor envolve a chamada de saída
  • a requisição HTTP recebe um contexto
  • o cliente HTTP tem um timeout
  • falhas do lado do servidor contam como falhas do disjuntor
  • 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 ser compreendidas.

Name (Nome)

Name identifica o disjuntor.

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 esse nome em logs e métricas.

MaxRequests

MaxRequests controla quantas requisições são permitidas enquanto o disjuntor está meio-aberto.

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

Interval

Interval controla quando as contagens internas são limpas enquanto o disjuntor está fechado.

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

Timeout

Timeout controla quanto tempo o disjuntor fica aberto antes de passar para meio-aberto.

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 então ajuste com base nas métricas de produção.

ReadyToTrip

ReadyToTrip decide quando o disjuntor deve abrir.

Uma regra simples é falhas consecutivas:

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

Isso é fácil de raciocinar, mas pode não ser o certo 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 disjuntor
  • estado antigo
  • novo estado
  • carimbo de data/hora

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

IsSuccessful

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

Isso é importante.

Nem todo erro deve abrir o disjuntor. 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, redefinição 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 barulhento.

O Que Deve Contar Como Falha?

É aqui que o julgamento de engenharia importa.

Geralmente conte estes como falhas:

  • timeouts de rede
  • conexão recusada
  • redefinição de conexão
  • HTTP 500
  • HTTP 502
  • HTTP 503
  • HTTP 504
  • respostas repetidas 429
  • respostas mal formadas da dependência
  • prazo do contexto excedido 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 disjuntor deve representar a saúde da dependência, não a falha geral da 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 contexto controla quanto tempo essa chamada pode executar e se deve parar quando o chamador se for.

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 contexto deve fluir através da cadeia de chamadas:

contexto da requisição incoming
-> método do serviço
-> método do cliente
-> requisição HTTP
-> dependência

Evite criar contextos de background desanexados dentro de código escopado por 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 contexto protege a requisição.

Normalmente 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 ao redor da chamada da dependência

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

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

Se o disjuntor envolver toda a operação de retry, o disjuntor 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 forma é razoável:

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

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

Para clientes de nível inferior, esta forma também pode fazer sentido:

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

Isso significa que o disjuntor protege cada tentativa.

A regra mais importante é esta:

Não faça retry cegamente.

Use:

  • uma contagem máxima de retry pequena
  • backoff exponencial
  • jitter
  • timeouts por tentativa
  • um prazo 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 da segurança de retry, veja Idempotency in Distributed Systems That Actually Works.

Circuit Breakers e Idempotência

Os 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 der timeout, o pagamento falhou? Talvez. Ele teve sucesso, mas a resposta foi perdida? Também talvez.

Se você fizer retry sem uma chave de idempotência, você 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 únicas
  • outbox transacional
  • orquestração de workflow
  • reconciliação explícita

Um circuit breaker pode impedir que você continue chamando um provedor de pagamento falho, 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 obsoletos com um aviso
  • omitir uma seção não crítica
  • enfileirar trabalho para mais tarde
  • mudar para outro provedor
  • retornar um erro temporário
  • mostrar funcionalidade degradada
  • falhar na requisição rapidamente

Um fallback deve ser honesto.

Por exemplo, isso geralmente é bom:

{
  "status": "temporary_unavailable",
  "message": "As recomendações estão temporariamente indisponíveis"
}

Isso é 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.

Rastreie pelo menos estas métricas:

  • estado atual do disjuntor
  • mudanças de estado
  • chamadas permitidas
  • chamadas rejeitadas
  • sucessos
  • falhas
  • timeouts
  • respostas de fallback
  • tentativas de retry
  • latência downstream
  • códigos de status downstream

Rótulos úteis incluem:

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

Evite rótulos de alta cardinalidade, como ID de usuário, URL completo, ID de requisição ou mensagens de erro brutas.

Você deve ser capaz de responder a estas perguntas a partir dos painéis:

  • 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 disjuntor abrir?
  • O volume de retry aumentou antes do disjuntor abrir?
  • A dependência se recuperou?

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

Uma Forma de Cliente HTTP Mais Amigável à Produção

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

Crie um pequeno pacote de cliente ao redor da dependência.

Estrutura de exemplo:

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

O handler não deve conhecer 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 contexto
  • execução do disjuntor
  • manipulação de código de status
  • decodificação de resposta
  • métricas
  • mapeamento de erro

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

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

O padrão de 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 disjuntor fora da lógica de negócio quando possível.

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

provedor de pagamento indisponível
recomendações indisponíveis
timeout do serviço de perfil

Não precisa entender 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 é parte do tópico App Architecture in Production — ao lado de guias de idempotência, outbox, saga e orquestração em Integration Patterns.

Erros Comuns

Erro 1: Sem Timeout

Um circuit breaker não para mágicamente chamadas lentas a menos que as chamadas retornem.

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

Sempre use timeouts.

Erro 2: Um Disjuntor Global para Tudo

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

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

Use disjuntores separados para operações de dependência separadas quando seus modos de falha diferirem.

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

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

Não treine o disjuntor com seus próprios bugs.

Erro 4: Fazendo Retry em 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 graciosamente, não falsificar a realidade.

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

Erro 6: Ajustando Sem Dados de Produção

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

Ajuste baseado 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 é um substituto para:

  • descarte de carga
  • limitação de taxa
  • limites de fila
  • autoescalabilidade
  • 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:

Timeout do cliente HTTP: 2 a 5 segundos
Timeout de contexto por requisição: baseado no SLA do chamador
Regra de falha do disjuntor: 5 falhas consecutivas ou 50 por cento de falha após 20 requisições
Timeout aberto: 10 a 30 segundos
Requisições meio-abertas: 1 a 5
Contagem de retry: 1 a 3 tentativas
Backoff de retry: exponencial com jitter

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

Para APIs voltadas para o usuário, mantenha orçamentos totais de latência apertados. Para trabalhos em background, 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 disjuntor?
  • Qual 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 disjuntor será ajustado após a implantação?

Se você não pode responder a estas, adicionar um disjuntor 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 disparam o disjuntor
  • timeout de contexto é 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, "indisponível", 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 disjuntor curtos nos testes. Para mais sobre testar código Go concorrente com tempo falso e bolhas isoladas, veja Testing Concurrent Go Code with testing/synctest.

Você Deve Construir Seu Próprio Circuit Breaker?

Construir um pequeno circuit breaker é um bom exercício de aprendizado. 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 disjuntor de produção precisa lidar com:

  • concorrência
  • transições de estado
  • contadores
  • sondas meio-abertas
  • callbacks
  • classificação de falha personalizada
  • comportamento livre de corrida
  • manipulação de erro previsível

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

A biblioteca chata geralmente é a melhor escolha.

Conclusão

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

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

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

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

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

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

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

Referências

Assinar

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