MÓDULO 08 · CONCEITO 08 DE 14

Fault Injection

Tipos, blast radius e abort conditions — a mecânica de introduzir falhas controladas sem transformar experimento em incidente

Tempo de leitura ~20 min Pré-requisito 07 · Chaos tooling Próximo 09 · Game days & resilience drills

Fault injection é a técnica de introduzir falhas deliberadas em um sistema para observar como ele se comporta. É o mecanismo operacional do chaos engineering: onde os princípios definem o quê e o porquê, a fault injection define o como. A disciplina tem raízes na engenharia de hardware (FMEA — Failure Mode and Effects Analysis, padrão da indústria aeroespacial desde os anos 1940) e foi adaptada para software ao longo das décadas de 1990 e 2000, antes de ganhar a nomenclatura "chaos engineering" com o Netflix.

A diferença entre fault injection bem praticada e irresponsabilidade operacional é o grau de controle. Um engenheiro que desativa um serviço em produção sem hipótese definida, sem abort conditions, e sem rollback planejado não está fazendo fault injection — está criando um incidente. A fault injection responsável é cirúrgica: o tipo de falha é específico, o escopo é mínimo, a duração é definida, e o sistema de abort está configurado antes do experimento começar.

Taxonomia de falhas a injetar

As categorias de falha relevantes para sistemas distribuídos modernos podem ser organizadas em cinco grupos:

Falhas de latência: adicionar atraso artificial nas respostas de um serviço ou nas chamadas de rede. É a falha mais segura de injetar porque o sistema continua "funcionando" tecnicamente — apenas mais devagar. Revela problemas de timeout muito curto, falta de circuit breaker, e ausência de caching de fallback. Implementada com tc netem delay a nível de rede, ou com interceptors a nível de código.

Falhas de erro: fazer uma dependência retornar erros (HTTP 500, exception, connection refused) para uma fração das requisições. Revela se o sistema trata erros corretamente em vez de propagar exceções não tratadas. Mais agressivo que latência — pode disparar circuit breaker se a taxa for alta o suficiente.

Esgotamento de recurso: consumir CPU, memória, descritores de arquivo, ou conexões de banco até um percentual configurado. Revela memory leaks sob pressão, thread starvation, e problemas de connection pool não configurado com limite. Implementado com ferramentas como stress-ng no Linux.

Partição de rede: bloquear completamente a comunicação entre dois serviços por um período. Simula falha de switch, partição de data center, ou problema de DNS. É a falha mais agressiva — revela como o sistema se comporta quando uma dependência simplesmente desaparece sem aviso.

Skew de clock: avançar ou atrasar o relógio de um serviço por alguns segundos. Revela problemas de tokens JWT com validade baseada em timestamp, lock distribuído com lease time, e logs desordenados que dificultam diagnóstico. Menos comum, mas revelou problemas graves em sistemas que não eram testados para isso.

Tipo de falha Risco relativo O que revela Ferramenta típica
Latência adicional Baixo Timeouts, caching, circuit breaker tc netem, toxiproxy, code interceptors
Erro em % das chamadas Médio Error handling, fallback, retry WireMock, respx, toxiproxy
Esgotamento de recurso Médio-Alto Limites de pool, backpressure stress-ng, chaos toolkit actions
Partição de rede Alto Comportamento split-brain, timeouts, reconexão iptables, Pumba, Litmus network-loss
Skew de clock Médio JWT, leases distribuídos, ordering de logs faketime, settimeofday (root)

Níveis de injeção — onde introduzir a falha

A falha pode ser injetada em diferentes camadas da pilha, cada uma com trade-offs distintos:

Nível de código (código da aplicação): interceptors ou middleware que introduzem erro ou latência antes de cada chamada externa. A abordagem mais controlada — a injeção é configurável em runtime (via feature flag ou variável de ambiente), reproduzível, e auditável. Desvantagem: só testa o código da aplicação, não a rede real.

// Exemplo de fault injection a nível de código (C#)
public class FaultInjectionHandler : DelegatingHandler
{
    private readonly FaultConfig _config;

    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken ct)
    {
        // Injetar latência se configurado
        if (_config.LatencyMs > 0)
            await Task.Delay(_config.LatencyMs, ct);

        // Injetar erro em % das requisições
        if (_config.ErrorRate > 0 &&
            Random.Shared.NextDouble() < _config.ErrorRate)
            return new HttpResponseMessage(HttpStatusCode.ServiceUnavailable);

        return await base.SendAsync(request, ct);
    }
}

Nível de proxy (toxiproxy, Envoy): um proxy transparente entre o serviço e suas dependências que pode introduzir latência, cortar conexões, ou limitar largura de banda. Mais realista que injeção de código — a rede de fato é afetada. Toxiproxy (Shopify, open-source) é a ferramenta mais popular para isso: um proxy TCP configurável via API HTTP que suporta toxics (latency, bandwidth, slow_close, timeout, slicer, etc.).

Nível de rede (tc netem, iptables): usando ferramentas do kernel Linux, é possível introduzir latência (tc netem delay), perda de pacote (tc netem loss), ou bloquear tráfego (iptables DROP) para endereços específicos. É o nível mais realista — afeta a comunicação de rede real — mas também o mais perigoso: um comando errado pode isolar o servidor do resto da rede.

Nível de infraestrutura (Litmus, AWS FIS): terminar instâncias, drenar nós, deletar volumes. É o nível mais alto de abstração — você descreve o que quer (matar esse pod) e a ferramenta executa. Mais seguro que comandos de rede diretos porque tem rollback automático integrado.

Abort conditions — o safety net obrigatório

Abort conditions são as condições que devem disparar o rollback automático de um experimento antes do tempo planejado. São o mecanismo de segurança que converte um experimento que está saindo do controle em um incidente contido.

Toda abort condition tem duas partes: a métrica observada e o threshold que dispara o abort. As métricas mais comuns para abort:

regra de ouro

A abort condition deve ser mais conservadora que o SLO. Se o SLO é taxa de erro < 0.1%, a abort condition deve ser taxa de erro > 0.5% — não > 5%. O espaço entre abort condition e SLO é onde o experimento opera sem disparar rollback. Se o abort é muito próximo do SLO, o experimento aborta constantemente e nunca coleta dados. Se o abort está muito longe, o experimento pode violar o SLO antes de abortar.

Toxiproxy — fault injection via proxy

Toxiproxy (github.com/Shopify/toxiproxy) merece atenção especial por ser o ponto de equilíbrio entre realismo e controlabilidade. Criado pela Shopify para testar resiliência de seus serviços antes do Black Friday, o Toxiproxy é um proxy TCP que pode injetar falhas configuráveis via API HTTP — sem modificar o código da aplicação.

# Configurar Toxiproxy
# 1. Criar um proxy (redirecionar tráfego pelo toxiproxy)
curl -X POST http://localhost:8474/proxies \
  -H "Content-Type: application/json" \
  -d '{
    "name": "inventory-service",
    "listen": "0.0.0.0:5432",
    "upstream": "inventory-service:5432",
    "enabled": true
  }'

# 2. Adicionar toxic de latência
curl -X POST \
  http://localhost:8474/proxies/inventory-service/toxics \
  -H "Content-Type: application/json" \
  -d '{
    "name": "latency-500ms",
    "type": "latency",
    "attributes": {"latency": 500, "jitter": 50}
  }'

# 3. Após o experimento — remover o toxic
curl -X DELETE \
  http://localhost:8474/proxies/inventory-service/toxics/latency-500ms

A vantagem do Toxiproxy é que ele pode ser configurado e reconfigurado em runtime sem reiniciar a aplicação — perfeito para experimentos dinâmicos e para uso em testes de integração automatizados.

C# — Fault injection via feature flag em ambiente de teste
public class FaultInjectionMiddleware
{
    private readonly RequestDelegate _next;
    private readonly IFeatureClient _flags;

    public FaultInjectionMiddleware(
        RequestDelegate next, IFeatureClient flags)
    {
        _next = next;
        _flags = flags;
    }

    public async Task InvokeAsync(HttpContext ctx)
    {
        // Apenas em ambientes não-produção ou quando flag ativo
        var injectFault = await _flags.GetBooleanValueAsync(
            "chaos.fault_injection_enabled", false);

        if (injectFault)
        {
            var latency = await _flags.GetIntegerValueAsync(
                "chaos.latency_ms", 0);
            if (latency > 0)
                await Task.Delay(latency, ctx.RequestAborted);

            var errorRate = await _flags.GetDoubleValueAsync(
                "chaos.error_rate", 0.0);
            if (errorRate > 0 && Random.Shared.NextDouble() < errorRate)
            {
                ctx.Response.StatusCode = 503;
                await ctx.Response.WriteAsJsonAsync(new {
                    error = "fault_injected",
                    message = "Erro simulado para experimento de chaos"
                });
                return;
            }
        }
        await _next(ctx);
    }
}

Fault injection via feature flag permite ativar e desativar injeção sem deploy. Em staging, o flag pode estar habilitado por default; em produção, requer aprovação explícita. Combinação de latência + taxa de erro cobre os dois tipos de falha mais comuns.

Python — Fault injector para chamadas HTTP externas
import asyncio
import random
from dataclasses import dataclass, field
from typing import Callable


@dataclass
class FaultConfig:
    latency_ms: int = 0
    error_rate: float = 0.0
    error_status: int = 503
    enabled: bool = False


class FaultInjector:
    """Injetor de falhas configurável para dependências externas."""

    def __init__(self, config: FaultConfig):
        self.config = config

    async def wrap(self, coro_func: Callable, *args, **kwargs):
        if not self.config.enabled:
            return await coro_func(*args, **kwargs)

        if self.config.latency_ms > 0:
            await asyncio.sleep(self.config.latency_ms / 1000)

        if (self.config.error_rate > 0 and
                random.random() < self.config.error_rate):
            raise FaultInjectedError(
                f"Erro simulado (status {self.config.error_status})",
                status=self.config.error_status
            )
        return await coro_func(*args, **kwargs)


class FaultInjectedError(Exception):
    def __init__(self, message: str, status: int):
        super().__init__(message)
        self.status = status


# Uso em teste de integração
async def test_checkout_with_inventory_fault():
    injector = FaultInjector(FaultConfig(
        latency_ms=500, error_rate=0.1, enabled=True
    ))
    service = CheckoutService(inventory_injector=injector)
    result = await service.place_order(order)
    assert result.status in ("completed", "degraded")
    assert result.order_id is not None

O injetor é configurável por dependência e habilitado/desabilitado por flag. Pode ser injetado via DI em testes de integração sem modificar a lógica de negócio.

Go — Fault injection com Transport customizado
type FaultConfig struct {
    LatencyMs int64
    ErrorRate float64 // 0.0-1.0
    Enabled   bool
}

// FaultTransport wraps http.RoundTripper com fault injection
type FaultTransport struct {
    Wrapped http.RoundTripper
    Config  *FaultConfig
    mu      sync.RWMutex
}

func (t *FaultTransport) RoundTrip(r *http.Request) (*http.Response, error) {
    t.mu.RLock()
    cfg := *t.Config
    t.mu.RUnlock()

    if !cfg.Enabled {
        return t.Wrapped.RoundTrip(r)
    }
    if cfg.LatencyMs > 0 {
        select {
        case <-time.After(time.Duration(cfg.LatencyMs) * time.Millisecond):
        case <-r.Context().Done():
            return nil, r.Context().Err()
        }
    }
    if cfg.ErrorRate > 0 && rand.Float64() < cfg.ErrorRate {
        return &http.Response{
            StatusCode: http.StatusServiceUnavailable,
            Body:       io.NopCloser(strings.NewReader(`{"error":"fault_injected"}`)),
            Header:     make(http.Header),
        }, nil
    }
    return t.Wrapped.RoundTrip(r)
}

// Uso
transport := &FaultTransport{
    Wrapped: http.DefaultTransport,
    Config:  &FaultConfig{LatencyMs: 500, ErrorRate: 0.1, Enabled: true},
}
client := &http.Client{Transport: transport}

FaultTransport implementa http.RoundTripper — pode ser usado com qualquer client HTTP sem modificar o código de negócio. O sync.RWMutex permite reconfigurar em runtime de forma thread-safe.

Fault injection vs teste de stress

Uma confusão frequente é entre fault injection e teste de stress. Teste de stress (módulo 06) aplica carga crescente para encontrar limites de throughput — o sistema é sobrecarregado com volume de trabalho. Fault injection aplica falhas qualitativas em volume normal — o sistema opera com carga normal, mas com componentes danificados ou degradados.

São complementares: um sistema pode passar em stress test (aguenta 10.000 req/s) mas falhar em fault injection (não aguenta 500 req/s quando o serviço de inventário está com latência de 1 segundo). O primeiro testa capacidade; o segundo testa resiliência.

Como praticar

  1. Implementar um FaultTransport ou middleware injetável. Para o serviço do projeto do módulo (ou qualquer serviço existente), implemente um ponto de injeção de falha configurável: latência e taxa de erro ajustáveis via variável de ambiente ou feature flag. Confirme que é possível ativar e desativar sem restart. Teste com latência de 200ms e 50ms de jitter — observe o impacto no P99.
  2. Configurar o Toxiproxy para uma dependência real. Instale o Toxiproxy localmente (Docker: docker run -p 8474:8474 shopify/toxiproxy). Configure um proxy para redirecionar tráfego de uma dependência do seu serviço. Adicione o toxic de latência. Execute o serviço apontando para o proxy e observe o comportamento. Depois adicione o toxic de connection_reset e observe.
  3. Definir abort conditions para um experimento. Para o serviço instrumentado com Prometheus/OpenTelemetry, escreva as regras de abort conditions como alertas: "se taxa de erro > 1% por 30s, disparar alerta de abort". Simule a condição de abort (forçando erro acima do threshold) e verifique que o alerta dispara no tempo esperado. Isso calibra o sistema de segurança antes de rodar experimentos reais.

Referências para aprofundar

  1. artigo Toxiproxy — A Framework for Simulating Network Conditions — Shopify Engineering Blog. O artigo original descrevendo o Toxiproxy — motivação, design, e como a Shopify usa para testar resiliência antes do Black Friday.
  2. docs Toxiproxy — GitHub README — github.com/Shopify/toxiproxy. Documentação completa de todos os toxics disponíveis: latency, bandwidth, slow_close, timeout, slicer. Inclui exemplos de API e SDKs para Ruby, Go, Python.
  3. artigo tc-netem — Network Emulation — Linux man page. Documentação do tc netem — a ferramenta de kernel para emulação de rede (latência, perda de pacote, jitter, corrupção). Referência para injeção a nível de rede.
  4. livro Chaos Engineering — Casey Rosenthal et al. (O'Reilly, 2020). Capítulo 9 cobre fault injection em detalhe — tipos de falha, camadas de injeção, e casos reais do Netflix e Amazon.
  5. artigo Fault Injection Testing at Microsoft — Azure Architecture Center. learn.microsoft.com/azure/architecture/reliability/testing-faults. Guia prático de fault injection para sistemas Azure — abordagem sistemática por tipo de falha.
  6. docs AWS FIS — Fault Types Reference — AWS Documentation. Catálogo completo de tipos de falha disponíveis no AWS FIS — EC2, ECS, EKS, RDS, Route 53, e networking.
  7. artigo Failure Mode and Effects Analysis (FMEA) — ASQ.org. Origem do método FMEA na engenharia aeroespacial — o ancestral da fault injection moderna. Contexto histórico útil.
  8. vídeo Fault Injection at Netflix — SREcon Americas, 2019. YouTube. Como o Netflix evoluiu de Chaos Monkey para fault injection sistemática — tipos de falha, ferramentas internas, e integração com CI/CD.
  9. artigo Pumba — Chaos Testing Tool for Docker — GitHub. github.com/alexei-led/pumba. Fault injection para containers Docker — netem delay/loss/corrupt, kill, pause. Simples de usar em ambiente local.
  10. docs Envoy Fault Filter — envoyproxy.io/docs. Documentação do fault filter do Envoy proxy — injeção de latência e erro a nível de proxy, sem modificar código da aplicação. Útil em service meshes.
  11. paper Simple Testing Can Prevent Most Critical Failures — Yuan et al. (OSDI, 2014). Motivação empírica para fault injection — análise de 198 falhas mostrando que a maioria era detectável com testes simples.
  12. artigo Istio Fault Injection — istio.io/docs. Como injetar falhas em service meshes Istio via VirtualService — abordagem declarativa sem modificar a aplicação.