Estratégias de injeção de falhas em APIs para validar resiliência

1. Fundamentos da Injeção de Falhas em APIs

1.1. O que é injeção de falhas e por que é crucial para resiliência

A injeção de falhas é uma técnica de engenharia de caos que consiste em introduzir intencionalmente erros, latências ou interrupções em um sistema para observar seu comportamento sob condições adversas. Em APIs, essa prática é essencial para validar se os mecanismos de resiliência — como circuit breakers, retries e timeouts — estão funcionando conforme o esperado. Sem a injeção controlada de falhas, equipes só descobrem vulnerabilidades quando incidentes reais ocorrem em produção.

1.2. Diferença entre testes de carga, testes de estresse e injeção de falhas

  • Testes de carga: avaliam o comportamento da API sob volumes esperados de tráfego, medindo throughput e latência.
  • Testes de estresse: levam a API além dos limites normais para identificar o ponto de ruptura.
  • Injeção de falhas: foca em cenários específicos de erro (ex.: um serviço dependente cai, latência sobe para 10s) e verifica se a API degrada graciosamente.

Enquanto os dois primeiros testam capacidade, a injeção de falhas testa comportamento sob falha.

1.3. Princípios de Chaos Engineering aplicados a APIs

  • Hipótese: definir o comportamento esperado (ex.: "se o banco de dados falhar, a API deve retornar erro 503 em vez de travar").
  • Blast radius controlado: iniciar com pequenos grupos de usuários ou endpoints de baixo risco.
  • Automação: experimentos devem ser repetíveis e integráveis ao pipeline de CI/CD.
  • Observabilidade: métricas e logs são coletados durante o experimento para validar a hipótese.

2. Tipos de Falhas Comuns em APIs e Como Simulá-las

2.1. Falhas de latência: atrasos artificiais em respostas HTTP

Atrasos simulam degradação de rede ou lentidão em serviços downstream.

# Usando Toxiproxy para adicionar 3 segundos de latência em um endpoint
toxiproxy-cli create users-api -l localhost:8080 -u backend:3000
toxiproxy-cli toxic add users-api --type latency --attribute latency=3000

2.2. Falhas de erro: retorno de códigos HTTP 4xx e 5xx

Simular erros HTTP permite testar como o cliente lida com falhas específicas.

# Usando Envoy com filtro de falha para retornar 503 em 50% das requisições
fault:
  abort:
    http_status: 503
    percentage: 50

2.3. Falhas de rede: perda de pacotes, desconexões e timeouts

Desconexões simulam problemas de infraestrutura como queda de servidores.

# Usando iptables para descartar pacotes em uma porta específica
iptables -A INPUT -p tcp --dport 8080 -j DROP
sleep 30
iptables -D INPUT -p tcp --dport 8080 -j DROP

3. Ferramentas e Frameworks para Injeção de Falhas

3.1. Uso de proxies e sidecars (Toxiproxy, Envoy)

Toxiproxy atua como proxy entre cliente e servidor, permitindo injetar latência, desconexões e limites de banda sem modificar o código da aplicação. Envoy, com seu filtro de fault injection, pode ser configurado via arquivos YAML para simular falhas em ambientes de service mesh.

3.2. Bibliotecas de injeção em nível de código (Resilience4J, Hystrix)

Essas bibliotecas permitem injetar falhas programaticamente em pontos específicos do código.

// Resilience4J - injetar falha em um endpoint específico
@GetMapping("/users")
@CircuitBreaker(name = "usersService", fallbackMethod = "fallback")
public List<User> getUsers() {
    // Simula falha em 30% das chamadas
    if (Math.random() < 0.3) {
        throw new RuntimeException("Falha simulada");
    }
    return userService.findAll();
}

3.3. Plataformas de Chaos Engineering (Chaos Monkey, Gremlin, Litmus)

  • Chaos Monkey: derruba instâncias aleatórias em produção para testar resiliência a falhas de nó.
  • Gremlin: oferece ataques pré-configurados (latência, packet loss, shutdown de containers) com interface web e controle de blast radius.
  • Litmus: focado em Kubernetes, permite injetar falhas em pods, nós e volumes de forma declarativa.

4. Estratégias de Injeção em APIs REST e GraphQL

4.1. Injeção em endpoints críticos e de alta frequência

Priorize endpoints que impactam diretamente a experiência do usuário: login, checkout, busca de produtos. Use técnicas de canary release para injetar falhas apenas em uma fração do tráfego.

4.2. Simulação de falhas em dependências externas

Bancos de dados, filas de mensagens e APIs de terceiros são pontos comuns de falha. Simule a queda do banco via Toxiproxy ou derrubando o container da dependência.

# Simulando falha no banco de dados PostgreSQL
kubectl run chaos-pod --image=alpine -- /bin/sh -c "apk add curl && curl -X POST http://litmus:8080/experiment -d '{\"target\": \"postgresql\", \"action\": \"kill\"}'"

4.3. Testes de degradação gradual vs. falhas catastróficas

  • Degradação gradual: aumentar latência em 100ms a cada minuto até atingir 5s. Testa se o sistema degrada graciosamente.
  • Falha catastrófica: derrubar o banco de dados subitamente. Testa se o circuit breaker abre e o fallback é acionado.

5. Padrões de Resiliência para Mitigar Falhas Injetadas

5.1. Circuit Breaker: abrindo e fechando o circuito sob falhas

O circuit breaker monitora a taxa de falhas e, ao ultrapassar um limiar (ex.: 50% de erros em 10 requisições), abre o circuito e retorna fallback imediato.

# Configuração do Circuit Breaker no Resilience4J
resilience4j.circuitbreaker:
  configs:
    default:
      slidingWindowSize: 10
      minimumNumberOfCalls: 5
      failureRateThreshold: 50
      waitDurationInOpenState: 10000

5.2. Retry com backoff exponencial e jitter

Retries automáticos com backoff exponencial evitam sobrecarregar o serviço downstream. Jitter adiciona aleatoriedade para evitar thundering herd.

# Configuração de retry no Resilience4J
resilience4j.retry:
  configs:
    default:
      maxAttempts: 3
      waitDuration: 500
      exponentialBackoffMultiplier: 2
      enableExponentialBackoff: true

5.3. Timeouts e bulkheads: isolamento de recursos por thread pool

Timeouts impedem que chamadas lentas consumam recursos indefinidamente. Bulkheads isolam pools de threads para que uma falha em um serviço não afete os demais.

# Configuração de bulkhead no Resilience4J
resilience4j.bulkhead:
  configs:
    default:
      maxConcurrentCalls: 10
      maxWaitDuration: 500

6. Métricas e Observabilidade Durante os Experimentos

6.1. Monitoramento de latência, taxa de erro e throughput em tempo real

Use Prometheus + Grafana para dashboards que mostram métricas antes, durante e após o experimento. Compare a latência p95 e a taxa de erro com a linha de base.

6.2. Rastreamento distribuído (tracing) para identificar gargalos

Ferramentas como Jaeger ou Zipkin permitem visualizar onde exatamente a falha ocorre na cadeia de chamadas. Se a latência injetada em um serviço A impacta o serviço B, o tracing mostrará o caminho completo.

6.3. Coleta de logs e métricas de resiliência

Exponha métricas específicas dos padrões de resiliência: taxa de circuit breaker aberto, número de retries, tempo médio de fallback.

# Métricas do Resilience4J expostas via Micrometer
circuit_breaker_state{name="usersService", state="open"} 1
resilience4j_retry_calls_total{name="usersService", kind="successful_with_retry"} 12

7. Planejamento e Execução Segura de Experimentos

7.1. Definição de hipóteses e blast radius controlado

Cada experimento deve ter uma hipótese clara (ex.: "A API continuará funcionando se o banco de dados ficar indisponível por 30 segundos"). O blast radius deve ser limitado: comece com um endpoint de baixa criticidade e 1% dos usuários.

7.2. Uso de feature flags e rollback automático

Feature flags permitem ativar/desativar a injeção de falhas sem deploy. Configure rollback automático se métricas críticas (ex.: latência p99 > 5s) forem violadas.

# Exemplo de feature flag com LaunchDarkly
if (ldClient.variation("inject-latency", user, false)) {
    Thread.sleep(2000); // injeta latência
}

7.3. Execução em ambientes de staging e canários antes da produção

Sempre execute experimentos primeiro em staging. Depois, promova para canários em produção (ex.: 1% do tráfego) antes de expandir. Documente cada experimento e compartilhe os resultados com o time.

Conclusão

A injeção de falhas em APIs é uma prática indispensável para construir sistemas resilientes. Ao simular latências, erros e desconexões de forma controlada, equipes podem validar se seus padrões de resiliência — circuit breakers, retries e bulkheads — funcionam sob pressão real. Com ferramentas como Toxiproxy, Resilience4J e plataformas de Chaos Engineering, é possível automatizar experimentos e integrá-los ao ciclo de desenvolvimento. Lembre-se: o objetivo não é quebrar o sistema, mas aprender como ele se comporta quando algo inevitavelmente falha.

Referências