Teu progresso
0 / 83 módulos0%
Estágio 04 · 04-04
BloqueadoSistema distribuído sempre tem componentes degradados. A pergunta é: o que acontece com SEUS clientes quando o de cima quebra? Default: cascading failure. Cliente espera; thread blocks; pool exhausts; outros clientes morrem. Catástrofe via deps.
Este módulo é resilience aplicada: timeouts, retries com backoff, jitter, idempotência (revisitada), circuit breaker, bulkhead, hedged requests, load shedding, graceful degradation, chaos engineering. Você sai sabendo desenhar serviço que fica de pé quando deps caem.
Slow failure é pior que hard. Consumer não detecta rápido.
Sempre defina timeout em qualquer dep call (HTTP, DB, Redis, queue). Sem timeout, espera infinitamente em bug do upstream.
Timeouts por camada:
Em microservices: deadline propagation. Upstream passa "tempo restante" pra downstream.
Sem deadline propagation, cada hop tenta seu próprio timeout. Resultado: cliente já desistiu, mas service A ainda chama B com 5s, B chama C com 5s, todo mundo trabalha à toa, recursos consumidos. Custo invisível em escala.
Pseudocódigo padrão (TypeScript-flavored):
// Header propagado: X-Request-Deadline = unix_ms_absoluto
type Deadline = { atMs: number };
function withDeadline(parent: Deadline, maxBudgetMs: number): Deadline {
// Filho herda menor entre o que sobrou e seu max próprio
const remaining = parent.atMs - Date.now();
return { atMs: Date.now() + Math.min(remaining, maxBudgetMs) };
}
function remainingMs(d: Deadline): number {
return Math.max(0, d.atMs - Date.now());
}
async function call<T>(svc: string, path: string, deadline: Deadline): Promise<T> {
const ms = remainingMs(deadline);
if (ms <= 0) throw new DeadlineExceededError(svc);
// Reserva 50ms de buffer pra processamento local + serialização
const downstreamBudget = ms - 50;
if (downstreamBudget <= 0) throw new DeadlineExceededError(svc);
const ctrl = new AbortController();
const t = setTimeout(() => ctrl.abort(), ms);
try {
const res = await fetch(`https://${svc}${path}`, {
signal: ctrl.signal,
headers: { 'X-Request-Deadline': String(deadline.atMs) }
});
return res.json();
} finally {
clearTimeout(t);
}
}
Handler do gateway (Logística API entry):
app.post('/orders', async (req, res) => {
// Cliente HTTP timeout de 5s; deadline absoluto pra todo o flow
const deadline = { atMs: Date.now() + 5000 };
// 1. valida (10ms typical, budget 50ms)
const data = await validateInput(req.body, withDeadline(deadline, 50));
// 2. chama courier-svc (p99 200ms, budget 500ms)
const courier = await call<Courier>('courier-svc', '/assign',
withDeadline(deadline, 500));
// 3. chama payment-svc (p99 800ms, budget 1500ms)
const payment = await call<Payment>('payment-svc', '/authorize',
withDeadline(deadline, 1500));
// 4. persiste (budget = remaining minus reserva)
await db.transaction(async tx => {
if (remainingMs(deadline) < 200) throw new DeadlineExceededError('db');
await tx.insert(orders).values({ ...data, courier, payment });
});
res.json({ ok: true });
});
Cada hop downstream lê X-Request-Deadline, calcula remaining, recusa work se inviável. Pegadinha: clock skew entre hosts > tolerância → deadline absoluto via wall clock falha. NTP sane (drift < 50ms) é pré-requisito; em ambientes com skew alto (containers em hardware velho), use deadline relativo (ms) e re-derive a cada hop.
Implementações production-ready:
context.WithDeadline em Go, Metadata grpc-timeout em wire.deadline como W3C baggage.signal: AbortSignal que cascateia.Cruza com 04-09 §2.13 (observability cost de calls que continuam após cliente desistir) e 04-04 §2.5 (circuit breaker fecha antes de deadline expirar).
Retry em failures transientes (network blip, momento de leader election). NUNCA em failures permanentes (4xx auth errors).
Quando retry:
Como:
2^n * base + jitter. Sem jitter, cluster sincroniza retries (thundering herd).random(0, base * 2^n) é robusto.Retry storm: cada layer retries N vezes. 3 layers x 3 retries = 27x carga em downstream falhando. Normalmente: retries só na camada mais externa (cliente edge); inner calls fail-fast.
AWS Architecture Blog (Marc Brooker, 2015) compara 3 estratégias de jitter; decorrelated jitter vence em throughput sob contention. Implementação production-ready:
type RetryConfig = {
baseMs: number; // delay inicial, ex: 100
maxMs: number; // cap por tentativa, ex: 30_000
maxAttempts: number; // 3-5 típico; nunca infinito
totalBudgetMs?: number; // deadline absoluto pra retry chain
retryableErrors: (err: unknown) => boolean;
onRetry?: (attempt: number, delayMs: number, err: unknown) => void;
};
class RetryError extends Error {
constructor(public attempts: number, public lastError: unknown) {
super(`Failed after ${attempts} attempts`);
}
}
async function retryWithBackoff<T>(
fn: () => Promise<T>,
config: RetryConfig
): Promise<T> {
const startedAt = Date.now();
let lastDelay = config.baseMs;
let lastErr: unknown;
for (let attempt = 1; attempt <= config.maxAttempts; attempt++) {
try {
return await fn();
} catch (err) {
lastErr = err;
// Não-retriable: bail imediato
if (!config.retryableErrors(err)) throw err;
// Última tentativa: bail
if (attempt === config.maxAttempts) break;
// Calcula delay com decorrelated jitter (Marc Brooker pattern):
// sleep = min(maxMs, random(baseMs, lastDelay * 3))
const upperBound = Math.min(config.maxMs, lastDelay * 3);
const delayMs = config.baseMs + Math.random() * (upperBound - config.baseMs);
lastDelay = delayMs;
// Respeita budget total se configurado
if (config.totalBudgetMs && Date.now() - startedAt + delayMs > config.totalBudgetMs) {
break;
}
config.onRetry?.(attempt, delayMs, err);
await sleep(delayMs);
}
}
throw new RetryError(config.maxAttempts, lastErr);
}
const sleep = (ms: number) => new Promise(r => setTimeout(r, ms));
| Estratégia | Fórmula | Concorrência | Quando |
|---|---|---|---|
| No jitter | delay = base * 2^attempt | Catastrófica em N clientes (thundering herd) | Nunca em produção |
| Full jitter | delay = random(0, base * 2^attempt) | Boa; spread uniforme | Default sano; baixo overhead |
| Decorrelated jitter | delay = min(max, random(base, last * 3)) | Vence em contention alta (AWS bench) | Retry pesado em recursos saturados |
Brooker mostrou: em 100k clientes batendo serviço degradado, decorrelated entrega ~30% mais throughput de retry sucesso vs full jitter. Para casos comuns, full jitter é OK.
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_KEY!);
async function captureWithRetry(paymentIntentId: string, idempotencyKey: string) {
return retryWithBackoff(
() => stripe.paymentIntents.capture(paymentIntentId, undefined, {
idempotencyKey, // CRITICAL: retry sem idempotency duplica cobrança
}),
{
baseMs: 200,
maxMs: 10_000,
maxAttempts: 4,
totalBudgetMs: 30_000,
retryableErrors: (err) => {
if (!(err instanceof Stripe.errors.StripeError)) return false;
// Retry: rate limit, server error, network. Não retry: auth, validation, fraud.
return ['rate_limit_error', 'api_connection_error', 'api_error'].includes(err.type);
},
onRetry: (attempt, delayMs, err) =>
log.warn({ attempt, delayMs, err: err.code }, 'Stripe capture retrying'),
}
);
}
idempotencyKey é OBRIGATÓRIO em retry de mutação (payment, delivery dispatch, send notification). Sem ele, retry duplica side effect.Retry-After header (HTTP 429 / 503): respeite. delayMs = max(decorrelated_calc, retry_after_header_ms). Ignorar é hostil ao upstream.AbortSignal pra interromper retry chain quando deadline expira (§2.2).Cruza com 04-04 §2.4 (idempotency é pré-requisito), 04-04 §2.5 (circuit breaker complementa retry), 04-04 §2.2 (deadline propagation cancela retry chain).
Retry sem idempotência = duplicação. Problema crítico em pagamentos, mensagens, mutations.
Padrões:
ON CONFLICT).UPDATE x SET status='paid' WHERE id=Y).Pattern (Michael Nygard, "Release It!"). Estados:
Benefícios:
Libs: opossum (Node), resilience4j (Java), gobreaker (Go), failsafe-go.
Configurar:
Isole recursos por consumer/tipo de request. Falha de um não derruba outros.
Exemplos:
Em K8s: pods separados por workload (rate-sensitive vs batch).
Server-side: protege contra abuse e overload. Cliente-side: protege downstream.
Em sistemas multi-tenant, rate limit por tenant (vimos em 02-11). Garante 1 tenant não engole capacity.
Quando overloaded, rejeite requests "novos" pra preservar SLO em-curso. Vimos em 03-10.
Indicators:
Rejeição com 503 + Retry-After. Cliente robusto recua.
Send mesma request a 2 backends; aceita primeira resposta. Reduz tail latency.
Quando: leituras idempotentes onde 99th percentile importa muito (search queries).
Cuidado: dobra carga. Faça só pra slow requests (após p95 estimado, dispare segunda).
Google publicou paper "The Tail at Scale", clássico do tema.
Quando dep falha, responder algo útil em vez de erro:
Em UI: skeleton + "indisponível", mas core funcional.
Anti-pattern: fallback que esconde problema permanente. Sempre alarme.
Deliberadamente injetar falhas pra validar resilience.
Game days: time agendado pra simular incidente, observar response, melhorar.
Vimos. Recap:
Em K8s: preStop hook pra delay (deregister de service mesh ou ALB) antes de SIGTERM.
Liveness vs readiness (03-03):
Anti-pattern: readiness verificando todas as deps. Cascade failure: dep down → cluster inteiro unready → não aceita reqs → não consegue se recuperar.
Best practice: shallow liveness (responde algo); medium readiness (deps críticos próprios); deep checks via separate /diag endpoint.
Cache pode mascarar dep falha:
Cuidado: stale data infinito esconde bug.
Falha em região 1 não deve afetar região 2. Falha em tenant A não deve afetar tenant B.
Multi-region active-active complexo (04-09). Single-region multi-AZ é base.
Tenants críticos isolados (database, pool, deployment) limita blast.
Observação informa onde resilience falhou.
Aceite que failure acontece. Defina threshold:
Cultura SRE.
p-retry, async-retry libs com backoff/jitter.AbortSignal.timeout(ms).Dean & Barroso (2013, "The Tail at Scale") observam: P99 latency em service distribuído explode com fan-out. Pra agregação que chama N backends, P99 do todo é dominado pelo lento.
Solution: hedged requests.
Custo: 5-10% mais requests. Benefício: P99 cai 20-40% típico.
async function hedge(call: () => Promise<T>, hedgeAfterMs: number): Promise<T> {
const a = call();
const timeout = new Promise<T>((_, rej) => setTimeout(() => rej('hedge'), hedgeAfterMs));
try {
return await Promise.race([a, timeout]);
} catch {
const b = call();
return await Promise.race([a, b]);
}
}
Cuidados:
Google Bigtable, Spanner, Cassandra usam hedging interno. CRDB (CockroachDB) tem opção config.
Hedge com timeout fixo (ex: 50ms) é hack. Timeout deve adaptar a P95 observado dinamicamente pra capturar a cauda real:
import { TDigest } from 'tdigest'; // approximate quantile estimator
class AdaptiveHedger {
private digest = new TDigest(0.01); // 1% accuracy
private minHedgeAfter = 5; // ms; floor
private maxHedgeAfter = 200; // ms; ceiling
hedgeAfter(): number {
if (this.digest.size() < 100) return 50; // sem amostras suficientes, fallback
const p95 = this.digest.percentile(0.95);
return Math.max(this.minHedgeAfter, Math.min(this.maxHedgeAfter, p95));
}
recordLatency(ms: number) {
this.digest.push(ms);
}
async call<T>(replicas: (() => Promise<T>)[]): Promise<T> {
if (replicas.length < 2) return replicas[0](); // sem hedge possível
const after = this.hedgeAfter();
const start = performance.now();
const ctrlA = new AbortController();
const ctrlB = new AbortController();
const a = replicas[0]().finally(() => ctrlB.abort());
const hedge = new Promise<T>(resolve => setTimeout(() => {
const b = replicas[1]();
b.finally(() => ctrlA.abort());
resolve(b);
}, after));
try {
const result = await Promise.race([a, hedge]);
this.recordLatency(performance.now() - start);
return result;
} catch (err) {
this.recordLatency(performance.now() - start);
throw err;
}
}
}
Custo: ~5-10% extra requests (ones que excederiam P95). Benefício real medido em produção (Google Bigtable, 2013): P99 de query lookup cai 30-43% sem aumentar load total.
| Cenário | Hedge? |
|---|---|
| Read replica de DB + read-only query | ✅ Sim, AdaptiveHedger |
| LLM call com SLA de latency | ⚠️ Sim, mas com cap absoluto + idempotency-key |
| Payment capture | ❌ Nunca (idempotency frágil + custo) |
| Cache lookup | ❌ Não (cache miss → DB hit em ambos) |
| Cross-region replication read | ✅ Sim, hedge entre regiões mais próximas |
Cruza com 04-09 §2.13 (observability de latency tail é pré-requisito), 04-04 §2.4 (idempotency é mandatory), 04-04 §2.20 (adaptive concurrency limits são complemento).
Em vez de rate limit fixo, adapt limit dinamicamente baseado em latency observada.
Algoritmos: TCP-like (additive increase, multiplicative decrease), Little's Law-based, gradient descent.
Concurrency Limits library da Netflix. Princípio:
Resulta: under load, throttle ANTES de cair. P99 protege.
Comparison fixed rate limit:
Reactive Streams spec (Java, JS): publisher demand-driven.
request(n): subscriber pede n items.onNext(item): publisher emite (≤ n).onComplete() / onError().Implementations: RxJava, Project Reactor, RxJS, Akka Streams.
Princípio: consumer dita ritmo. Publisher buffers até demand chegar; em buffer overflow, drop ou block.
Em Node streams: pipe() aplica backpressure automático. writable.write() retorna false quando buffer cheio; producer should pause.
Em Kafka: consumer poll pulled-based; producer block se broker queue cheio (acks=all).
Sem backpressure, OOM kill processo é fim típico.
Token bucket:
tokens = min(C, tokens + (now - last_refill) * R)
if tokens >= 1:
tokens -= 1
allow
else:
deny
Leaky bucket:
Trade-off:
Distributed implementation: Redis Lua script (atomic), ou DynamoDB conditional updates. Sliding window log (precise mas pesado), sliding window counter (approximate, fast).
[request fails N times]
┌──────► CLOSED ─────────────────────► OPEN ◄─────┐
│ (normal) (reject all) │
│ ▲ │ │
│ │ [success] [timeout] │ │
│ │ ▼ │
└──┴───────────── HALF_OPEN ──────────────────────┘
(allow 1 trial) [fail]
Estados:
Tunables:
Errors counted: timeout sim, 5xx sim, 4xx tipicamente não (cliente error, não backend issue).
Hystrix (Netflix, agora maintenance) era padrão; substituído por resilience4j (Java), opossum (Node), failsafe-go.
Bulkhead (origem nautica): compartimentos isolados; flood em um não afunda navio.
Em sistema:
Trade-off: isolamento custa recursos (N pools = N * size).
Logística: rate limit por lojista (não 1 cliente abusivo derruba todos).
Cenário: lojista premium paga $500/mês com SLA p99 200ms; lojista free com 0% SLA. 1 lojista free com query mal-fatorada não pode degradar premium. Bulkhead via connection pool partitioning:
// db-pools.ts — pools dedicados por tier
import pg from 'pg';
type Tier = 'premium' | 'standard' | 'free';
const POOLS: Record<Tier, pg.Pool> = {
premium: new pg.Pool({
connectionString: process.env.DATABASE_URL,
max: 20, // 20 conexões dedicadas
idleTimeoutMillis: 30_000,
connectionTimeoutMillis: 1_000,
application_name: 'logistics-premium'
}),
standard: new pg.Pool({
connectionString: process.env.DATABASE_URL,
max: 10,
idleTimeoutMillis: 30_000,
connectionTimeoutMillis: 2_000,
application_name: 'logistics-standard'
}),
free: new pg.Pool({
connectionString: process.env.DATABASE_URL,
max: 5, // free compartilha 5 conexões
idleTimeoutMillis: 10_000, // libera mais agressivo
connectionTimeoutMillis: 5_000, // espera mais antes de rejeitar
statement_timeout: 10_000, // mata query > 10s
application_name: 'logistics-free'
})
};
export function poolFor(tier: Tier) { return POOLS[tier]; }
Middleware de roteamento:
app.use(async (req, res, next) => {
const tenant = await tenantFromAuth(req); // do JWT (02-13)
req.pool = poolFor(tenant.tier);
req.tenant = tenant;
next();
});
app.get('/orders', async (req, res) => {
// Query usa pool específico do tier
const { rows } = await req.pool.query(
'SELECT * FROM orders WHERE tenant_id = $1 LIMIT 100',
[req.tenant.id]
);
res.json(rows);
});
Resultados observáveis em produção:
WHERE description LIKE '%foo%' em tabela 100M rows segura só o pool free (5 conns). Premium continua snappy.pg_pool_active{tier=...} por tier expõe saturação independente.pg_pool_saturation{tier="premium"} > 0.8 é P1; tier="free" é informational.Variantes:
Anti-pattern: 1 pool global compartilhado por todos os tenants/features = noisy neighbor pesado. Você só descobre quando lojista premium liga reclamando enquanto free user roda backup script.
Cruza com 02-09 §2.12 (PgBouncer modes), 04-08 §2.19 (multi-tenancy isolation models — Pool/Bridge/Silo), 04-09 §2.16 (rate limit per tenant em Redis).
Active-passive: primary serve; standby aguarda. Failover manual ou automático (heartbeat-based).
Active-active: ambos serve. Carga distribuída. Conflict resolution se mesma escrita em ambos.
Multi-region:
Trade-off latency vs consistency: multi-region active-active típicamente eventual consistency. Strong consistency multi-region requer Spanner-class (TrueTime + Paxos).
Premissas:
Tools:
Game days (03-15) executam manualmente; chaos engineering automatiza.
Erro budget (03-15): 1 - SLO. SLO 99.9% → 0.1% = 43 min/mês.
Behavior:
Engineering impact:
Patterns aplicados: 25% budget = canário sem cuidado; 75% queimado = só hotfixes; 100% queimado = só revert + recovery.
§2.23 cobre primitives. Aqui: tuning numérico, code copy-paste, anti-patterns observados em produção 2026.
1. Timeout discipline — production rules
X-Request-Deadline: <unix-ms>. Cada hop subtrai network buffer + own work; se remaining < min_useful, fail-fast antes de chamar dependência.SET statement_timeout = '5s' per-session.setTimeout(fn, 30000) default em todo HTTP client → oncall page-out cascading quando upstream lento. Sempre service-specific.2. Retry strategy — production patterns
5xx, ECONNRESET, ETIMEDOUT, EAI_AGAIN. NUNCA 4xx — cliente bug, retry produz mesmo erro infinitamente.function backoffDelay(attempt: number, baseMs = 100, maxMs = 5000): number {
const exp = Math.min(maxMs, baseMs * 2 ** attempt);
return exp * (0.5 + Math.random() * 0.5); // 50-100% of exp
}
// attempt=3 → 400-800ms
delay = min(maxMs, random(baseMs, prev_delay * 3)). Evita thundering herd em recovery síncrono.3. Hedging requests (cruza wave 11 adaptive hedging)
AbortController.async function hedgedFetch(url: string, p99Ms: number, signal: AbortSignal): Promise<Response> {
const ctrl1 = new AbortController();
const ctrl2 = new AbortController();
const onAbort = () => { ctrl1.abort(); ctrl2.abort(); };
signal.addEventListener('abort', onAbort, { once: true });
const p1 = fetch(url, { signal: ctrl1.signal });
const hedge = new Promise<Response>((resolve, reject) => {
const t = setTimeout(async () => {
try { resolve(await fetch(url, { signal: ctrl2.signal })); }
catch (e) { reject(e); }
}, p99Ms);
signal.addEventListener('abort', () => clearTimeout(t), { once: true });
});
try {
const winner = await Promise.race([p1, hedge]);
if (winner === await p1) ctrl2.abort(); else ctrl1.abort();
return winner;
} finally {
signal.removeEventListener('abort', onAbort);
}
}
4. Circuit breaker — production state machine deep (cruza §2.23)
/geocode hostile não pode derrubar /route.opossum 8+ (Node), resilience4j 2+ (JVM), Polly v8 (.NET), gobreaker (Go).import CircuitBreaker from 'opossum'; // 8+
const breaker = new CircuitBreaker(callMapboxGeocode, {
timeout: 3000,
errorThresholdPercentage: 50,
volumeThreshold: 20,
resetTimeout: 30000,
rollingCountTimeout: 10000,
rollingCountBuckets: 10,
});
breaker.fallback(async (address: string) => {
const cached = await redis.get(`geocode:${address}`);
if (cached) return JSON.parse(cached);
throw new Error('GEOCODE_DEGRADED');
});
breaker.on('open', () => metrics.increment('breaker.geocode.open'));
breaker.on('halfOpen', () => metrics.increment('breaker.geocode.halfopen'));
5. Fallback hierarchy
Primary → cached value (Redis, TTL curto) → default value (UX degraded) → error explícito (last resort).
Logística: courier ETA primary via routing engine → cached ETA (TTL 5 min) → "ETA indisponível" UX → nunca user-facing 500.
Cached fallback aceitável em 90% leituras business; NUNCA em writes (read model + write model conflict, corrupção).
6. Bulkhead concrete (cruza §2.24)
courier-tracking-svc separado de order-api; tracking outage não derruba order placement.7. Load shedding patterns
X-Priority lido em middleware shed.queue_depth ou in_flight_requests como signal.8. Logística applied resilience stack
Idempotency-Key obrigatório (UUID per intent) + retry 3x decorrelated jitter, sem hedging (write).SET statement_timeout = '5s' default; deadlock detection automática; SET lock_timeout = '2s' em writes críticos.9. Anti-patterns observados (10 itens)
4xx — loop infinito; cliente bug não resolve sozinho.10. Cruza com: 04-09 (scaling, backpressure end-to-end), 02-08 (frameworks, fastify retry plugin), 02-11 (Redis fallback cache), 03-15 (incident response, post-incident tuning), 03-07 (observability, métricas breaker state + retry rate).
§2.26 cobre principles. Aqui: tooling 2026, gameday playbook, blast radius discipline, achados típicos em produção.
1. Why chaos engineering
2. Hierarchy of chaos sophistication
3. Tools 2026
4. Toxiproxy pattern Logística (integration tests)
import Toxiproxy from 'toxiproxy-node-client';
const client = new Toxiproxy('http://toxiproxy:8474');
beforeEach(async () => {
await client.populate([{
name: 'redis_proxy',
listen: '0.0.0.0:6380',
upstream: 'redis:6379',
}]);
});
test('order creation survives Redis 500ms latency', async () => {
const proxy = await client.get('redis_proxy');
const toxic = await proxy.addToxic({
type: 'latency',
attributes: { latency: 500, jitter: 50 },
});
const start = Date.now();
const res = await fetch('/orders', { method: 'POST', body: JSON.stringify(order) });
expect(res.status).toBe(201);
expect(Date.now() - start).toBeLessThan(2000); // total budget §2.28
await proxy.removeToxic(toxic.name);
});
test('order creation falls back to memory cache if Redis down', async () => {
const proxy = await client.get('redis_proxy');
await proxy.disable(); // cuts connection
const res = await fetch('/orders', { method: 'POST', body: JSON.stringify(order) });
expect(res.status).toBe(201); // graceful fallback ativo
await proxy.enable();
});
5. AWS FIS — production-grade fault injection
Experiment template (JSON): targets + actions + stop conditions. Pattern Logística — terminate 1 EC2 em orders-api ASG:
{
"actions": {
"terminateInstance": {
"actionId": "aws:ec2:terminate-instances",
"parameters": { "instanceCount": "1" },
"targets": { "Instances": "ordersAsg" }
}
},
"targets": {
"ordersAsg": {
"resourceType": "aws:ec2:instance",
"selectionMode": "COUNT(1)",
"resourceTags": { "AutoScalingGroup": "orders-api" }
}
},
"stopConditions": [{
"source": "aws:cloudwatch:alarm",
"value": "arn:aws:cloudwatch:us-east-1:123:alarm:orders-api-error-rate-high"
}]
}
aws:rds:reboot-db-instances, aws:network:disrupt-connectivity, aws:eks:pod-cpu-stress.6. K8s chaos via Litmus
apiVersion: litmuschaos.io/v1alpha1
kind: ChaosEngine
metadata:
name: orders-api-pod-delete
namespace: logistica
spec:
appinfo:
appns: logistica
applabel: app=orders-api
appkind: deployment
chaosServiceAccount: litmus-admin
experiments:
- name: pod-delete
spec:
components:
env:
- name: TOTAL_CHAOS_DURATION
value: '60'
- name: CHAOS_INTERVAL
value: '10'
- name: PODS_AFFECTED_PERC
value: '20'
Chaos Mesh equivalente: kind: PodChaos, action: pod-kill, selector.labelSelectors. Litmus mais customizável; Chaos Mesh UI superior 2026.
7. Gameday playbook
03-15 §2.19).03-15).8. Blast radius management
stopConditions.9. Logística chaos engineering program
10. Common findings (chaos reveals)
11. Anti-patterns observados (10 itens)
12. Cruza com: 04-04 §2.26 (chaos principles foundation), 04-04 §2.28 (resilience tuning informa chaos targets — onde tunar timeout antes de injetar latency), 03-15 (incident response, IC roles em gameday), 03-04 (CI/CD, integration tests com Toxiproxy), 04-09 (scaling, blast radius cresce com scale), 02-17 §2.20 (mobile structured concurrency — Swift Concurrency Task tree, Kotlin Coroutines coroutineScope propagam cancellation determinística — análogo a supervision tree Erlang).
1. DR ≠ HA. HA é continuous — replica síncrona, load balancer tira instance ruim, usuário não percebe. DR é after-the-disaster — região AWS inteira foi (us-east-1 outage 2021/2023/2025), datacenter pegou fogo, ransomware criptografou prod. HA opera dentro do failure domain; DR cruza failure domains. Sistema com 99.99% HA e zero DR plan está uma região-outage de virar manchete. DR sem HA = downtime constante; HA sem DR = downtime catastrófico raro. Precisa dos dois, e tratar separado.
2. RPO/RTO definitions. RPO (Recovery Point Objective): quanto dado pode perder, medido em tempo. RPO 5min = última cópia válida pode ter até 5min de write atrás do prod. RTO (Recovery Time Objective): quanto tempo até voltar a servir. RTO 1min = de "região caiu" a "tráfego servido" em 60s. RPO determina tipo de replication (async vs sync). RTO determina tipo de standby (cold/warm/hot/active). Custos crescem exponencialmente com RPO/RTO menores.
| Tier | RPO | RTO | Padrão típico | Custo relativo | Caso de uso |
|---|---|---|---|---|---|
| 1 | 0 | < 1min | Multi-region active/active | 5-10x | Pagamentos, trading |
| 2 | < 5s | < 5min | Hot standby + DNS failover | 3-5x | Orders, checkout |
| 3 | < 1min | < 30min | Warm standby (Aurora Global) | 2-3x | Catálogo, user profile |
| 4 | < 1h | < 4h | Pilot light (replica + infra dormant) | 1.3-1.8x | Analytics, reports |
| 5 | < 24h | days | Backup restore (S3 cross-region snapshots) | 1.05-1.2x | Audit log, archive |
Tier escolhido por dado, não por sistema. Orders DB tier 2; audit log tier 5; catalog tier 3.
3. Multi-region patterns (cost vs RTO):
| Pattern | Standby state | RTO | Cost overhead | Trade-off |
|---|---|---|---|---|
| Active/Active | Servindo tráfego | seconds | ~2x infra + traffic eng | Conflict resolution complexa (CRDT, last-write-wins) |
| Hot standby | Replica ligada, idle | 1-5min | ~1.5x | Capacity planning — standby precisa aguentar 100% |
| Warm standby | Replica ligada, undersized | 5-30min | ~1.2-1.5x | Scale up no failover (autoscaling lag) |
| Pilot light | Só dados replicando, infra dormant | 30min-4h | ~1.1x | Terraform apply no DR — infra cold start |
| Backup/Restore | S3 snapshots cross-region | 4h-days | ~1.05x | Restore time proporcional ao dataset size |
Stack típico: tier 1 active/active (DynamoDB Global, Spanner), tier 2 hot standby (Aurora Global Database), tier 3-4 warm/pilot light, tier 5 backup.
4. Replication mechanics + lag math. AWS RDS cross-region read replica (Postgres/MySQL): async, lag típico 1-5s em condições normais, pode subir 30s+ em write storms. Aurora Global Database (2026): physical replication via storage layer, RPO target < 1s, RTO < 1min em managed failover, até 5 secondary regions. DynamoDB Global Tables: multi-master active/active, last-write-wins por timestamp, replication lag tipicamente < 1s. Cloud Spanner: external consistency, multi-region synchronous via Paxos, RPO 0, RTO seconds. S3 Cross-Region Replication: SLA 15min para 99.99% dos objetos, mas typical < 1min.
RPO calculation example (async replica):
write_rate = 500 writes/sec
replication_lag_p99 = 3s
RPO_p99 = write_rate * lag = 500 * 3 = 1500 writes potentially lost on hard failover
Não declarar RPO sem medir lag p99 sob carga real. CloudWatch metric ReplicaLag (RDS) ou AuroraGlobalDBReplicationLag.
5. Route53 failover record + health check:
resource "aws_route53_health_check" "primary" {
fqdn = "api-primary.fathom.io"
port = 443
type = "HTTPS"
resource_path = "/health/deep"
failure_threshold = 3
request_interval = 10 # 10s (fast) ou 30s (standard, mais barato)
measure_latency = true
regions = ["us-east-1", "eu-west-1", "ap-southeast-1"] # 3+ regions p/ evitar false positive
}
resource "aws_route53_record" "api_primary" {
zone_id = var.zone_id
name = "api.fathom.io"
type = "A"
ttl = 60 # CRÍTICO: TTL baixo p/ failover rápido. NUNCA 3600
set_identifier = "primary"
failover_routing_policy { type = "PRIMARY" }
health_check_id = aws_route53_health_check.primary.id
records = [aws_eip.primary.public_ip]
}
resource "aws_route53_record" "api_secondary" {
zone_id = var.zone_id
name = "api.fathom.io"
type = "A"
ttl = 60
set_identifier = "secondary"
failover_routing_policy { type = "SECONDARY" }
records = [aws_eip.secondary.public_ip]
}
Health check 10s interval + 3 failures = ~30s detection. TTL 60s = ~30s propagation (caches respeitam). Total RTO DNS-bound ≈ 60-90s. Caches que ignoram TTL (browsers, alguns ISPs) podem manter old IP por minutos — daí GSLB com Anycast (Cloudflare Load Balancer, AWS Global Accelerator) é melhor que DNS failover puro.
6. Failover process (sequência crítica):
SELECT pg_promote() (Postgres 14+) ou managed (Aurora failover-global-cluster).POST /order → GET /order/:id).Aurora Global DB managed failover (RTO < 1min):
aws rds failover-global-cluster \
--global-cluster-identifier fathom-orders-global \
--target-db-cluster-identifier arn:aws:rds:eu-west-1:...:cluster:fathom-orders-eu \
--allow-data-loss # accept RPO > 0; sem isso, espera replication catch up
7. Runbook structure (markdown template, vive em repo + impresso em war room):
# Runbook: Failover orders DB primary us-east-1 → eu-west-1
## Preconditions (verify ANTES de executar)
- [ ] PagerDuty incident criado, IC nomeado
- [ ] Replication lag eu-west-1 < 5s (CloudWatch `AuroraGlobalDBReplicationLag`)
- [ ] No active migration running (`SELECT * FROM pg_stat_activity WHERE query LIKE '%ALTER%'`)
- [ ] Backup snapshot < 1h old
## Steps
1. Announce em #incident-room: "Initiating failover orders-db at HH:MM UTC"
2. Fence primary: aws ec2 revoke-security-group-ingress --group-id sg-prod-db --protocol tcp --port 5432 --cidr 10.0.0.0/8
3. Promote eu-west-1: aws rds failover-global-cluster --global-cluster-identifier fathom-orders-global --target-db-cluster-identifier arn:aws:rds:eu-west-1:...
4. Wait for promotion: aws rds describe-global-clusters --query 'GlobalClusters[0].Status' (target=available, ~45s)
5. Update app config: kubectl set env deploy/orders-api DATABASE_URL=$EU_WEST_PRIMARY -n prod
6. Force DNS: aws route53 change-resource-record-sets --hosted-zone-id ... (já automático via health check, mas force se TTL alto)
## Verification
- [ ] curl https://api.fathom.io/health/deep retorna 200 + db_region=eu-west-1
- [ ] Synthetic: POST /orders + GET /orders/:id round-trip < 2s
- [ ] Error rate em Datadog < baseline + 10% (15min window)
## Rollback (se passos 3-5 falham)
1. Re-allow security group primary us-east-1
2. Revert DATABASE_URL deploy
3. Promote us-east-1 back if eu-west-1 não está catching up
## Postconditions
- [ ] Update runbook with timestamps, who-did-what, surprises
- [ ] Schedule post-mortem dentro de 48h
Runbook que nunca foi executado é ficção. Quarterly tabletop + anual real failover (gameday) ou está desatualizado.
8. Tabletop exercise (90min, quarterly):
Scenario: us-east-1 RDS API endpoint returns 5xx for 8min, then full region degradation
Roles: IC, comms, DB engineer, SRE, customer support lead
Script (facilitator):
T+0: "Datadog alert: orders API error rate 60%. CloudWatch RDS APIs returning 5xx."
T+2m: "Replication lag eu-west-1 jumped to 45s, now stable at 12s."
T+5m: "Customer support: 200 tickets in 5min, Twitter trending #fathomdown."
T+8m: "AWS Health Dashboard: 'Increased Error Rates' us-east-1 RDS."
Questions to answer LIVE (não retrospectiva):
- Quem decide failover? (IC sozinho? precisa CTO sign-off?)
- Aceitamos RPO 12s (lag atual) ou esperamos catch-up?
- Como comunicamos status page? Quem aprova?
- Se eu-west-1 também degrada em T+15m, qual o plano?
Output: gaps documentados → ações com owner + deadline.
Tabletop NÃO é "send email asking what would you do". É síncrono, na sala (física ou call), com clock real, com IC tomando decisões. 90min, quarterly. Anti-pattern: "tabletop" virou async questionnaire que ninguém responde.
9. DR drills vs gamedays. Tabletop: discussão, sem touch em prod, baixo custo. DR drill: failover real em staging mirror de prod, mensal. Gameday: failover real em prod, quarterly/biannual, janela anunciada, full team. Sem gameday real, runbook é fanfic. Netflix Chaos Monkey kills instances; DR gameday kills regions.
10. Stack Logística aplicada:
11. 10 anti-patterns:
12. Cruza com: 04-04 §2.25 (failover patterns base), 04-04 §2.26 (chaos principles, gameday é DR drill com escopo maior), 04-04 §2.27 (failure budget — DR test consome budget conscientemente), 03-15 (incident response, IC roles em gameday + tabletop), 03-05 (cloud, managed DR services Aurora Global / Spanner / DynamoDB Global), 04-09 (scaling, multi-region adiciona complexidade — replicação, conflict resolution), 02-09 §2.13 (Postgres replication base, streaming + logical), 04-01 §2.5 (CAP — DR é AP situation often, aceita stale read pra continuar disponível).
Você precisa, sem consultar:
Resilience hardening do Logística v2.
async-retry ou p-retry em chamadas crítical idempotent.POST /reports/heatmap) com semaphore.Idempotency-Key em mutations críticas (POST /orders, POST /payment).preStop 5s sleep antes de SIGTERM (let LB deregister).RUNBOOK.md cobrindo:
Threshold de Maestria
Acerte todas as 5 pra marcar o módulo como concluído. Sem pressa, sem timer. Tudo fica salvo no teu navegador.
Q1Por que slow failure costuma ser mais perigoso que hard failure?
Q2Em retry com exponential backoff, qual o problema de não usar jitter?
Q3No padrão circuit breaker, qual o papel do estado half-open?
Q4Por que readiness probe verificando todas as deps externas é anti-pattern?
Q5Qual a função da deadline propagation em arquitetura distribuída?
Destrava
04-04 é prereq dos seguintes módulos: