Sistemas distribuídos introduzem falhas que não existem em aplicações monolíticas: redes partem, mensagens chegam fora de ordem, serviços ficam lentos sem parar completamente, relógios divergem. Este guia cobre os conceitos fundamentais para construir sistemas resilientes.
CAP Theorem e PACELC
O CAP Theorem (Brewer, 2000) afirma que um sistema distribuído pode garantir apenas dois dos três em caso de partição de rede:
- C — Consistency: todas as leituras retornam o dado mais recente
- A — Availability: toda requisição recebe uma resposta (sem garantia de ser a mais recente)
- P — Partition Tolerance: o sistema continua funcionando mesmo com falha de comunicação entre nós
Como partições de rede são inevitáveis em produção, a escolha real é entre CP (consistência) e AP (disponibilidade) quando uma partição ocorre.
Exemplos de sistemas reais:
CP (priorizam consistência):
├── HBase, Zookeeper, etcd, Consul
├── PostgreSQL em cluster com quórum
└── MongoDB com write concern majority
AP (priorizam disponibilidade):
├── Cassandra, DynamoDB (por padrão)
├── CouchDB
└── RiakPACELC estende o CAP para o caso SEM partição: mesmo quando a rede está sã, há trade-off entre Latência (L) e Consistência (C).
PACELC: se Partição → escolha entre A e C
senão (Else) → escolha entre L e C
Exemplo prático:
- DynamoDB: PA/EL (AP em partição, baixa latência em operação normal)
- PostgreSQL: PC/EC (CP em partição, consistência forte em operação normal)
- Cassandra: PA/EL com tunagem: você escolhe o trade-off por operação (consistency level)Consistência Eventual vs Forte
Consistência forte: após uma escrita, qualquer leitura subsequente em qualquer nó retorna o valor escrito. Custo: latência alta, disponibilidade reduzida.
Consistência eventual: após uma escrita, os nós convergirão para o mesmo valor eventualmente. Custo: leituras podem retornar dados desatualizados por um período.
// Exemplo de impacto no código — carrinho de compras com Cassandra (eventual consistency)
// ❌ Assumindo consistência forte onde não existe
public class CartController {
@PostMapping("/cart/{id}/add")
public ResponseEntity<Cart> addItem(@PathVariable String id, @RequestBody CartItem item) {
cartRepository.addItem(id, item);
Cart updated = cartRepository.findById(id); // pode retornar versão antiga!
return ResponseEntity.ok(updated);
}
}
// ✅ Read-your-writes: retorna o que você acabou de escrever, sem re-ler
public class CartController {
@PostMapping("/cart/{id}/add")
public ResponseEntity<Cart> addItem(@PathVariable String id, @RequestBody CartItem item) {
Cart updated = cartService.addItem(id, item); // retorna o estado atualizado localmente
return ResponseEntity.ok(updated);
}
}
// ✅ Ou force leitura com quórum quando necessário (ex: checkout)
public Cart getCartForCheckout(String cartId) {
return cartRepository.findWithConsistencyLevel(cartId, ConsistencyLevel.QUORUM);
// mais lento, mas garantido atualizado
}Two-Phase Commit (2PC) — Problemas e Alternativas
O 2PC é o protocolo clássico para transações distribuídas: fase 1 (prepare/vote) e fase 2 (commit/rollback). Na prática, tem problemas sérios.
2PC — por que evitar em microsserviços:
Fase 1 (Prepare): Fase 2 (Commit):
┌──────────────────┐ ┌──────────────────┐
│ Coordenador │ │ Coordenador │
│ → "prepare?" │ │ → "commit!" │
│ │ │ │
│ Participante A │ │ Participante A │
│ ← "ready" │ │ ← "committed" │
│ │ │ │
│ Participante B │ │ Participante B │
│ ← "ready" │ │ ← ??? (falhou) │
└──────────────────┘ └──────────────────┘
Problemas:
1. Bloqueio: participantes ficam bloqueados esperando coordenador
2. Single point of failure: coordenador cai após fase 1 → participantes ficam em dúvida indefinidamente
3. Latência: dois round-trips em rede para cada transação
4. Não escala: aumentar participantes aumenta janela de bloqueioAlternativas preferidas:
- Saga Pattern (veja Event-Driven Design) — transações compensatórias via eventos
- Outbox Pattern — atomicidade via banco + relay assíncrono
- Eventual consistency — aceitar que nem tudo precisa ser atômico entre serviços
Idempotência — Implementação com Idempotency Keys
// IMPLEMENTAÇÃO COMPLETA — Idempotency Key em API REST
@Entity
@Table(name = "idempotency_keys",
uniqueConstraints = @UniqueConstraint(columnNames = {"idempotency_key", "endpoint"}))
public class IdempotencyRecord {
@Id
private String id = UUID.randomUUID().toString();
private String idempotencyKey;
private String endpoint;
private int responseStatus;
private String responseBody; // JSON da resposta original
private Instant createdAt;
private Instant expiresAt; // limpeza após N horas
}
@Service
public class IdempotencyService {
private final IdempotencyRepository repo;
public Optional<StoredResponse> findExisting(String key, String endpoint) {
return repo.findByIdempotencyKeyAndEndpoint(key, endpoint)
.filter(r -> r.expiresAt().isAfter(Instant.now()))
.map(r -> new StoredResponse(r.responseStatus(), r.responseBody()));
}
@Transactional
public void store(String key, String endpoint, int status, String body) {
IdempotencyRecord record = new IdempotencyRecord(
key, endpoint, status, body, Instant.now(), Instant.now().plus(Duration.ofHours(24))
);
repo.save(record);
}
}
@RestController
@RequestMapping("/api/payments")
public class PaymentController {
private final PaymentService paymentService;
private final IdempotencyService idempotencyService;
@PostMapping
public ResponseEntity<PaymentResponse> processPayment(
@RequestBody PaymentRequest body,
@RequestHeader(value = "Idempotency-Key", required = false) String idempotencyKey) {
String endpoint = "POST /api/payments";
// Se tem Idempotency-Key, verifica cache primeiro
if (idempotencyKey != null) {
Optional<StoredResponse> cached = idempotencyService.findExisting(idempotencyKey, endpoint);
if (cached.isPresent()) {
log.info("Idempotency cache hit para key: {}", idempotencyKey);
return ResponseEntity
.status(cached.get().status())
.body(deserialize(cached.get().body(), PaymentResponse.class));
}
}
// Processa normalmente
PaymentResponse response = paymentService.process(body);
int status = HttpStatus.CREATED.value();
// Armazena para futuras chamadas com a mesma chave
if (idempotencyKey != null) {
idempotencyService.store(idempotencyKey, endpoint, status, serialize(response));
}
return ResponseEntity.status(status).body(response);
}
}Circuit Breaker — Estados, Fallbacks e Implementação
O Circuit Breaker evita cascata de falhas quando um serviço downstream está degradado.
Estados do Circuit Breaker:
CLOSED (funcionando normal)
↓ X falhas consecutivas ou Y% de falhas em janela de tempo
OPEN (falha imediata sem chamar o serviço)
↓ após timeout de espera
HALF-OPEN (testa com algumas chamadas)
↓ chamada de teste bem-sucedida
CLOSED (volta ao normal)
↓ chamada de teste falha
OPEN (volta a bloquear)// IMPLEMENTAÇÃO COM RESILIENCE4J
@Configuration
public class CircuitBreakerConfig {
@Bean
public CircuitBreakerRegistry circuitBreakerRegistry() {
CircuitBreakerConfig config = CircuitBreakerConfig.custom()
.failureRateThreshold(50) // abre com 50% de falhas
.slowCallRateThreshold(80) // também abre com 80% de chamadas lentas
.slowCallDurationThreshold(Duration.ofSeconds(2)) // "lenta" = > 2s
.waitDurationInOpenState(Duration.ofSeconds(30)) // espera 30s antes de half-open
.permittedNumberOfCallsInHalfOpenState(5) // 5 chamadas de teste
.slidingWindowSize(20) // analisa as últimas 20 chamadas
.minimumNumberOfCalls(10) // aguarda ao menos 10 chamadas antes de calcular
.build();
return CircuitBreakerRegistry.of(config);
}
}
@Service
public class PaymentService {
private final PaymentGatewayClient gatewayClient;
private final CircuitBreaker circuitBreaker;
public PaymentService(PaymentGatewayClient client, CircuitBreakerRegistry registry) {
this.gatewayClient = client;
this.circuitBreaker = registry.circuitBreaker("payment-gateway");
// Monitoramento de estado
this.circuitBreaker.getEventPublisher()
.onStateTransition(event ->
log.warn("Circuit Breaker '{}' mudou estado: {} → {}",
event.getCircuitBreakerName(),
event.getStateTransition().getFromState(),
event.getStateTransition().getToState()
)
);
}
public PaymentResult charge(Money amount, String customerId) {
// Decora a chamada com circuit breaker
Supplier<PaymentResult> call = CircuitBreaker.decorateSupplier(
circuitBreaker,
() -> gatewayClient.charge(amount, customerId)
);
try {
return call.get();
} catch (CallNotPermittedException e) {
// Circuito ABERTO — fallback imediato
log.warn("Circuit aberto para payment-gateway. Usando fallback.");
return PaymentResult.serviceUnavailable(
"Sistema de pagamento temporariamente indisponível. Tente em alguns instantes."
);
} catch (PaymentGatewayException e) {
// Erro do gateway — circuit breaker registra a falha
throw new PaymentFailedException("Falha no processamento: " + e.getMessage(), e);
}
}
}
// Spring Boot — configuração via application.yml
// resilience4j:
// circuitbreaker:
// instances:
// payment-gateway:
// failure-rate-threshold: 50
// wait-duration-in-open-state: 30s
// sliding-window-size: 20Rate Limiting — Algoritmos e Implementação
Token Bucket: um balde com capacidade N. Tokens são adicionados a uma taxa fixa. Cada requisição consome um token. Permite rajadas curtas (até N requisições imediatas).
Leaky Bucket: requisições entram e saem a uma taxa constante. Buffer de capacidade N. Suaviza rajadas — saída sempre constante.
Sliding Window: conta requisições em uma janela de tempo deslizante. Mais preciso que janelas fixas que resetam no início do período.
// IMPLEMENTAÇÃO — Token Bucket com Redis
@Component
public class RateLimiter {
private final RedisTemplate<String, String> redis;
// Retorna true se a requisição pode prosseguir
public boolean allowRequest(String clientId, int maxRequests, Duration window) {
String key = "rate_limit:" + clientId;
// Script Lua garante atomicidade no Redis
String luaScript = """
local key = KEYS[1]
local max = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
-- Remove timestamps fora da janela
redis.call('ZREMRANGEBYSCORE', key, 0, now - window * 1000)
-- Conta requisições na janela
local count = redis.call('ZCARD', key)
if count < max then
-- Adiciona esta requisição
redis.call('ZADD', key, now, now .. math.random())
redis.call('EXPIRE', key, window + 1)
return 1 -- permitido
else
return 0 -- bloqueado
end
""";
Long result = redis.execute(
RedisScript.of(luaScript, Long.class),
List.of(key),
String.valueOf(maxRequests),
String.valueOf(window.getSeconds()),
String.valueOf(System.currentTimeMillis())
);
return result != null && result == 1L;
}
}
// INTERCEPTOR Spring — aplica rate limit por IP ou por cliente autenticado
@Component
public class RateLimitInterceptor implements HandlerInterceptor {
private final RateLimiter rateLimiter;
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
String clientId = extractClientId(request); // IP ou userId do JWT
boolean allowed = rateLimiter.allowRequest(clientId, 100, Duration.ofMinutes(1));
if (!allowed) {
response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
response.addHeader("Retry-After", "60");
response.addHeader("X-RateLimit-Limit", "100");
response.addHeader("X-RateLimit-Remaining", "0");
response.getWriter().write("{\"error\": \"Rate limit excedido. Tente em 1 minuto.\"}");
return false;
}
return true;
}
private String extractClientId(HttpServletRequest request) {
// Prefere userId autenticado, cai para IP
String userId = (String) request.getAttribute("userId");
return userId != null ? "user:" + userId : "ip:" + request.getRemoteAddr();
}
}Observabilidade — Distributed Tracing, Correlation IDs e Structured Logging
Em sistemas distribuídos, um único request passa por múltiplos serviços. Sem observabilidade, depurar falhas é impossível.
// CORRELATION ID — propaga contexto entre serviços
@Component
public class CorrelationIdFilter implements Filter {
public static final String CORRELATION_ID_HEADER = "X-Correlation-ID";
public static final String MDC_KEY = "correlationId";
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpReq = (HttpServletRequest) req;
// Usa correlation ID do header de entrada ou gera um novo
String correlationId = Optional
.ofNullable(httpReq.getHeader(CORRELATION_ID_HEADER))
.orElse(UUID.randomUUID().toString());
// Adiciona ao MDC para todos os logs deste request
MDC.put(MDC_KEY, correlationId);
try {
chain.doFilter(req, res);
} finally {
MDC.remove(MDC_KEY);
}
}
}
// STRUCTURED LOGGING — logs em JSON para ingestão em Elasticsearch/Loki
// logback.xml com logstash-logback-encoder:
// <appender name="JSON" class="net.logstash.logback.appender.LogstashConsoleAppender">
// <encoder class="net.logstash.logback.encoder.LogstashEncoder"/>
// </appender>
// Log output:
// {
// "timestamp": "2024-01-15T10:23:45.123Z",
// "level": "INFO",
// "service": "order-service",
// "correlationId": "abc-123-xyz",
// "traceId": "4bf92f3577b34da6",
// "spanId": "00f067aa0ba902b7",
// "message": "Pedido criado com sucesso",
// "orderId": "order-456",
// "customerId": "cust-789",
// "total": 150.00
// }
// DISTRIBUTED TRACING com Micrometer (Spring Boot 3)
// application.yml:
// management:
// tracing:
// sampling:
// probability: 1.0 # 100% em dev; 0.1 (10%) em produção
// spring:
// application:
// name: order-service
// Com Zipkin ou Jaeger, o trace mostra:
// order-service [100ms]
// ├── POST /api/orders [10ms]
// ├── inventory-service.reserve [30ms]
// ├── payment-service.charge [45ms] ← identificar onde está lento
// └── notification-service.send [15ms]
// PROPAGANDO CORRELATION ID PARA OUTROS SERVIÇOS
@Component
public class CorrelationIdPropagator {
private final RestClient.Builder restClientBuilder;
public RestClient buildClientWithCorrelation() {
return restClientBuilder
.requestInterceptor((req, body, execution) -> {
String correlationId = MDC.get(CorrelationIdFilter.MDC_KEY);
if (correlationId != null) {
req.getHeaders().add(CorrelationIdFilter.CORRELATION_ID_HEADER, correlationId);
}
return execution.execute(req, body);
})
.build();
}
}Service Discovery — Client-Side vs Server-Side
CLIENT-SIDE DISCOVERY (ex: Eureka + Ribbon)
┌──────────────────────────────────────────────┐
│ Serviço A (client) │
│ 1. consulta registry: onde está B? │
│ 2. registry retorna: B está em 10.0.0.5:8080│
│ 3. A chama B diretamente │
└──────────────────────────────────────────────┘
Vantagem: sem single point of failure no path
Desvantagem: cliente precisa de lógica de descoberta
SERVER-SIDE DISCOVERY (ex: Kubernetes + kube-proxy, AWS ALB)
┌─────────────────────────────────────────────────────┐
│ Serviço A → Load Balancer/API Gateway → Serviço B │
│ (faz a descoberta internamente) │
└─────────────────────────────────────────────────────┘
Vantagem: clientes simples, sem lógica de descoberta
Desvantagem: load balancer pode ser gargalo/ponto de falha# KUBERNETES — service discovery via DNS
# Serviço é acessado por nome DNS interno: http://payment-service:8080
apiVersion: v1
kind: Service
metadata:
name: payment-service
namespace: ecommerce
spec:
selector:
app: payment-service
ports:
- port: 8080
targetPort: 8080
# Order service acessa: http://payment-service.ecommerce.svc.cluster.local:8080
# ou simplesmente: http://payment-service:8080 (no mesmo namespace)API Gateway Patterns
Padrões comuns em API Gateway:
1. AUTHENTICATION — centraliza autenticação (JWT validation, OAuth)
Client → [Gateway valida token] → Microserviços
2. RATE LIMITING — limita por cliente antes de chegar nos serviços
Client → [Gateway aplica limite] → Microserviços
3. REQUEST AGGREGATION — combina múltiplos endpoints em um
Client → [Gateway agrega] → Serviço A + Serviço B + Serviço C
Reduz roundtrips do cliente
4. PROTOCOL TRANSLATION — REST → gRPC, HTTP → WebSocket
Client (REST) → [Gateway traduz] → Microserviços (gRPC)
5. CIRCUIT BREAKER — fallback antes de chegar no serviço
Client → [Gateway + CB] → Serviço (se down, retorna cached ou erro amigável)Health Checks e Graceful Shutdown
// SPRING BOOT — Health Check customizado
@Component
public class DatabaseHealthIndicator extends AbstractHealthIndicator {
private final DataSource dataSource;
@Override
protected void doHealthCheck(Health.Builder builder) throws Exception {
try (Connection conn = dataSource.getConnection();
Statement stmt = conn.createStatement()) {
stmt.execute("SELECT 1");
builder.up()
.withDetail("database", "PostgreSQL")
.withDetail("responseTime", measureResponseTime());
} catch (SQLException e) {
builder.down()
.withDetail("error", e.getMessage());
}
}
}
// application.yml
// management:
// health:
// livenessstate:
// enabled: true
// readinessstate:
// enabled: true
// endpoint:
// health:
// show-details: always
// probes:
// enabled: true
// Kubernetes usa:
// /actuator/health/liveness → reinicia o pod se falhar
// /actuator/health/readiness → remove do load balancer se falhar
// GRACEFUL SHUTDOWN — finaliza requests em andamento antes de parar
// application.yml:
// server:
// shutdown: graceful
// spring:
// lifecycle:
// timeout-per-shutdown-phase: 30s # aguarda até 30s por requests em andamento
// Fluxo: SIGTERM → para de aceitar novas conexões → aguarda requests ativos → desligaTwo-Phase Commit — Alternativas Práticas
// SAGA com Outbox — alternativa ao 2PC para checkout
// (ver event-driven-design.md para implementação completa)
// RESUMO DOS PADRÕES DE TRANSAÇÕES DISTRIBUÍDAS:
//
// 2PC: atômico, consistente, mas lento e bloqueante — evite em microsserviços
// Saga: eventual consistency, alta disponibilidade, compensação via eventos
// Outbox: atomicidade local banco + publicação assíncrona confiável no broker
// Idempotência: handler pode ser chamado N vezes com o mesmo resultado
//
// Combine: Outbox + Saga + Idempotência = transações distribuídas confiáveis sem 2PCConsensus — Raft e Paxos
Em um sistema distribuído, múltiplos nós precisam concordar sobre um único valor mesmo diante de falhas: qual nó é o líder, qual transação veio antes, qual entrada de log aplicar. Esse é o problema do consenso.
Paxos
Desenvolvido por Leslie Lamport, o Paxos resolve o consenso em quatro fases: Prepare → Promise → Accept → Accepted. É matematicamente correto, mas notoriamente difícil de implementar e entender:
PAXOS — fluxo básico (Single-Decree Paxos)
Proposer Acceptors (quórum)
| |
|-- Prepare(n) ---------------> | fase 1a: propõe número de rodada n
|<-- Promise(n, valorAnterior)- | fase 1b: promete não aceitar n' < n
| |
|-- Accept(n, valor) ---------> | fase 2a: propõe o valor
|<-- Accepted(n, valor) ------- | fase 2b: aceita se n ainda é válido
| |
| -- notifica learners (valor acordado)
Problemas práticos do Paxos:
- Multi-Paxos (sequência de valores) requer extensões não definidas no paper original
- "Livelock": dois proposers podem se bloquear mutuamente incrementando n
- Difícil de depurar e auditar em produçãoRaft
Raft (Ongaro & Ousterhout, 2014) foi projetado explicitamente para ser compreensível. Divide o problema em três partes independentes:
RAFT — componentes principais
1. ELEIÇÃO DE LÍDER
- Nós começam como Followers
- Se não recebem heartbeat em election timeout → viram Candidate
- Candidate pede votos (RequestVote RPC)
- Quem receber maioria simples vira Leader
- Novo leader começa o termo (term), número que cresce monotonicamente
2. LOG REPLICATION
Leader Followers
| |
|-- AppendEntries(log) --> | replica entradas de log
|<-- success ------------- |
| |
| após maioria confirmar |
| → entry é COMMITTED |
| → aplica à state machine|
3. SAFETY
- Um leader só é eleito se seu log está tão atualizado quanto
a maioria dos nós (lastLogTerm + lastLogIndex)
- Garante que nenhum dado committed é perdido em failoverQuórum
Para tolerar f falhas, o cluster precisa de 2f + 1 nós. A maioria simples (f + 1) forma o quórum:
3 nós → tolera 1 falha (quórum: 2)
5 nós → tolera 2 falhas (quórum: 3)
7 nós → tolera 3 falhas (quórum: 4)Raft na prática
| Sistema | Uso do Raft |
|---|---|
| etcd | Armazena configuração do Kubernetes; todo write passa por Raft |
| CockroachDB | Cada range (partição) tem seu próprio grupo Raft |
| TiKV | Armazenamento distribuído do TiDB; Raft por região |
| Consul | Catálogo de serviços e KV distribuído |
| Kafka KRaft | Metadados do Kafka sem ZooKeeper (desde Kafka 3.3) |
Split-brain e como o Raft evita
Split-brain ocorre quando dois nós acreditam simultaneamente ser o líder e aceitam escritas divergentes. O Raft previne isso pelo quórum: um leader só confirma uma entrada se a maioria dos nós a replicou. Se a rede partir em dois grupos minoritários, nenhum deles consegue atingir quórum — nenhum progride, mas nenhum diverge.
Partição de rede com 5 nós:
Grupo A (2 nós): sem quórum → não confirma escritas → fica parado
Grupo B (3 nós): quórum atingido → continua operando normalmente
Após partição curar: Grupo A descobre term maior → reverte para followerVector Clocks e Causalidade
Por que relógios físicos falham
Em sistemas distribuídos, não se pode confiar em relógios físicos para ordenar eventos: clock drift (desvio acumulado de até centenas de milissegundos por hora) e a imprecisão do NTP (que sincroniza com latência variável) tornam impossível saber se dois eventos com timestamps próximos aconteceram “ao mesmo tempo” ou em qual ordem.
Lamport Timestamps
Leslie Lamport definiu a relação happens-before (→): o evento a → b se a ocorreu no mesmo processo antes de b, ou se a é o envio de uma mensagem e b o recebimento.
Regra do Lamport Clock:
- Cada processo mantém um contador C
- Ao gerar um evento: C += 1
- Ao enviar mensagem: inclui C no envelope
- Ao receber mensagem: C = max(C_local, C_mensagem) + 1
Processo P1: C=1(a) C=2(b) ────────── C=4(d)
↑
Processo P2: C=1(c) ──── envia C=2 ──> C=3(recebe) → C=4(e)
Limitação: se C(a) < C(b) não sabemos se a→b ou se são concorrentesVector Clocks
Um Vector Clock resolve a limitação dos Lamport timestamps mantendo um contador por nó. Com isso é possível detectar não só ordem, mas também concorrência (dois eventos que não se influenciaram):
3 nós: A, B, C
Vector Clock de cada evento: [CA, CB, CC]
A envia mensagem com VC [1,0,0]:
B recebe → B.VC = [1,1,0] (max de cada posição + 1 no próprio)
C envia → C.VC = [0,0,1] (independente de B)
Comparar [1,1,0] e [0,0,1]:
- [1,1,0] não domina [0,0,1] (C tem 0 < 1 na posição de C)
- [0,0,1] não domina [1,1,0]
→ eventos são CONCORRENTES — há conflito potencialUsos práticos
DynamoDB Versioning:
- Cada item tem um vector version (VectorClock interno)
- Em conflito de escritas concorrentes → retorna ambas as versões ao cliente
- Cabe à aplicação (ou a uma função de merge) resolver
Conflict Detection em bancos distribuídos:
- CouchDB, Riak: armazenam versões conflitantes; usuário ou função resolve
- Git (inspiração conceitual): merge conflita quando ambas as branches modificaram a mesma linha
CRDTs (Conflict-free Replicated Data Types):
- Alternativa sem coordenação: estruturas de dados projetadas para que
qualquer merge de dois estados produza um resultado determinístico
- Exemplos: G-Counter (contador só cresce), LWW-Register (last-write-wins),
OR-Set (add/remove sem conflito), Automerge (documento colaborativo)
- Usados em: Redis CRDT (Enterprise), Figma (multiplayer), NotionChaos Engineering
Chaos Engineering é a prática de introduzir falhas controladas em produção para descobrir fraquezas antes que os usuários as encontrem. A premissa: se você não testa que seu sistema sobrevive a falhas, você não sabe se ele sobrevive.
Princípios fundamentais
1. Defina o "steady state" — métricas que definem o sistema saudável
(ex: p99 de latência < 200ms, taxa de erro < 0.1%, throughput > 1000 req/s)
2. Formule uma hipótese — "se eu matar o pod X, o steady state se mantém?"
3. Introduza a falha — com blast radius controlado (ex: apenas 5% do tráfego)
4. Observe — o steady state se manteve? Onde o sistema compensou?
5. Aprenda e corrija — se falhou, corrija antes de aumentar o scopeChaos vs Teste de Carga
| Teste de Carga | Chaos Engineering | |
|---|---|---|
| Objetivo | Encontrar limites de capacidade | Encontrar fraquezas de resiliência |
| Variável | Volume de tráfego | Falhas de infraestrutura |
| Quando | Antes de um lançamento | Continuamente em produção |
| Exemplo | ”Aguenta 10.000 req/s?" | "O que acontece se o banco primário cair?” |
Progressão de implementação
# Estágio 1 — Nível pod/processo (comece aqui)
# Matar um pod aleatório no Kubernetes
kubectl delete pod $(kubectl get pods -l app=payment-service -o name | shuf -n1)
# Estágio 2 — Falhas de rede (injetar latência ou perda de pacotes)
# Com tc (traffic control) dentro de um contêiner
tc qdisc add dev eth0 root netem delay 200ms # latência artificial
tc qdisc add dev eth0 root netem loss 10% # 10% de pacotes perdidos
# Estágio 3 — Degradação de dependências
# Simular timeout em chamadas a um serviço externo
# (use um proxy/sidecar para interceptar e atrasar)
# Estágio 4 — Falha de zona de disponibilidade inteira
# (em cloud: isolar subnet, revogar security group de uma AZ)Ferramentas
Netflix Chaos Monkey (original):
- Mata instâncias EC2 aleatórias durante horário comercial
- Forçou a Netflix a construir resiliência real
Gremlin:
- SaaS com UI; suporta: CPU stress, memory stress, latência, blackhole,
DNS failure, shutdown, time travel
- Controle fino de blast radius e agendamento
Chaos Mesh (Kubernetes-native):
- CRDs do Kubernetes para definir experimentos como código
- PodChaos, NetworkChaos, StressChaos, DNSChaos, HTTPChaos# Exemplo: Chaos Mesh — injetar latência de rede no serviço de pagamentos
apiVersion: chaos-mesh.org/v1alpha1
kind: NetworkChaos
metadata:
name: payment-service-latency
spec:
action: delay
mode: percent # aplica em percentual dos pods
value: "30" # 30% dos pods afetados
selector:
namespaces: [production]
labelSelectors:
app: payment-service
delay:
latency: "150ms"
jitter: "50ms"
duration: "5m" # experimento dura 5 minutosGame Days
Um Game Day é um exercício planejado em que a equipe simula um incidente grave (falha de datacenter, DDoS, corrupção de banco). Diferente de experimentos automáticos, o Game Day testa processos humanos: runbooks, comunicação, tempo de detecção e resposta.
Estrutura de um Game Day:
1. Defina o cenário (ex: "região AWS us-east-1 indisponível")
2. Time de ataque injeta as falhas sem avisar o horário exato
3. Time de resposta detecta, diagnostica, mitiga
4. Retrospectiva: o que pegou de surpresa? Onde o runbook falhou?
5. Ações: correções concretas com prazo definidoService Mesh
O problema
Em uma arquitetura de microsserviços com dezenas de serviços, cada serviço precisa lidar individualmente com: retries, timeouts, circuit breakers, mTLS entre serviços, rastreamento distribuído e coleta de métricas. Isso resulta em código de infraestrutura duplicado em cada serviço — em linguagens e frameworks diferentes.
O Service Mesh extrai essa responsabilidade para a camada de infraestrutura.
Arquitetura: sidecar proxy
SEM service mesh:
Serviço A → (HTTP direto) → Serviço B
(retry, mTLS, trace: responsabilidade do código da aplicação)
COM service mesh (sidecar):
Serviço A → Envoy (sidecar A) → rede → Envoy (sidecar B) → Serviço B
(retry, mTLS, trace: responsabilidade do Envoy — transparente à aplicação)
Cada pod no Kubernetes ganha um container Envoy injetado automaticamente.
Todo tráfego entra e sai pelo Envoy — a aplicação fala apenas com localhost.Control Plane vs Data Plane
DATA PLANE (Envoy sidecars):
- Intercepta e roteiam o tráfego real
- Aplica políticas em tempo de execução: retry, timeout, mTLS, balanceamento
- Emite métricas (Prometheus), traces (Zipkin/Jaeger) e logs de acesso
CONTROL PLANE (Istiod / Linkerd control plane):
- Distribui configuração para os sidecars via xDS API
- Gerencia certificados mTLS (rotação automática)
- Provê APIs (CRDs Kubernetes) para os operadores configurarem políticasIstio
# VirtualService — roteamento inteligente
# Envia 90% do tráfego para v1 e 10% para v2 (canary deployment)
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: payment-service
spec:
hosts: [payment-service]
http:
- route:
- destination:
host: payment-service
subset: v1
weight: 90
- destination:
host: payment-service
subset: v2
weight: 10
---
# DestinationRule — define os subsets e a política de conexão
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
name: payment-service
spec:
host: payment-service
trafficPolicy:
connectionPool:
tcp:
maxConnections: 100
outlierDetection: # circuit breaker automático
consecutive5xxErrors: 5
interval: 30s
baseEjectionTime: 30s
subsets:
- name: v1
labels:
version: v1
- name: v2
labels:
version: v2
---
# PeerAuthentication — mTLS automático entre todos os serviços do namespace
apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
name: default
namespace: production
spec:
mtls:
mode: STRICT # rejeita tráfego sem TLS mútuoLinkerd
Linkerd é uma alternativa mais simples ao Istio: proxy em Rust (linkerd2-proxy) com overhead de memória menor e configuração mais direta. Não usa Envoy.
# Instalação
linkerd install | kubectl apply -f -
linkerd check
# Injetar sidecar em um deployment
kubectl annotate namespace production linkerd.io/inject=enabled
# Dashboard de métricas (golden metrics: latência, taxa de sucesso, throughput)
linkerd viz dashboardQuando um service mesh justifica sua complexidade
Um service mesh adiciona overhead operacional real: mais containers por pod (memória), mais latência por hop (microsegundos a milissegundos), curva de aprendizado, debugging mais difícil.
Vale a pena quando:
✓ Muitos serviços (>10) em linguagens diferentes sem biblioteca de resiliência comum
✓ mTLS obrigatório entre serviços (compliance, segurança zero-trust)
✓ Canary deployments e traffic shifting frequentes
✓ Time de plataforma dedicado para operar o mesh
Provavelmente não vale:
✗ Poucos serviços (<5) em uma única linguagem (use a biblioteca: Resilience4j, go-kit)
✗ Time pequeno sem expertise em Kubernetes avançado
✗ Latência ultra-baixa onde cada microssegundo importa
✗ Apenas para ter observabilidade — Prometheus + Grafana + OpenTelemetry resolvem mais simples