Patrones de Circuit Breaker en Go: Detener las Fallas en Cascada

Detenga las fallas en cascada en microservicios de Go.

Índice

Un interruptor de circuito (circuit breaker) impide que tu servicio en Go bombardee una dependencia con fallos, previniendo fallos en cascada que consumen goroutines, sockets y memoria hasta que todo el sistema colapsa.

La parte difícil no es la máquina de estados. Es decidir dónde pertenece el interruptor, qué se considera un fallo, cómo interactúa con los tiempos de espera (timeouts) y los reintentos, y qué debería hacer tu servicio cuando el circuito está abierto.

interruptor de circuito

En Go, el patrón de interruptor de circuito es especialmente útil en llamadas salientes: APIs HTTP, pasarelas de pago, servicios de búsqueda, proveedores de correo electrónico, pasarelas de LLM, microservicios internos y otras dependencias que pueden volverse lentas, sobrecargadas o parcialmente inaccesibles. Usado correctamente, un interruptor de circuito reduce los fallos en cascada. Usado mal, se convierte en otro modo de fallo obscuro.

¿Qué problema resuelve un interruptor de circuito?

Los sistemas distribuidos rara vez fallan de manera limpia.

Una dependencia podría no estar completamente caída. Podría estar:

  • devolviendo errores 500
  • devolviendo respuestas de límite de tasa 429
  • aceptando conexiones TCP pero nunca respondiendo
  • respondiendo en 30 segundos en lugar de 300 milisegundos
  • fallando solo para algunas solicitudes
  • sobrecargada porque cada cliente está reintentando al mismo tiempo

El peor caso a menudo no es un fallo duro. Es una dependencia lenta.

Las llamadas lentas consumen goroutines, sockets, conexiones a bases de datos, memoria y capacidad de trabajadores. Si tu servicio sigue esperando a una dependencia que ya no es saludable, tu servicio también puede volverse no saludable.

Un interruptor de circuito evita esto fallando rápido después de que la dependencia cruza un umbral de fallo.

En lugar de hacer esto para siempre:

solicitud -> llamar a dependencia -> esperar -> tiempo de espera agotado -> reintento -> esperar -> fallo

el servicio eventualmente hace esto:

solicitud -> circuito abierto -> devolver respuesta alternativa o error inmediatamente

Ese fallo rápido no siempre es agradable, pero es predecible. Un fallo predecible es más fácil de operar que un colapso lento.

Los tres estados del interruptor de circuito

La mayoría de los interruptores de circuito usan tres estados.

Cerrado

El circuito está cerrado durante la operación normal.

Se permiten las solicitudes. El interruptor registra los éxitos y los fallos. Si el número o la proporción de fallos cruza un umbral, el interruptor se abre.

Cerrado no significa “seguro para siempre”. Significa “el tráfico está actualmente permitido”.

Abierto

El circuito está abierto cuando la dependencia se considera no saludable.

Las solicitudes se rechazan inmediatamente. El servicio debería devolver una respuesta alternativa (fallback), una respuesta en caché, una respuesta degradada o un error claro de upstream.

Abierto no arregla la dependencia. Le da a la dependencia tiempo para recuperarse y protege al llamante de desperdiciar recursos.

Semiabierto

Después de un período de enfriamiento, el interruptor entra en un estado semiabierto.

Solo se permite un número limitado de solicitudes de prueba. Si tienen éxito, el interruptor se cierra. Si fallan, el interruptor se abre nuevamente.

El estado semiabierto es importante porque evita dos extremos malos:

  • no intentar la dependencia nunca más
  • enviar todo el tráfico de vuelta demasiado rápido

Las transiciones de estado se ven así:

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

Interruptor de circuito vs Timeout vs Reintento

Un error común es tratar los interruptores de circuito, los reintentos y los tiempos de espera como intercambiables. Están relacionados, pero resuelven problemas diferentes.

Timeout (Tiempo de espera)

Un timeout limita cuánto tiempo puede ejecutarse una operación.

En Go, esto suele significar pasar un context.Context con un plazo o timeout a la llamada saliente.

Un timeout responde a esta pregunta:

¿Cuánto tiempo estoy dispuesto a esperar por esta llamada?

Reintento

Un reintento repite una operación cuando el fallo podría ser temporal.

Los reintentos son útiles para fallos de red cortos, respuestas temporales 503, reinicios de conexión y otros fallos transitorios.

Un reintento responde a esta pregunta:

¿Debería intentar esta llamada nuevamente?

Interruptor de circuito

Un interruptor de circuito detiene las llamadas cuando la dependencia probablemente no es saludable.

Responde a esta pregunta:

¿Debería llamar a esta dependencia en absoluto ahora mismo?

Limitador de tasa

Un limitador de tasa controla cuánta cantidad de tráfico se permite en el tiempo.

Responde a esta pregunta:

¿Cuánto tráfico debería enviar este llamante?

Bulkhead (Compartimento estanco)

Un bulkhead aísla los recursos para que una dependencia no pueda consumir todo.

Responde a esta pregunta:

¿Cuánto de mi servicio puede dañar esta dependencia?

Estos patrones son más fuertes cuando se usan juntos. Un interruptor de circuito sin timeouts es débil. Los reintentos sin jitter pueden crear tormentas de reintento. Una respuesta alternativa sin métricas puede ocultar una interrupción.

Cuándo usar un interruptor de circuito en Go

Usa un interruptor de circuito cuando tu servicio llama a una dependencia que puede fallar independientemente de tu servicio.

Buenos candidatos incluyen:

  • APIs HTTP externas
  • procesadores de pagos
  • proveedores de correo electrónico y SMS
  • servicios de búsqueda
  • servicios de recomendación
  • pasarelas de inferencia de LLM
  • endpoints de microservicios internos
  • APIs SaaS de terceros
  • servicios de lectura lentos o sobrecargados

Los interruptores de circuito son especialmente útiles cuando el llamante puede degradarse de manera elegante.

Por ejemplo:

  • devolver datos de productos en caché
  • omitir un bloque de recomendaciones
  • marcar un proveedor de pagos como temporalmente no disponible
  • poner trabajo en cola para más tarde
  • devolver una respuesta parcial
  • fallar rápido con un error temporal claro

La pregunta importante no es “¿puede fallar esta llamada?”. Todo puede fallar. La mejor pregunta es:

Si esta dependencia está fallando, ¿debemos seguir enviando tráfico completo a ella?

Si la respuesta es no, un interruptor de circuito puede ayudar.

Cuándo no usar un interruptor de circuito

No agregues un interruptor de circuito a cada función solo porque el patrón suena responsable.

Un interruptor de circuito usualmente no es útil para:

  • llamadas a funciones locales dentro del proceso
  • CRUD simple dentro de un monolito
  • lógica de validación
  • reglas de negocio deterministas
  • operaciones locales solo de CPU
  • caminos de código donde no existe una respuesta alternativa útil
  • operaciones de escritura que no son idempotentes
  • dependencias ya protegidas por una capa de flujo de trabajo más fuerte

Un interruptor de circuito tampoco reemplaza la higiene básica:

  • configurar timeouts
  • propagar contexto
  • usar pools de conexiones correctamente
  • manejar errores explícitamente
  • hacer reintentos seguros
  • observar tasas de fallo

Un mal interruptor de circuito puede hacer que un sistema sea más difícil de entender. Puede ocultar el problema real, rechazar tráfico demasiado agresivamente o crear un comportamiento confuso durante la recuperación.

La regla ligeramente opinada es simple:

Agrega interruptores de circuito en los límites de las dependencias, no en todas partes.

Eligiendo una biblioteca de interruptor de circuito para Go

Puedes implementar un interruptor de circuito básico tú mismo, pero la mayoría de los servicios Go en producción deberían usar una biblioteca.

La elección simple más común es sony/gobreaker.

Te ofrece:

  • estados cerrado, abierto y semiabierto
  • umbrales de fallo configurables
  • timeout de estado abierto configurable
  • callbacks de cambio de estado
  • contadores de solicitudes
  • soporte genérico en v2
  • una superficie de API pequeña

Para pipelines de resiliencia más grandes, también puedes buscar bibliotecas que compongan múltiples políticas, como reintento, timeout, respuesta alternativa, limitación de tasa, aislamiento bulkhead e interrupción de circuito. Eso puede ser útil cuando quieres una capa de resiliencia única alrededor de una operación.

Para muchos servicios Go, sin embargo, gobreaker es suficiente.

Paquetes de interruptor de circuito en Go comparados

Go no incluye un interruptor de circuito incorporado en la biblioteca estándar. En la práctica, usualmente eliges entre una pequeña biblioteca de interruptor de circuito, un marco de resiliencia más grande o un paquete estilo Hystrix más antiguo.

Para la mayoría de los nuevos servicios Go, la decisión es simple:

  • usa sony/gobreaker si quieres un interruptor de circuito pequeño y enfocado
  • usa failsafe-go si quieres interruptores de circuito compuestos con reintentos, timeouts, respuestas alternativas, bulkheads, límites de tasa y otras políticas de resiliencia
  • evita comenzar nuevos proyectos con hystrix-go a menos que ya tengas código legado usándolo
Paquete Mejor para Fortalezas Compensaciones
sony/gobreaker/v2 Interruptores de circuito simples alrededor de clientes HTTP/RPC API pequeña, soporte genérico v2, modelo de estado claro, fácil de envolver clientes de dependencia Solo resuelve la interrupción de circuito; los reintentos, timeouts y respuestas alternativas deben componerse por separado
failsafe-go Composición completa de políticas de resiliencia Reintento, respuesta alternativa, interruptor de circuito, timeout, bulkhead, limitador de tasa, caché, hedge, limitador adaptativo y regulador adaptativo Más conceptos para aprender; más pesado de lo necesario si solo quieres un interruptor básico
afex/hystrix-go Sistemas estilo Hystrix legados Conceptos familiares de Hystrix, ejecución estilo comando, uso histórico Diseño más antiguo; no es la mejor opción predeterminada para nuevos servicios Go
go-kit/kit/circuitbreaker Servicios basados en endpoints de Go kit Se ajusta al estilo de middleware y arquitectura de endpoints de Go kit Útil principalmente si tu servicio ya usa Go kit
cep21/circuit Comportamiento de interruptor de circuito similar a Hystrix Enfoque estilo Hystrix con más características Menos común como predeterminado simple; puede ser más de lo necesario para servicios pequeños

Mi recomendación predeterminada es aburrida a propósito: comienza con sony/gobreaker/v2 cuando solo necesitas un interruptor de circuito. Apunta a failsafe-go cuando quieras expresar una política de resiliencia completa en un solo lugar.

Esa división mantiene la arquitectura limpia. Un pequeño cliente de servicio no necesita un marco de resiliencia completo solo para dejar de llamar a una dependencia con fallos. Pero una pasarela, agregador, SDK de cliente API o capa de integración de alto tráfico puede beneficiarse de políticas compuestas.

Instalando gobreaker

Usa el paquete v2 para código nuevo:

go get github.com/sony/gobreaker/v2

Luego impórtalo:

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

Un interruptor de circuito básico en Go

Aquí hay un pequeño ejemplo alrededor de una llamada 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 no es un cliente de producción completo, pero muestra la forma:

  • el interruptor envuelve la llamada saliente
  • la solicitud HTTP recibe un contexto
  • el cliente HTTP tiene un timeout
  • los fallos del lado del servidor cuentan como fallos del interruptor
  • los errores de circuito abierto se mapean a un error de aplicación

Configurando los ajustes de gobreaker

Los ajustes clave valen la pena entenderlos.

Name (Nombre)

Name identifica el interruptor.

Usa un nombre estable y específico:

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

Evita nombres vagos como:

http-client
external-call
default

Querrás este nombre en los registros (logs) y métricas.

MaxRequests

MaxRequests controla cuántas solicitudes se permiten mientras el interruptor está semiabierto.

Un número pequeño suele ser más seguro. El propósito de semiabierto es probar la recuperación, no enviar todo el tráfico inmediatamente.

Interval

Interval controla cuándo se borran los conteos internos mientras el interruptor está cerrado.

Si es cero, los conteos no se borran automáticamente. Un intervalo no nulo le da al interruptor una ventana de memoria tipo rodante, aunque no es lo mismo que una implementación completa de ventana deslizante.

Timeout

Timeout controla cuánto tiempo permanece el interruptor abierto antes de pasar a semiabierto.

Si el timeout es demasiado corto, tu servicio seguirá probando una dependencia que no se ha recuperado. Si es demasiado largo, la recuperación se retrasará.

Comienza con algo conservador, como 10 a 30 segundos, y luego ajusta con métricas de producción.

ReadyToTrip

ReadyToTrip decide cuándo el interruptor debería abrirse.

Una regla simple es fallos consecutivos:

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

Eso es fácil de entender, pero puede no ser correcto para servicios de alto volumen.

Otra opción es la proporción de fallos después de un número mínimo de solicitudes:

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

    if total < 20 {
        return false
    }

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

Esto evita abrir el circuito después de un tamaño de muestra diminuto.

OnStateChange

OnStateChange es donde deberías emitir registros o métricas.

Como mínimo, registra:

  • nombre del interruptor
  • estado anterior
  • nuevo estado
  • marca de tiempo

Para sistemas de producción, expón el estado del interruptor como una métrica. Los registros son útiles para depuración, pero las métricas son mejores para alertas y paneles.

IsSuccessful

IsSuccessful te permite decidir qué errores cuentan como fallos.

Esto es importante.

No cada error debería abrir el interruptor. Por ejemplo, un 404 Not Found de un servicio de usuarios puede ser un resultado de negocio válido. Un 400 Bad Request podría ser culpa del llamante, no de la dependencia.

Un 503 Service Unavailable, timeout, reinicio de conexión o 429 Too Many Requests pueden ser una señal real de salud de la dependencia.

Ten cuidado aquí. Contar los errores incorrectos es una de las formas más fáciles de construir un interruptor de circuito ruidoso.

¿Qué debería contar como fallo?

Aquí es donde importa el juicio de ingeniería.

Usualmente cuenta estos como fallos:

  • timeouts de red
  • conexión rechazada
  • conexión reiniciada
  • HTTP 500
  • HTTP 502
  • HTTP 503
  • HTTP 504
  • respuestas 429 repetidas
  • respuestas mal formadas de la dependencia
  • límite de contexto excedido durante la llamada saliente

Usualmente no cuentes estos como fallos de dependencia:

  • errores de validación
  • errores de serialización local
  • respuestas 404 esperadas
  • fallos de autorización del lado del llamante
  • rechazos de reglas de negocio
  • errores de entrada del usuario

El interruptor debería representar la salud de la dependencia, no el fallo general de la aplicación.

Interruptores de circuito y context.Context

En Go, los interruptores de circuito no deberían reemplazar context.Context.

Un interruptor de circuito decide si intentar una llamada. Un contexto controla cuánto tiempo puede ejecutarse esa llamada y si debería detenerse cuando el llamante se va.

Una buena llamada saliente usualmente debería tener ambos:

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

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

El contexto debería fluir a través de la cadena de llamadas:

contexto de solicitud entrante
-> método de servicio
-> método de cliente
-> solicitud HTTP
-> dependencia

Evita crear contextos de fondo desconectados dentro del código con alcance de solicitud. Si la solicitud del usuario se cancela, el trabajo aguas abajo usualmente también debería detenerse.

La regla tranquila es:

El interruptor protege el sistema. El contexto protege la solicitud.

Normalmente necesitas ambos.

Interruptores de circuito y reintentos

Los reintentos y los interruptores de circuito pueden funcionar bien juntos, pero el orden importa.

El predeterminado más seguro es:

timeout por intento
reintento con backoff y jitter
interruptor de circuito alrededor de la llamada de dependencia

Pero no hay una respuesta universal. Piensa en qué quieres contar.

Si cada intento de reintento pasa a través del interruptor, una solicitud de usuario puede contribuir con múltiples fallos. Eso puede abrir el interruptor más rápido, lo cual puede ser bueno o malo.

Si el interruptor envuelve toda la operación de reintento, el interruptor ve un éxito o fallo final por solicitud de usuario. Eso es más tranquilo, pero puede ocultar el número de intentos fallidos.

Para muchos servicios de aplicación, esta forma es razonable:

solicitud de usuario
-> interruptor de circuito
   -> política de reintento
      -> un intento HTTP con timeout

Eso significa que el interruptor rastrea si la operación de dependencia funcionó finalmente para el llamante.

Para clientes de nivel inferior, esta forma también puede tener sentido:

solicitud de usuario
-> política de reintento
   -> interruptor de circuito
      -> un intento HTTP con timeout

Eso significa que el interruptor protege cada intento.

La regla más importante es esta:

No reintentes a ciegas.

Usa:

  • un recuento máximo de reintento pequeño
  • backoff exponencial
  • jitter
  • timeouts por intento
  • un plazo general de solicitud
  • idempotencia para escrituras
  • métricas para intentos de reintento

Sin esos, los reintentos pueden convertir una pequeña interrupción en una más grande. Para un tratamiento más profundo de la seguridad de reintento, consulta Idempotencia en sistemas distribuidos que realmente funciona.

Interruptores de circuito e idempotencia

Los interruptores de circuito a menudo aparecen junto a los reintentos, y los reintentos plantean la pregunta de la idempotencia.

Para operaciones de lectura, reintentar suele ser seguro.

Para operaciones de escritura, reintentar puede ser peligroso.

Considera esta llamada de pago:

POST /charge

Si la solicitud tiene un timeout, ¿el pago falló? Tal vez. ¿Tuvo éxito pero la respuesta se perdió? También tal vez.

Si reintentas sin una clave de idempotencia, podrías cobrar dos veces.

Para operaciones de escritura, usa una o más de estas:

  • claves de idempotencia
  • IDs de solicitud
  • IDs de operación
  • restricciones únicas
  • outbox transaccional
  • orquestación de flujos de trabajo
  • reconciliación explícita

Un interruptor de circuito puede detenerte de seguir llamando a un proveedor de pagos con fallos, pero no puede hacer que los reintentos inseguros sean seguros.

Interruptores de circuito y respuestas alternativas (Fallbacks)

Cuando el circuito está abierto, tu servicio necesita un plan.

Las estrategias de respuesta alternativa posibles incluyen:

  • devolver datos en caché
  • devolver datos obsoletos con una advertencia
  • omitir una sección no crítica
  • poner trabajo en cola para más tarde
  • cambiar a otro proveedor
  • devolver un error temporal
  • mostrar funcionalidad degradada
  • fallar la solicitud rápidamente

Una respuesta alternativa debería ser honesta.

Por ejemplo, esto suele ser bueno:

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

Esto es arriesgado:

{
  "recommendations": []
}

Una lista vacía puede parecer un resultado válido. Puede ocultar una interrupción, confundir a los usuarios y hacer la depuración más difícil.

Las respuestas alternativas silenciosas son tentadoras. También son peligrosas.

Interruptores de circuito y observabilidad

Un interruptor de circuito sin observabilidad es principalmente un generador de sorpresas.

Rastrea al menos estas métricas:

  • estado actual del interruptor
  • cambios de estado
  • llamadas permitidas
  • llamadas rechazadas
  • éxitos
  • fallos
  • timeouts
  • respuestas alternativas
  • intentos de reintento
  • latencia aguas abajo
  • códigos de estado aguas abajo

Las etiquetas útiles incluyen:

  • nombre del interruptor
  • nombre de la dependencia
  • nombre de la operación
  • clase de estado
  • categoría de error

Evita etiquetas de alta cardinalidad como ID de usuario, URL completa, ID de solicitud o mensajes de error crudos.

Deberías poder responder estas preguntas desde los paneles:

  • ¿Qué interruptores de circuito están abiertos ahora mismo?
  • ¿Con qué frecuencia se abren?
  • ¿Qué dependencia causó la apertura?
  • ¿Los usuarios están viendo respuestas alternativas?
  • ¿La latencia mejoró después de que el interruptor se abrió?
  • ¿El volumen de reintento aumentó antes de que el interruptor se abriera?
  • ¿La dependencia se recuperó?

Si no puedes observar el interruptor, no puedes ajustarlo. Para registro estructurado que se empareja bien con métricas, consulta Registro estructurado en Go con slog.

Una forma de cliente HTTP más amigable para producción

Para servicios reales, evita esparcir la lógica del interruptor de circuito entre los manejadores.

Crea un pequeño paquete de cliente alrededor de la dependencia.

Estructura de ejemplo:

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

El manejador no debería conocer los detalles de gobreaker. Debería depender de un método de cliente a nivel de dominio:

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

Entonces la implementación puede contener:

  • creación de solicitud HTTP
  • propagación de contexto
  • ejecución del interruptor
  • manejo de códigos de estado
  • decodificación de respuesta
  • métricas
  • mapeo de errores

Esto mantiene la política de resiliencia cerca del límite de la dependencia. Para más información sobre la clasificación de errores en los límites, consulta Arquitectura de manejo de errores en Go: Límites y patrones.

Dónde encajan los interruptores de circuito en la arquitectura de aplicaciones

El patrón de interruptor de circuito pertenece a los límites de integración.

En una aplicación Go, eso usualmente significa:

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

Mantén el interruptor fuera de la lógica de negocio cuando sea posible.

La capa de negocio debería entender errores de dominio como:

payment provider unavailable
recommendations unavailable
profile service timeout

No debería necesitar entender los estados de gobreaker.

Esta separación mantiene la arquitectura limpia:

  • las preocupaciones de transporte permanecen en los clientes
  • la política de resiliencia permanece cerca de las dependencias
  • la lógica de dominio permanece legible
  • los manejadores permanecen delgados
  • las pruebas son más fáciles de escribir

Este artículo es parte del tema Arquitectura de aplicaciones en producción — junto con guías de idempotencia, outbox, saga y orquestación en Patrones de Integración.

Errores comunes

Error 1: Sin timeout

Un interruptor de circuito no detiene mágicamente las llamadas lentas a menos que las llamadas devuelvan.

Si la operación saliente puede colgar para siempre, el interruptor puede no ver un fallo lo suficientemente rápido.

Usa siempre timeouts.

Error 2: Un interruptor global para todo

No uses un interruptor para todas las dependencias.

Un proveedor de correo electrónico con fallos no debería abrir el circuito para tu proveedor de pagos. Un endpoint de búsqueda lento no debería bloquear las llamadas al perfil de usuario.

Usa interruptores separados para operaciones de dependencia separadas cuando sus modos de fallo difieran.

Error 3: Contar errores del llamante como fallos de dependencia

Si tu servicio envía entrada mala y recibe 400 Bad Request, eso usualmente no es una interrupción aguas abajo.

No entrenes al interruptor con tus propios errores.

Error 4: Reintentar escrituras no idempotentes

Los reintentos no son gratis. Pueden duplicar escrituras, pagos, mensajes o efectos secundarios.

Haz que las escrituras sean idempotentes antes de reintentarlas.

Error 5: Ocultar interrupciones detrás de respuestas alternativas

Las respuestas alternativas deberían degradarse elegantemente, no falsificar la realidad.

Si una dependencia está caída, tus métricas y registros deberían hacer eso obvio.

Error 6: Ajustar sin datos de producción

Los umbrales copiados de ejemplos son solo puntos de partida.

Ajusta basado en:

  • volumen de solicitudes
  • tasa de error normal
  • latencia de dependencia
  • impacto en el usuario
  • tiempo de recuperación
  • calidad de la respuesta alternativa

Error 7: Usar interruptores de circuito en lugar de gestión de capacidad

Un interruptor de circuito no es un sustituto para:

  • descarte de carga (load shedding)
  • limitación de tasa
  • límites de cola
  • autoescalado
  • ajuste de base de datos
  • límites de pool de conexiones
  • cuotas de upstream

Es una parte de una estrategia de resiliencia.

Predeterminados prácticos

Para un servicio Go típico llamando a una dependencia HTTP interna, un punto de partida razonable podría ser:

Timeout del cliente HTTP: 2 a 5 segundos
Timeout de contexto por solicitud: basado en el SLA del llamante
Regla de fallo del interruptor: 5 fallos consecutivos o 50 por ciento de fallo después de 20 solicitudes
Timeout abierto: 10 a 30 segundos
Solicitudes semiabiertas: 1 a 5
Recuento de reintento: 1 a 3 intentos
Backoff de reintento: exponencial con jitter

Estos no son valores universales. Son puntos de inicio bastante seguros.

Para APIs orientadas al usuario, mantén los presupuestos de latencia total ajustados. Para trabajos en segundo plano, puedes tolerar esperas más largas. Para proveedores de pagos, sé mucho más cuidadoso con los reintentos y la idempotencia.

Lista de verificación del interruptor de circuito

Antes de agregar un interruptor de circuito, responde estas preguntas:

  • ¿Qué dependencia se está protegiendo?
  • ¿Qué operación se está protegiendo?
  • ¿Qué errores cuentan como fallo de dependencia?
  • ¿Qué errores deberían ser ignorados por el interruptor?
  • ¿Qué timeout aplica a cada llamada?
  • ¿Están permitidos los reintentos?
  • ¿Son idempotentes las escrituras?
  • ¿Qué sucede cuando el circuito está abierto?
  • ¿Hay una respuesta alternativa?
  • ¿Es visible la respuesta alternativa en las métricas?
  • ¿Quién recibe la alerta si el circuito sigue abriéndose?
  • ¿Cómo se ajustará el interruptor después del despliegue?

Si no puedes responder estas, agregar un interruptor puede crear más confusión que resiliencia.

Probando interruptores de circuito en Go

Prueba el comportamiento, no la máquina de estados interna de la biblioteca.

Las pruebas útiles incluyen:

  • la dependencia tiene éxito y la respuesta se devuelve
  • la dependencia falla repetidamente y el circuito se abre
  • el circuito abierto devuelve un error temporal
  • los errores de validación del lado del cliente no activan el interruptor
  • el timeout del contexto se respeta
  • la respuesta alternativa se devuelve cuando se espera
  • las métricas se emiten en los cambios de estado

Usa servidores HTTP falsos para pruebas de estilo integración:

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

Para pruebas unitarias, oculta la dependencia detrás de una interfaz e inyecta una implementación falsa.

Mantén las pruebas deterministas. Evita dormir durante duraciones reales largas. Configura timeouts de interruptor cortos en las pruebas. Para más información sobre pruebas de código Go concurrente con tiempo falso y burbujas aisladas, consulta Probando código Go concurrente con testing/synctest.

¿Deberías construir tu propio interruptor de circuito?

Construir un pequeño interruptor de circuito es un buen ejercicio de aprendizaje. Te ayuda a entender la máquina de estados.

Para código de producción, prefiere una biblioteca mantenida a menos que tus necesidades sean muy específicas.

Un interruptor de producción necesita manejar:

  • concurrencia
  • transiciones de estado
  • contadores
  • pruebas semiabiertas
  • callbacks
  • clasificación de fallos personalizada
  • comportamiento libre de carreras
  • manejo de errores predecible

Eso no es imposible, pero es fácil equivocarse sutilmente.

La biblioteca aburrida suele ser la mejor elección.

Conclusión

El patrón de interruptor de circuito no es polvo mágico de confiabilidad.

En Go, funciona mejor cuando es parte de una pila de resiliencia pequeña y explícita:

timeout de contexto
+ reintento con backoff y jitter
+ interruptor de circuito
+ respuesta alternativa
+ métricas

El patrón es más útil en los límites de las dependencias, especialmente alrededor de servicios remotos que pueden volverse lentos o parcialmente inaccesibles.

Úsalo para detener fallos en cascada. Úsalo para fallar rápido cuando una dependencia claramente no es saludable. Úsalo para dar a los sistemas sobrecargados espacio para recuperarse.

Pero no lo uses como excusa para ignorar timeouts, idempotencia, observabilidad o arquitectura limpia.

Un buen interruptor de circuito hace que el fallo sea más claro y más barato. Uno malo solo hace que el fallo sea más misterioso.

Referencias

Suscribirse

Recibe nuevas publicaciones sobre sistemas, infraestructura e ingeniería de IA.