Seguranca e resiliencia

Circuit Breakers e Padrões de Resiliência: Projetando Sistemas Distribuídos que Sobrevivem a Falhas

Como circuit breakers, retries com exponential backoff e outros padrões de resiliência previnem falhas em cascata e garantem confiabilidade em arquiteturas distribuídas.

10/03/20266 min de leituraSeguranca
Circuit Breakers e Padrões de Resiliência: Projetando Sistemas Distribuídos que Sobrevivem a Falhas

Resumo executivo

Como circuit breakers, retries com exponential backoff e outros padrões de resiliência previnem falhas em cascata e garantem confiabilidade em arquiteturas distribuídas.

Ultima atualizacao: 10/03/2026

A realidade das falhas distribuídas

Em aplicações monolíticas, os modos de falha são relativamente previsíveis: o servidor responde ou não responde. Em arquiteturas distribuídas construídas sobre microsserviços, APIs, serviços terceirizados e infraestrutura cloud, a falha se torna o estado padrão. Partições de rede ocorrem, dependências ficam lentas, bancos de dados sofrem timeout e serviços experimentam picos inesperados de carga.

O desafio fundamental: quando um componente degrada, como evitar que essa degradação se propague por todo o sistema e cause uma interrupção completa?

Padrões de resiliência abordam esse problema projetando sistemas que lidam, isolam e se recuperam de falhas de forma elegante. Circuit breakers atuam como o padrão fundamental, mas são mais efetivos quando combinados com retries com exponential backoff, timeouts, bulkheads e fallbacks. Para times de engenharia operando sistemas distribuídos em escala, implementar esses padrões não é opcional—é essencial para sobrevivência.

Circuit Breakers: Previndo falhas em cascata

Um circuit breaker monitora chamadas a serviços externos e abre o circuito quando a taxa de falhas excede um limite. Uma vez aberto, chamadas subsequentes falham imediatamente sem tentar acessar o serviço remoto, permitindo que ele se recupere enquanto previne que seu sistema desperdice recursos em requisições condenadas ao fracasso.

Os três estados de um circuit breaker

Estado Fechado (Operação Normal)

  • Todas as requisições passam para o serviço
  • Falhas são rastreadas contra limites
  • Quando o limite de falhas é excedido, circuito transiciona para aberto

Estado Aberto (Modo Fail-Fast)

  • Todas as requisições falham imediatamente sem atingir o serviço
  • Um timeout decorre antes de tentar fechar o circuito
  • Previne sobrecarregar um serviço em dificuldade

Estado Meio-Aberto (Teste)

  • Uma única requisição é permitida para testar se o serviço se recuperou
  • Se bem-sucedida, circuito fecha; se falhar, reabre
  • Provê mecanismo de recuperação controlada

Padrões de implementação

typescriptclass CircuitBreaker {
  constructor(private options: CircuitBreakerOptions) {
    this.state = 'closed';
    this.failures = 0;
    this.lastFailureTime = 0;
    this.successCount = 0;
  }

  async execute<T>(fn: () => Promise<T>): Promise<T> {
    if (this.state === 'open') {
      if (this.shouldAttemptReset()) {
        this.state = 'half-open';
      } else {
        throw new CircuitBreakerOpenError('Circuit is open');
      }
    }

    try {
      const result = await fn();
      this.onSuccess();
      return result;
    } catch (error) {
      this.onFailure();
      throw error;
    }
  }

  private onSuccess() {
    if (this.state === 'half-open') {
      this.state = 'closed';
      this.successCount = 0;
    } else {
      this.failures = 0;
    }
  }

  private onFailure() {
    this.failures++;
    this.lastFailureTime = Date.now();

    if (this.failures >= this.options.failureThreshold) {
      this.state = 'open';
      this.successCount = 0;
    }
  }

  private shouldAttemptReset(): boolean {
    const cooldownPeriod = this.options.openTimeoutMs;
    return Date.now() - this.lastFailureTime > cooldownPeriod;
  }
}

Parâmetros críticos de configuração

ParâmetroPropósitoRisco de Configuração Incorreta
Limite de FalhasNúmero de falhas antes de abrir circuitoMuito baixo: abre em falhas transitórias. Muito alto: permite falhas em cascata.
Janela de TimeoutJanela de tempo para contar falhasMuito curta: sensível a variância normal. Muito longa: lento para detectar degradação.
Timeout de AberturaQuanto tempo circuito fica aberto antes de testarMuito curto: inunda serviço em recuperação. Muito longo: interrupções prolongadas desnecessárias.
Max Requisições Meio-AbertoRequisições permitidas durante estado meio-abertoMuitas: inunda serviço durante recuperação. Poucas: pode perder recuperação bem-sucedida.

Retries com Exponential Backoff: Lidando com falhas transitórias

Nem toda falha justifica abrir um circuito. Falhas transitórias—soluços de rede, timeouts breves de banco, indisponibilidade temporária de serviço—frequentemente se resolvem sozinhas. O padrão de retry tenta operações que falharam com delays cuidadosamente calculados.

Por que retries ingênuos são perigosos

Uma estratégia de retry ingênua que imediatamente retenta requisições falhadas pode exacerbá-lo:

typescript// PERIGOSO: Retries imediatos criam thundering herd
async function naiveRetry(fn: () => Promise<any>, maxRetries: number) {
  let attempts = 0;
  while (attempts < maxRetries) {
    try {
      return await fn();
    } catch (error) {
      attempts++;
      if (attempts >= maxRetries) throw error;
      // Sem delay: retry imediato inunda serviço
    }
  }
}

Quando múltiplos clientes retentam simultaneamente, eles criam um thundering herd que sobrecarrega o serviço já em dificuldade, transformando um problema transitório em uma interrupção sustentada.

Exponential backoff com jitter

typescriptasync function retryWithBackoff<T>(
  fn: () => Promise<T>,
  options: RetryOptions
): Promise<T> {
  let attempt = 0;
  let lastError: Error;

  while (attempt < options.maxRetries) {
    try {
      return await fn();
    } catch (error) {
      lastError = error as Error;
      attempt++;

      if (attempt >= options.maxRetries) {
        throw lastError;
      }

      // Exponential backoff: delay cresce exponencialmente
      const baseDelay = options.initialDelayMs;
      const exponentialDelay = baseDelay * Math.pow(2, attempt - 1);

      // Adiciona jitter para prevenir retries sincronizados
      const jitter = exponentialDelay * options.jitterFactor;
      const randomizedDelay = exponentialDelay + (Math.random() * jitter);

      await sleep(randomizedDelay);
    }
  }

  throw lastError!;
}

interface RetryOptions {
  maxRetries: number;
  initialDelayMs: number;
  jitterFactor: number; // Tipicamente 0.1 a 0.5
}

Por que jitter importa: Sem jitter, todos os clientes experimentando a mesma falha vão retentar nos mesmos intervalos aproximados após exponential backoff, criando ondas sincronizadas de carga. Jitter randomiza esses delays levemente, espalhando retries mais uniformemente.

Quando retry vs. quando falhar rápido

Tipo de FalhaEstratégia de RetryRacional
Timeout de redeRetry com backoffProvavelmente transitório, condições de rede flutuam
Erros 5xx do servidorRetry com backoffServiço pode estar temporariamente sobrecarregado
429 rate limitedRetry com exponential backoffAguardar janela de rate limit resetar
404 not foundNão retryRecurso não existe, retry não ajuda
Erros 4xx (400, 401, 403)Não retryErro do cliente, retry não corrige
Erros 500 (erros de lógica)Retries limitadosPode ser bug, retry pode funcionar com input diferente

Bulkheads: Isolando impacto de falhas

O padrão bulkhead particiona recursos para que falhas em um domínio não consumam todos os recursos disponíveis. Nomeado após compartimentos estanques em navios, bulkheads previnem que um único ponto de falha afunde o sistema inteiro.

Isolamento de thread pool

typescriptclass BulkheadExecutor {
  constructor(private threadPoolSize: number) {
    this.threadPool = new WorkerPool(threadPoolSize);
  }

  async execute<T>(task: () => Promise<T>): Promise<T> {
    if (this.threadPool.availableWorkers === 0) {
      throw new BulkheadExhaustedError('Bulkhead capacity exhausted');
    }

    const worker = this.threadPool.acquire();
    try {
      return await task();
    } finally {
      this.threadPool.release(worker);
    }
  }
}

// Bulkheads separados para diferentes domínios
const paymentBulkhead = new BulkheadExecutor(10);
const inventoryBulkhead = new BulkheadExecutor(20);
const notificationBulkhead = new BulkheadExecutor(5);

Benefício operacional: Se o serviço de pagamento ficar lento e esgotar seu thread pool, serviços de inventário e notificação continuam operando porque têm pools isolados.

Rate limiting baseado em semáforo

typescriptclass SemaphoreBulkhead {
  constructor(private maxConcurrent: number) {
    this.semaphore = new Semaphore(maxConcurrent);
  }

  async execute<T>(fn: () => Promise<T>): Promise<T> {
    const permit = await this.semaphore.acquire();
    try {
      return await fn();
    } finally {
      this.semaphore.release(permit);
    }
  }
}

Estratégias de Timeout: Previndo esgotamento de recursos

Toda chamada externa deve ter um timeout. Sem timeouts, serviços lentos causam acumulação de conexões em seu sistema, esgotam thread pools e eventualmente falham completamente.

Timeouts por operação

typescriptasync function callWithTimeout<T>(
  fn: () => Promise<T>,
  timeoutMs: number,
  operationName: string
): Promise<T> {
  const timeoutPromise = new Promise<never>((_, reject) => {
    setTimeout(() => {
      reject(new TimeoutError(`Operação ${operationName} excedeu timeout após ${timeoutMs}ms`));
    }, timeoutMs);
  });

  try {
    return await Promise.race([fn(), timeoutPromise]);
  } catch (error) {
    if (error instanceof TimeoutError) {
      // Log timeout com contexto
      metrics.recordTimeout(operationName, timeoutMs);
    }
    throw error;
  }
}

Estratégias de timeout em camadas

Diferentes operações justificam diferentes durações de timeout baseadas em sua complexidade e importância:

typescriptconst timeoutConfig = {
  // Operações rápidas: timeouts estritos
  healthCheck: 500,
  cacheLookup: 100,
  apiValidation: 200,

  // Operações médias: timeouts balanceados
  apiCall: 3000,
  databaseQuery: 2000,
  messageQueuePublish: 1000,

  // Operações pesadas: timeouts generosos com circuit breaker
  dataProcessingJob: 30000,
  reportGeneration: 60000,
  bulkImport: 120000,
};

Fallbacks: Degradação elegante quando serviços falham

Circuit breakers previnem falhas em cascata, mas fallbacks fornecem comportamento alternativo quando serviços estão indisponíveis. Fallbacks mantêm funcionalidade mesmo se recursos estão degradados.

Estratégias de fallback por criticidade de serviço

typescriptclass FallbackHandler {
  async executeWithFallback<T>(
    primaryFn: () => Promise<T>,
    fallbackFn: () => Promise<T>,
    service: string
  ): Promise<T> {
    try {
      return await primaryFn();
    } catch (error) {
      metrics.recordFallback(service, error);
      return await fallbackFn();
    }
  }
}

// Exemplos de fallback
const fallbacks = {
  // Fallback de cache: servir dados obsoletos
  productCatalog: async (productId: string) => {
    return await cache.get(`product:${productId}`) ||
           await database.getProduct(productId);
  },

  // Feature toggle: desabilitar recursos não-críticos
  recommendations: async (userId: string) => {
    return await recommendationsService.getForUser(userId) ||
           { items: [], source: 'fallback-disabled' };
  },

  // Serviço alternativo: mudar para provedor backup
  emailDelivery: async (email: Email) => {
    try {
      return await primaryEmailProvider.send(email);
    } catch (error) {
      return await backupEmailProvider.send(email);
    }
  },

  // Mensagem de erro elegante: informar usuário de problema temporário
  paymentProcessing: async (payment: Payment) => {
    try {
      return await paymentProcessor.charge(payment);
    } catch (error) {
      return {
        status: 'temporarily_unavailable',
        message: 'Processamento de pagamento temporariamente indisponível. Por favor, tente novamente em alguns minutos.',
        retryAfter: 300 // 5 minutos
      };
    }
  }
};

Monitoramento operacional: Observando resiliência em ação

Padrões de resiliência só são efetivos se você puder observar quando eles são acionados. Sem monitoramento, você não saberá se seus circuit breakers estão abrindo com muita frequência ou se retries estão mascarando problemas mais profundos.

Métricas-chave para rastrear

typescriptinterface ResilienceMetrics {
  // Métricas de circuit breaker
  circuitBreakerStateChanges: {
    service: string;
    fromState: 'closed' | 'open' | 'half-open';
    toState: 'closed' | 'open' | 'half-open';
    timestamp: Date;
  }[];

  // Métricas de retry
  retryAttempts: {
    service: string;
    attempt: number;
    totalAttempts: number;
    success: boolean;
    delayMs: number;
  }[];

  // Métricas de timeout
  timeoutOccurrences: {
    operation: string;
    timeoutMs: number;
    actualDurationMs: number;
  }[];

  // Métricas de fallback
  fallbackInvocations: {
    service: string;
    fallbackType: 'cache' | 'alternative' | 'disabled';
    latencyMs: number;
  }[];

  // Métricas de bulkhead
  bulkheadRejections: {
    bulkhead: string;
    rejectedRequests: number;
    availableCapacity: number;
  }[];
}

Estratégia de alertas

yamlresilience_alerts:
  circuit_breaker_open:
    condition: "CircuitBreakerOpen > 3 em 5min para mesmo serviço"
    severity: critico
    action: "Investigação imediata: serviço pode estar down ou degradado"

  high_retry_rate:
    condition: "RetryRate > 50% para operação"
    severity: aviso
    action: "Revisar performance do serviço: pode indicar instabilidade"

  timeout_spike:
    condition: "TimeoutRate > 10% aumento da baseline"
    severity: aviso
    action: "Verificar regressão de performance ou problemas de rede"

  fallback_activation:
    condition: "FallbackRate > 20% para serviço crítico"
    severity: aviso
    action: "Serviço degradado: dependência primária indisponível"

Anti-padrões de implementação

Anti-padrão 1: Circuit breakers sem monitoramento

Configurar circuit breakers mas não rastrear quando abrem significa que você não detectará serviços degradados até usuários reportarem problemas.

Solução: Implementar métricas e alertas abrangentes para todos os padrões de resiliência.

Anti-padrão 2: Retries excessivos

Retentar operações não-idempotentes (como cobrar cartão de crédito) pode causar transações duplicadas e corrupção de dados.

Solução: Classificar operações como idempotentes ou não-idempotentes. Retentar apenas operações idempotentes automaticamente.

Anti-padrão 3: Configuração tamanho-único

Usar mesmas configurações de retry e timeout para todos os serviços ignora suas características de performance distintas.

Solução: Configurar parâmetros de resiliência por serviço baseados em SLAs observados e modos de falha.

Anti-padrão 4: Fallbacks que escondem problemas

Fallbacks que sempre têm sucesso mascaram problemas subjacentes, prevenindo análise de causa raiz.

Solução: Projetar fallbacks para degradar funcionalidade visivelmente, e alertar quando ativados.

Chaos Engineering: Testando resiliência proativamente

Construir padrões de resiliência é o primeiro passo. Validar que funcionam requer induzir falhas proativamente em ambientes similares à produção.

Testando circuit breakers

typescriptasync function testCircuitBreaker() {
  const circuitBreaker = new CircuitBreaker({
    failureThreshold: 3,
    timeoutMs: 10000
  });

  // Simular falhas para acionar circuit breaker
  const failingService = async () => {
    throw new Error('Service unavailable');
  };

  for (let i = 0; i < 5; i++) {
    try {
      await circuitBreaker.execute(failingService);
    } catch (error) {
      console.log(`Tentativa ${i + 1}: ${error.message}`);
    }
  }

  // Circuito deve estar aberto após 3 falhas
  assert(circuitBreaker.state === 'open', 'Circuito deve estar aberto');
}

Testando estratégias de retry

typescriptasync function testExponentialBackoff() {
  const attempts: number[] = [];

  const flakyService = async () => {
    attempts.push(attempts.length + 1);
    if (attempts.length < 3) {
      throw new Error('Temporary failure');
    }
    return 'success';
  };

  const result = await retryWithBackoff(flakyService, {
    maxRetries: 5,
    initialDelayMs: 100,
    jitterFactor: 0.1
  });

  assert(attempts.length === 3, 'Deve ter retentado duas vezes');
  assert(result === 'success', 'Deve eventualmente ter sucesso');
}

Conclusão

Padrões de resiliência transformam sistemas distribuídos de redes frágeis de dependências em arquiteturas robustas que sobrevivem falhas elegantemente. Circuit breakers previnem falhas em cascata, retries com exponential backoff lidam com problemas transitórios, bulkheads isolam falhas, timeouts previnem esgotamento de recursos e fallbacks mantêm funcionalidade degradada.

O insight-chave: falha não é questão de se, mas quando. Projetar para falha não é pessimismo—é pragmatismo. Ao implementar esses padrões e observá-los através de monitoramento abrangente, times de engenharia podem construir sistemas que não apenas resistem falhas mas se recuperam delas automaticamente.

O próximo passo não é implementar todos os padrões de resiliência simultaneamente. Comece com os padrões de maior impacto para sua arquitetura: circuit breakers para dependências críticas, timeouts sensatos para todas as chamadas externas e exponential backoff para operações idempotentes. Monitore essas implementações, valide que funcionam através de chaos engineering, e expanda sua estratégia de resiliência iterativamente.


Sua arquitetura distribuída está experimentando falhas em cascata e interrupções inesperadas? Fale com especialistas em engenharia da Imperialis para projetar e implementar uma estratégia abrangente de resiliência que previne falhas de impactar seus clientes.

Fontes

Leituras relacionadas