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:
- Taxa de erro acima de X% (tipicamente 2× o SLO)
- P99 de latência acima de Yms (tipicamente 2× o SLO)
- Número de alertas PagerDuty disparados
- Throughput caindo abaixo de Z req/s
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.
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.
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.
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
- 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.
- 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. - 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
- artigo Toxiproxy — A Framework for Simulating Network Conditions — Shopify Engineering Blog.
- docs Toxiproxy — GitHub README — github.com/Shopify/toxiproxy.
- artigo tc-netem — Network Emulation — Linux man page.
- livro Chaos Engineering — Casey Rosenthal et al. (O'Reilly, 2020).
- artigo Fault Injection Testing at Microsoft — Azure Architecture Center.
- docs AWS FIS — Fault Types Reference — AWS Documentation.
- artigo Failure Mode and Effects Analysis (FMEA) — ASQ.org.
- vídeo Fault Injection at Netflix — SREcon Americas, 2019.
- artigo Pumba — Chaos Testing Tool for Docker — GitHub.
- docs Envoy Fault Filter — envoyproxy.io/docs.
- paper Simple Testing Can Prevent Most Critical Failures — Yuan et al. (OSDI, 2014).
- artigo Istio Fault Injection — istio.io/docs.