Design de Software

Sistemas Distribuídos — Conceitos Essenciais

CAP theorem, PACELC, consistência, transações distribuídas, Circuit Breaker, Rate Limiting, observabilidade e Service Discovery

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
└── Riak

PACELC 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 bloqueio

Alternativas 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: 20

Rate 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 → desliga

Two-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 2PC

Consensus — 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ção

Raft

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 failover

Quó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

SistemaUso do Raft
etcdArmazena configuração do Kubernetes; todo write passa por Raft
CockroachDBCada range (partição) tem seu próprio grupo Raft
TiKVArmazenamento distribuído do TiDB; Raft por região
ConsulCatálogo de serviços e KV distribuído
Kafka KRaftMetadados 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 follower

Vector 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 concorrentes

Vector 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 potencial

Usos 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), Notion

Chaos 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 scope

Chaos vs Teste de Carga

Teste de CargaChaos Engineering
ObjetivoEncontrar limites de capacidadeEncontrar fraquezas de resiliência
VariávelVolume de tráfegoFalhas de infraestrutura
QuandoAntes de um lançamentoContinuamente 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 minutos

Game 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 definido

Service 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íticas

Istio

# 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útuo

Linkerd

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 dashboard

Quando 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