Teu progresso
0 / 83 módulos0%
Estágio 04 · 04-09
Bloqueado"Escala" é discutida em abstract até bater. Aí a verdade aparece: o gargalo é DB, não app. Ou é egress de rede, não CPU. Ou é hot key em Redis, não thread count. Decisões erradas de scale são caras: shard prematuro, multi-region antes de provar single-region maduro, autoscaler sem rate limit de DB conn pool. Ou pior: time-to-market perdido por over-engineering "pra quando crescer".
Este módulo é scaling com método: vertical vs horizontal, statefulness, sharding (estratégias), read replicas, caching tiers, queue offload, multi-region active-passive vs active-active, geo-distribution, CDN, edge. Você sai sabendo medir + escalar consciente.
Default em web: começa vertical até dor (ou redundância pra HA force horizontal). Aí horizontal.
App stateless escala horizontal trivialmente (réplicas idênticas; LB distribui).
State em-memória do app (sessions, cache local) impede scale: load balancer manda usuário pra outro pod e session some.
Mover state pra fora: Redis sessions, sticky sessions (LB), JWT (stateless).
DBs são stateful por natureza. Scale via replication + sharding.
Postgres read replicas: writes no primary, reads em replicas. Lag eventual.
Padrões:
Limites: replication lag pode ser segundos sob load; reads ficam stale. Aceite ou route around.
Quando 1 primary é teto, sharding: dados particionados por chave entre N shards.
Estratégias de shard key:
Re-sharding é trabalho. Decisão de shard key influencia anos.
Tools:
Já vimos. Em scale:
Hot key: 1 chave que recebe muito tráfego. Mitigations: replicate em N keys, use process-local cache pra essa key, batch reads.
Cache invalidation continua "one of two hard problems".
L1 (process LRU) é o mais difícil de invalidar: 100 instâncias = 100 caches independentes. TTL curta funciona até dor de stale demais. Padrões:
1. Pub/sub broadcast (Redis Streams ou pub/sub)
Instância que muta DB publica invalidação; todas escutam.
const PUBSUB_CHANNEL = 'cache-invalidate';
class L1Cache<V> {
private store = new Map<string, { v: V; expiry: number }>();
private redis: Redis;
constructor(redis: Redis) {
this.redis = redis;
// Subscriber dedicado (Redis pub/sub bloqueia connection)
const sub = redis.duplicate();
sub.subscribe(PUBSUB_CHANNEL, (err) => err && log.error(err));
sub.on('message', (_, key) => {
this.store.delete(key); // local invalidate
});
}
get(key: string): V | undefined {
const e = this.store.get(key);
if (!e || e.expiry < Date.now()) return undefined;
return e.v;
}
set(key: string, v: V, ttlMs: number) {
this.store.set(key, { v, expiry: Date.now() + ttlMs });
}
// Quando você muta DB, broadcast invalidate
async invalidate(key: string) {
this.store.delete(key); // local
await this.redis.publish(PUBSUB_CHANNEL, key); // outras instâncias
}
}
Trade-offs: latência de propagation ~1-10ms; at-most-once (se subscriber down quando publish, perde invalidate); ok pra cache TTL curta como segunda linha de defesa.
2. CDC-based invalidation (zero dual-write)
Em vez de app publicar invalidação, CDC do Postgres (Debezium, ver 02-09 §2.13.1) emite evento de mudança; serviço cache-invalidator escuta e dispara invalidations:
Postgres UPDATE orders → WAL → Debezium → Kafka topic logistics.orders
↓
cache-invalidator service
↓
Redis PUBLISH cache-invalidate "order:{id}"
↓
all instances drop L1 entry
Vantagens: aplicação não sabe nada de cache; mudanças de qualquer fonte (admin script, migration, replication) propagam. Source of truth é DB. At-least-once garantido por Kafka (consumer commit offset).
Caveat: latência ~50-500ms (CDC + Kafka path). Pra cache de dados quase-real-time, OK; pra dados onde stale por 500ms é inaceitável (rate limit current count), broadcast direto é melhor.
3. Versioned keys (no-invalidation pattern)
Em vez de invalidar, mude a chave. Cada update incrementa version do object; cache lookup usa key:v123. Versão antiga vive até TTL natural.
async function getOrder(id: string): Promise<Order> {
// Version vem de Postgres ou Redis counter (atomic via INCR)
const version = await redis.get(`order:${id}:version`);
const cacheKey = `order:${id}:v${version}`;
const cached = await redis.get(cacheKey);
if (cached) return JSON.parse(cached);
const order = await db.queryOne(`SELECT * FROM orders WHERE id=$1`, [id]);
await redis.setEx(cacheKey, 3600, JSON.stringify(order));
return order;
}
async function updateOrder(id: string, patch: Partial<Order>) {
await db.execute(`UPDATE orders SET ... WHERE id=$1`, [...]);
await redis.incr(`order:${id}:version`); // todos os clients verão chave nova
}
Vantagens: zero invalidation race; old caches expiram naturalmente; distributed-friendly (qualquer instância pode update sem coordination).
Custos: storage cache fica maior (versões antigas até TTL); aplicação precisa fetch version antes de cache lookup (1 extra round-trip). Mitigação: MGET da version + cached value em pipeline.
4. Tag-based invalidation (Cloudflare-style)
CDN-level: cada response carrega Cache-Tag: tenant:abc, order:xyz. Invalidate via API:
curl -X POST 'https://api.cloudflare.com/client/v4/zones/Z/purge_cache' \
-H 'Authorization: Bearer ...' \
--data '{"tags":["order:xyz"]}'
Mesma ideia em app L2 (Redis): chaves carregam tag; SREM tag:xyz members + iterate pra invalidar todas que carregam aquela tag. Caro em escala alta de tags.
| Cenário | Pattern |
|---|---|
| App único + L1 process cache | Pub/sub broadcast (próprio app publish) |
| Multi-app + DB único + dados que mudam fora da app | CDC-based invalidation |
| Caches em N edges geográficos | Versioned keys (sem coordination) |
| CDN content + tagged invalidation | Tag-based via API do CDN |
Anti-padrões:
Cruza com 02-11 §2.11 (cache stampede protection complementar), 02-09 §2.13.1 (CDC pipeline já existe pra outras finalidades), 04-03 §2.8 (outbox emite eventos que feed invalidação).
Deslocar trabalho síncrono pra async:
Reduz latency user-facing; adiciona eventual consistency.
Egress da CDN é mais barato que de cloud regional; reduz cost.
Rate limit single-instance é trivial (counter em memória). Distribuído (N instâncias balanceadas) exige store compartilhado. Redis + Lua atomic é padrão.
Algoritmos comparados:
| Algoritmo | Memória/key | Smoothness | Burst handling | Implementation |
|---|---|---|---|---|
| Fixed window counter | 1 int | Hard edges (boundary effect: 2x rate em fronteira) | Permite burst no início | Trivial |
| Sliding window log | N timestamps (N = rate) | Perfeito | Restritivo | Memory custoso em rate alto |
| Sliding window counter | 2 ints (prev + curr window) | Aproximação do log com weighted average | Suave, eficiente | Padrão recomendado |
| Token bucket | 1 int (tokens) + 1 timestamp (last refill) | Suave | Permite burst até bucket cap | Padrão pra burst-friendly |
| Leaky bucket | 1 int (queue level) | Suave | Sem burst | Modela como queue |
Mais preciso, ideal pra rate limits estritos (auth, payments) onde precisão > custo de memória:
-- KEYS[1] = "ratelimit:user:123"
-- ARGV[1] = window_ms (ex: 60000)
-- ARGV[2] = max_requests (ex: 100)
-- ARGV[3] = now_ms (ex: 1714583492000)
-- ARGV[4] = request_id (uuid)
local key = KEYS[1]
local window = tonumber(ARGV[1])
local maxreqs = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local req_id = ARGV[4]
local cutoff = now - window
-- Limpa entries fora da janela
redis.call('ZREMRANGEBYSCORE', key, 0, cutoff)
-- Conta entries dentro da janela atual
local count = redis.call('ZCARD', key)
if count >= maxreqs then
local oldest = redis.call('ZRANGE', key, 0, 0, 'WITHSCORES')
local retry_after = (oldest[2] + window - now) -- ms até libertar slot
return {0, count, retry_after}
end
-- Registra request atual
redis.call('ZADD', key, now, req_id)
redis.call('PEXPIRE', key, window)
return {1, count + 1, 0}
Cliente Node:
const SCRIPT_SHA = await redis.scriptLoad(LUA_SCRIPT);
async function rateLimit(userId: string, windowMs = 60_000, maxReqs = 100) {
const [allowed, count, retryAfterMs] = await redis.evalSha(SCRIPT_SHA, {
keys: [`ratelimit:user:${userId}`],
arguments: [String(windowMs), String(maxReqs), String(Date.now()), randomUUID()]
});
if (!allowed) {
throw new RateLimitError({ count, retryAfterMs });
}
}
Atomicidade: Lua script roda single-threaded em Redis; ZREMRANGE + ZCARD + ZADD são uma transação implícita. Sem race entre instâncias.
Mais barato em memória (2 valores por key), permite burst bounded:
-- ARGV: capacity, refill_per_sec, now_ms, cost(=1 default)
local key = KEYS[1]
local capacity = tonumber(ARGV[1])
local refill = tonumber(ARGV[2]) -- tokens/sec
local now = tonumber(ARGV[3])
local cost = tonumber(ARGV[4])
local data = redis.call('HMGET', key, 'tokens', 'last_refill')
local tokens = tonumber(data[1]) or capacity
local last = tonumber(data[2]) or now
-- Refill baseado em tempo passado
local elapsed_sec = (now - last) / 1000
tokens = math.min(capacity, tokens + elapsed_sec * refill)
if tokens < cost then
redis.call('HMSET', key, 'tokens', tokens, 'last_refill', now)
redis.call('PEXPIRE', key, math.ceil(capacity / refill * 1000) + 1000)
return {0, tokens}
end
tokens = tokens - cost
redis.call('HMSET', key, 'tokens', tokens, 'last_refill', now)
redis.call('PEXPIRE', key, math.ceil(capacity / refill * 1000) + 1000)
return {1, tokens}
// Premium: 1000 req/min, burst 100
// Standard: 100 req/min, burst 20
// Free: 30 req/min, burst 5
const tiers = {
premium: { capacity: 100, refillPerSec: 1000/60 },
standard: { capacity: 20, refillPerSec: 100/60 },
free: { capacity: 5, refillPerSec: 30/60 },
};
app.use(async (req, res, next) => {
const tenant = req.tenant; // do middleware auth
const { capacity, refillPerSec } = tiers[tenant.tier];
try {
await tokenBucket(`rl:${tenant.id}`, capacity, refillPerSec);
next();
} catch (e) {
res.status(429)
.header('Retry-After', Math.ceil(e.retryAfterMs / 1000).toString())
.header('X-RateLimit-Limit', String(capacity))
.header('X-RateLimit-Remaining', String(e.tokens))
.json({ error: 'rate_limited', retry_after_ms: e.retryAfterMs });
}
});
{tenant.id} com hash tag pra forçar mesmo slot se você precisa script atômico cross-key.fail closed).now_ms calculado em Redis (redis.call('TIME')) em vez de cliente, pra evitar discrepância.Cruza com 04-04 §2.7 (rate limiting fundamentals) e 04-04 §2.24 (bulkhead per-tenant).
Multi-region:
Active-active escolhe:
Tools especializadas:
Multi-region multiplica complexity. Comece single-region multi-AZ. Move pra multi-region quando user latency justifica ou compliance força.
Triggers:
Horizontal Pod Autoscaler (K8s), AWS Auto Scaling Groups, Lambda concurrency.
Cuidado:
Predictive autoscale: ML models antecipam picos. AWS, GCP têm.
Em scale, conn é recurso. PgBouncer transaction mode multiplexa 1000 app conn em 50 DB conn.
Limites: prepared statements per session quebram. Configure app-side.
Long-running tasks:
/jobs/{id} ou recebe webhook.Padrão clássico, scale enorme. APIs públicos com video processing, ML inference, large reports usam.
Sob spike:
Sem backpressure: build-up até morte.
03-07 viu. Em scale, observability vira o maior centro de custo invisível depois de compute. Ordem de magnitude (preços públicos 2026):
Padrões obrigatórios pra não sangrar:
x-debug-trace: 1) ou error rate spike (dynamic verbosity).Regra de ouro: observability cost ≤ 10% do compute cost. Acima disso, audit emergencial.
CFO entra na conversa. Categorias por ordem típica de magnitude em SaaS escalado em AWS:
| Categoria | Faixa típica | Drivers principais |
|---|---|---|
| Egress | 20-40% | Transferências out, NAT, cross-AZ |
| Compute (EC2/Fargate/Lambda) | 25-45% | Right-sizing, reserved/spot mix |
| Database (RDS, DynamoDB) | 10-25% | IOPS, multi-AZ, read replicas |
| Observability | 5-15% | Logs ingest > metrics > traces |
| Storage (S3, EBS) | 3-10% | Tiering, lifecycle |
Ordem de ataque pra cortar conta sem quebrar prod:
Cost-per-unit-business ($/request, $/MAU, $/GB processado) é a métrica que importa, não fatura absoluta. Cresce a fatura mas cai o $/request? Saudável. Sobe os dois? Investigar agora.
Cruza com 04-16 (unit economics, Rule of 40) e 03-05 §2.19 (FinOps disciplina).
Métricas de saturação:
Alarme antes de bater teto. Vertical scale fácil; sharding caro. Plan ahead.
WebSocket connections per server em 2026: 100k-500k com tuning agressivo (kernel ulimits, ephemeral port range, SO_REUSEPORT, epoll edge-triggered, ring buffers ajustados). Soketi, Centrifugo, Pusher publicaram benchmarks acima de 1M conn/host em hardware moderno.
Limitadores reais antes do CPU:
ulimit -n, default Linux ~1024 — suba pra 1M).Fan-out cross-instance: Redis pub/sub (simples, eventual perda em failover), Kafka (durável, mais latência), NATS JetStream (meio-termo).
Push services especializados: Pusher, Ably, Soketi (self-host), Centrifugo. Trade entre operar você (custo fixo + skill) ou pagar por mensagem (escala linear, sem dor operacional). Break-even tipicamente em 100M+ messages/mês.
Cross-region writes eventualmente convergem. UI deve handle:
Avalie onde você realmente está e desenhe pra 10x atual, não 1000x.
Conway: scaling tech requer scaling org. Times de plataforma, SREs, dedicated DBA. Sem isso, "escalar arquitetura" sem mãos é fantasia.
§2.12 introduz backpressure como conceito. §2.20 entra em mecânica end-to-end — desde TCP layer (slow-start, congestion window) → application layer (Reactive Streams credit-based) → queue layer (consumer credit, prefetch) → producer layer (rate limit, circuit breaker). Sistema sem backpressure cascateado quebra na fila mais fraca; com backpressure correto, gracefully degrada.
Layer 1 — TCP backpressure (já vem grátis, mas tem armadilhas):
cwnd = 10 * MSS (~14KB), dobra a cada RTT até congestion event.rwnd em ACKs; sender limita in-flight bytes a min(cwnd, rwnd).send().tcp_rmem / tcp_wmem tuning + net.core.rmem_max + SO_RCVBUF em socket setup. Linux 6+ tem auto-tuning melhor.# Inspecionar cwnd, rwnd, retransmits por socket
ss -tin
# Tuning kernel (sysctl)
sysctl -w net.ipv4.tcp_rmem="4096 1048576 16777216"
sysctl -w net.ipv4.tcp_wmem="4096 1048576 16777216"
sysctl -w net.core.rmem_max=16777216
sysctl -w net.core.wmem_max=16777216
Layer 2 — Application backpressure: Reactive Streams:
Reactive Streams spec (2014, JEP 266 em Java 9, RxJS 6+, Project Reactor): consumer pede N items; producer respeita. Pattern: request(N) upstream, onNext(item) downstream. Sem request, producer não envia.
import { Subject, mergeMap, throttleTime } from 'rxjs';
// Producer com backpressure-aware flow
const orderEvents = new Subject<Order>();
orderEvents.pipe(
// throttle aplicado quando consumer está lento
throttleTime(100),
// mergeMap concurrency cap = backpressure ativo
mergeMap((order) => processOrder(order), 5), // max 5 concurrent
).subscribe({
next: (result) => log.info({ result }, 'processed'),
error: (err) => log.error({ err }),
});
Async Iterator pattern (modern, sem RxJS):
async function* eventStream(): AsyncGenerator<Order> {
while (true) {
const batch = await fetchBatch(); // fetches respeitam backpressure naturalmente
for (const order of batch) yield order;
}
}
for await (const order of eventStream()) {
await processOrder(order); // synchronous — só fetch next quando processou
}
for await...of tem backpressure embutido. eventStream só gera próximo batch quando consumer consumiu o anterior. Diferente de "fan-out": aqui processamento sequencial. Pra paralelo bounded:
import pLimit from 'p-limit';
const limit = pLimit(10);
for await (const order of eventStream()) {
await limit(() => processOrder(order));
// limit fica full → backpressure pro stream
}
Layer 3 — Message queue backpressure:
RabbitMQ — prefetch + manual ack:
await channel.prefetch(10); // max 10 msgs unacked por consumer
channel.consume('orders', async (msg) => {
if (!msg) return;
try {
await processOrder(JSON.parse(msg.content.toString()));
channel.ack(msg);
} catch (err) {
channel.nack(msg, false, false); // dead letter
}
});
Sem prefetch: broker dump mensagens no consumer; consumer overwhelmed; OOM ou message loss em crash. Prefetch = credit window. Tune por throughput vs latency.
Kafka — consumer poll + manual offset:
const consumer = kafka.consumer({ groupId: 'orders-processor' });
await consumer.run({
partitionsConsumedConcurrently: 3,
eachBatchAutoResolve: false,
eachBatch: async ({ batch, resolveOffset, heartbeat, isRunning }) => {
for (const message of batch.messages) {
if (!isRunning()) break;
await processOrder(JSON.parse(message.value!.toString()));
resolveOffset(message.offset);
await heartbeat();
}
},
});
eachBatchAutoResolve: false + resolveOffset manual = checkpoint só após processar. Crash mid-batch reprocessa o batch (idempotency obrigatória). partitionsConsumedConcurrently: bounded parallelism per partition.
SQS — visibility timeout + long polling: receive 10 mensagens; processar; delete. Sem delete em N segundos (visibility timeout) = volta pra queue. Backpressure: limit consumer instances; SQS naturally respeita (poll quando ready).
Layer 4 — Producer backpressure: load shedding e circuit breaker:
Load shedding em API: quando saturado, rejeita requests novos com 503 + Retry-After. NÃO espera, não enfileira.
// Express middleware
app.use((req, res, next) => {
const queueDepth = getQueueDepth();
const concurrentRequests = getConcurrentRequests();
if (queueDepth > 1000 || concurrentRequests > 500) {
res.status(503).set('Retry-After', '5').json({ error: 'saturated' });
return;
}
next();
});
Adaptive concurrency (Netflix Concurrency Limits): limit dinâmico baseado em latency observada. Quando latência sobe → limit cai → request rejection sobe → upstream learns.
Decision tree — drop, buffer, throttle, ou backpressure?:
| Workload | Recomendação |
|---|---|
| Real-time analytics (logs, traces) | Drop (head-drop ou tail-drop por sampling). Buffer infinito = OOM. |
| Financial transaction | Backpressure + persistent queue. Nunca drop. Slow OK; loss não. |
| User-facing API request | Load shed (503) + Retry-After. Não fila enorme; cliente decide retry. |
| Background job (email send) | Throttle + queue. Bounded queue; producer espera. |
| Bulk import | Backpressure via cursor (DB cursor + commit periódico). Sem load full em memória. |
Logística pipeline — backpressure stack completo:
Mobile app → API Gateway (load shed 503) → Order Service (concurrency limit)
↓
Postgres OUTBOX (transactional write)
↓
Debezium CDC → Kafka (partitioned, bounded retention)
↓
Order Processor (consumer prefetch=10, heartbeat)
↓
Notification Service (rate-limit por provider)
Cada hop tem backpressure ativo. Spike no mobile não derruba notification — degrada gracefully.
Observability obrigatório por layer:
ss -tin mostra cwnd, retransmits. Em scale, eBPF tcptracer ou Cilium Hubble.kafka-consumer-groups --describe), depth em RabbitMQ (rabbitmqctl list_queues), visibility timeout breaches em SQS.Retry-After issued, circuit breaker open count.Anti-patterns observados:
Queue<T> sem cap): producer infinitamente faster que consumer → OOM. Sempre bounded.for...of array quando array vem de stream: load tudo em memória primeiro. Use for await...of em iterator.eachBatchAutoResolve: false: crash mid-batch perde mensagens entre offset commit e processamento.prefetch=unlimited (default): consumer overwhelmed em spike. Set always.Cruza com 04-09 §2.12 (backpressure conceito), 04-09 §2.13 (observability é pré-req), 04-04 §2.3 (jittered backoff em retry), 04-04 §2.20 (adaptive concurrency Netflix), 04-02 §2.x (queue patterns RabbitMQ/Kafka), 02-09 §2.20 (DB capacity é limit final).
Sharding entra quando single-DB cap reached — não antes. Complexity 10x: routing layer, cross-shard JOINs, resharding ops, transactional limits. Decisão certa = entender quando shard, qual key, qual stack. Decisão errada = hot shard, scatter-gather lento, rebalance impossível.
Quando shard (vs vertical scaling vs read replicas):
db.r7g.16xlarge (~512GB RAM, 64 vCPU, 256k IOPS gp3). Aurora I/O Optimized estende mais um pouco. Beyond, must shard.Três estratégias de sharding:
user_id 0-1M → shard A; 1M-2M → shard B). Pros — range queries (WHERE id BETWEEN ...) eficientes, hit single shard. Cons — hot shards (recent users always active most; timestamp range = shard mais novo sempre pegando fogo).hash(key) % N → shard. Pros — uniform distribution, sem hot shard. Cons — range queries scatter-gather across all shards (lento).Sharding key — decisão crítica:
tenant_id: pros — multi-tenant isolation; query de um lojista hit single shard (eficiente). Cons — large tenants (top 1% lojistas com 10M orders) viram hot shards.order_id hash: pros — uniform; cons — dashboard do lojista (SELECT * FROM orders WHERE tenant_id = X) scatter-gather across all shards (slow).tenant_id com sub-sharding pra large tenants: pros — best of both; cons — routing complexo, directory layer pra split tenants grandes.tenant_id. Write-heavy global stream → hash de event_id.Vitess (MySQL) + PlanetScale architecture:
MoveTables + Reshard: zero-downtime split shard A → A1 + A2. Vstream replica continuamente; cutover atômico.Citus (Postgres) architecture:
rebalance_table_shards — online, usa logical replication.-- Citus 12+ on Postgres 16+ — Logística distributed schema
CREATE TABLE tenants (id uuid PRIMARY KEY, name text);
SELECT create_reference_table('tenants'); -- replicado pra todos workers
CREATE TABLE orders (
id uuid, tenant_id uuid NOT NULL, status text, total_cents bigint,
created_at timestamptz, PRIMARY KEY (tenant_id, id)
);
SELECT create_distributed_table('orders', 'tenant_id'); -- shard by tenant
CREATE TABLE tracking_pings (
order_id uuid, tenant_id uuid NOT NULL, ping_at timestamptz,
lat double precision, lng double precision,
PRIMARY KEY (tenant_id, order_id, ping_at)
);
SELECT create_distributed_table('tracking_pings', 'tenant_id', colocate_with => 'orders');
-- Rebalance online quando worker novo entra
SELECT rebalance_table_shards('orders', shard_transfer_mode := 'force_logical');
CockroachDB / TiDB / YugabyteDB (NewSQL):
Custom application-level sharding (alternativa pragmática):
import { drizzle } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';
import { eq } from 'drizzle-orm';
import { orders } from './schema';
const SHARDS = {
shard0: drizzle(postgres(process.env.DB_SHARD_0_URL!)),
shard1: drizzle(postgres(process.env.DB_SHARD_1_URL!)),
shard2: drizzle(postgres(process.env.DB_SHARD_2_URL!)),
shard3: drizzle(postgres(process.env.DB_SHARD_3_URL!)),
} as const;
function shardForTenant(tenantId: string): keyof typeof SHARDS {
// tenantId é uuid; primeiros 8 hex chars = uniform hash
const hash = parseInt(tenantId.slice(0, 8), 16);
return `shard${hash % 4}` as keyof typeof SHARDS;
}
export async function getOrdersForTenant(tenantId: string) {
const shard = SHARDS[shardForTenant(tenantId)];
return shard.select().from(orders).where(eq(orders.tenantId, tenantId));
}
// Cross-shard query (admin global dashboard) — scatter-gather
export async function countOrdersGlobal() {
const results = await Promise.all(
Object.values(SHARDS).map((s) => s.select({ c: orders.id }).from(orders)),
);
return results.flat().length;
}
Sharding decision matrix 2026:
| Approach | Pros | Cons | Cost |
|---|---|---|---|
| Vertical scaling | Simplest, zero complexity | Cap ~512GB RAM | $50k/mo RDS r7g.16xl |
| Read replicas | Solve reads | NÃO writes; lag | $5k/mo + RDS primary |
| Vitess / PlanetScale | MySQL compat, managed branching | Vendor lock (PlanetScale) | $0.50/M reads + $4/GB |
| Citus / Azure CosmosDB-PG | Postgres compat, reference tables | Self-host complex, coordinator SPOF | OSS ou Azure $$$ |
| CockroachDB | Auto-shard + global ACID | OLTP slower, enterprise expensive | $1k+/mo serious |
| App-level sharding | Full control, zero vendor | Reshard pain, eng team owns | Postgres × N + dev time |
Logística applied — when to shard (capstone roadmap):
orders + tracking_pings por tenant_id (Citus, hash, 4 shards initial). countries, couriers_global, pricing_tiers viram reference tables. Coordinator HA via standby coordinator + PgBouncer.Anti-patterns observados:
vstream configurado: resharding offline, downtime hours em scale grande.Cruza com 02-09 §2.x (Postgres deep, partitioning como precursor de sharding), 04-01 §2.x (distributed systems theory, CAP/PACELC justifica trade-offs sharded), 04-04 §2.x (resilience, shard failover + replica promotion), 04-13 §2.x (CDC sharded source pra lakehouse), 03-05 §2.x (AWS RDS / Aurora limits forçam decisão).
Multi-region não é "deploy em 2 regions atrás de DNS round-robin". Multi-region production é três decisões ortogonais empilhadas: (1) routing layer — como tráfego chega na region certa (latency-based DNS, GeoDNS, Anycast); (2) data residency — onde o byte do usuário pode legalmente repousar (GDPR, LGPD, DPDPA); (3) consistency model cross-region — o que acontece quando duas regions escrevem ao mesmo tempo (single-leader, multi-leader, CRDTs). Errar qualquer uma das três custa: latency surprise (write cross-region 80-150ms), multa regulatória (GDPR Article 83 — até 4% revenue global), ou silent data corruption (LWW destruindo conta bancária). §2.8 introduziu geo-distribution, §2.21 cobriu sharding intra-region. §2.22 é o playbook production das três decisões.
Geo-routing layer 2026. Quatro mecanismos, não intercambiáveis:
api.app.com — DNS responde com IP da region de menor RTT medido (Route53 mantém latency map global atualizado a cada hora). Funciona pra HTTP stateless. Falha quando: client cacheia DNS além do TTL (mobile networks com TTL ignore — sticky a region morta); RTT medido != RTT real (cliente atrás de VPN corporativo).sa-east-1, sempre, mesmo se us-east-1 estiver mais perto via backbone. Cuidado: GeoIP database tem 1-3% de erro (VPNs, corporate proxies, satellite ISPs).us-east-1. Reduz p99 jitter em 30-50% vs internet routing. AWS Global Accelerator cobra $0.025/hora por accelerator + $0.015/GB transfer — caro, vale pra tráfego latency-sensitive.# Route53 latency-based + failover record set (Terraform)
resource "aws_route53_record" "api_useast" {
zone_id = var.zone_id
name = "api.app.com"
type = "A"
set_identifier = "us-east-1"
latency_routing_policy { region = "us-east-1" }
health_check_id = aws_route53_health_check.useast.id # se region down, Route53 não retorna este RR
alias { name = aws_lb.useast.dns_name; zone_id = aws_lb.useast.zone_id; evaluate_target_health = true }
}
resource "aws_route53_record" "api_saeast" {
zone_id = var.zone_id
name = "api.app.com"
type = "A"
set_identifier = "sa-east-1"
latency_routing_policy { region = "sa-east-1" }
health_check_id = aws_route53_health_check.saeast.id
alias { name = aws_lb.saeast.dns_name; zone_id = aws_lb.saeast.zone_id; evaluate_target_health = true }
}
Data residency 2026 — o terreno regulatório. Não é opcional, não é "boa prática" — é multa. Mapa atual:
Tenant-routing por residency (pseudocode, edge worker):
// Cloudflare Worker — residency-aware tenant router
export default {
async fetch(req, env) {
const tenantId = req.headers.get('x-tenant-id');
const tenant = await env.TENANT_KV.get(tenantId, 'json'); // { residency: 'EU' | 'US' | 'BR' | 'IN' }
const regionMap = {
EU: 'https://api-eu-west-1.app.com',
US: 'https://api-us-east-1.app.com',
BR: 'https://api-sa-east-1.app.com',
IN: 'https://api-ap-south-1.app.com',
};
const target = regionMap[tenant.residency];
if (!target) return new Response('residency unknown', { status: 403 });
return fetch(target + new URL(req.url).pathname, { method: req.method, headers: req.headers, body: req.body });
}
};
Decisão de residency é per-tenant, gravada no signup e imutável (mudar residency = data migration projeto). tenant_id → region cacheado em edge KV (Cloudflare KV, DynamoDB Global Tables) — read p99 < 10ms.
Consistency cross-region — o trade-off real. Quatro padrões production:
us-east-1), read replicas nas outras. Writes vão pro primário — usuário em sa-east-1 paga 120-150ms p99 por write. Reads locais e rápidos. RPO Aurora Global < 1s, RTO promote < 1min (Q1 2025: managed failover). Simples, predictable, mas write latency cross-region é o custo. Use quando: write-volume moderado, read-heavy, residency permite cross-region.REGIONAL BY ROW (cada row pinada a uma region — write local), REGIONAL BY TABLE (table inteira em uma region), GLOBAL (read everywhere local, writes consensus global — caro).-- CockroachDB 24.x — REGIONAL BY ROW (cada row escolhe region pelo crdb_region column)
ALTER DATABASE app PRIMARY REGION "us-east1";
ALTER DATABASE app ADD REGION "europe-west1";
ALTER DATABASE app ADD REGION "southamerica-east1";
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email STRING NOT NULL,
region crdb_internal_region NOT NULL,
created_at TIMESTAMPTZ DEFAULT now()
) LOCALITY REGIONAL BY ROW AS region;
-- Insert pinado em region — write local, latency < 10ms
INSERT INTO users (email, region) VALUES ('user@x.com', 'southamerica-east1');
-- Follower read (stale por até 4.8s, lê de réplica local)
SELECT * FROM users AS OF SYSTEM TIME follower_read_timestamp() WHERE id = $1;
-- Cloud Spanner — external consistency (read snapshot global linearizável)
SELECT account_id, balance FROM accounts WHERE account_id = @id;
-- p99 latency cross-region: 100-500ms (TrueTime wait + Paxos quorum)
Real DB matrix 2026 (write-cross-region, RPO, RTO, strong consistency):
| DB | Write model | RPO | RTO | Strong cross-region | Custo relativo |
|---|---|---|---|---|---|
| Aurora Global Postgres | single-leader | < 1s | < 1min (managed) | não (read replicas eventual) | médio |
| DynamoDB Global Tables | multi-leader LWW | seconds | < 1min | sim (opt-in regional Q1/2025, 2× WCU) | alto |
| Cloud Spanner | Paxos global | 0 (synchronous) | seconds | sim (TrueTime) | muito alto |
| CockroachDB 24.x | Raft per range, regional locality | 0 (sync) | seconds | sim (configurable) | alto |
| Cosmos DB multi-region | multi-leader, 5 levels | configurable | seconds | bounded staleness opt-in | alto |
| YugabyteDB 2.20 | Raft, geo-partitioning | 0 | seconds | sim | médio |
| Vitess 21+ | sharded MySQL, multi-region | seconds | minutes | não nativo | médio |
Stateful services multi-region. Redis: cross-region replication via Redis Enterprise Active-Active (CRDB — CRDT-based) ou ElastiCache Global Datastore (single-leader). Kafka: MirrorMaker 2 ou Confluent Cluster Linking (active-active com offset translation). pgvector: replicação igual Postgres logical, mas embeddings updates frequentes geram bloat — vacuum agressivo. Stateful TCP (WebSocket, gRPC streaming): não use Anycast — use latency DNS + sticky session via region-affinity cookie.
Failover patterns. Zone failover (intra-region, AZ down): managed automaticamente (Aurora Multi-AZ, ElastiCache, MSK). RTO < 1min, RPO 0. Region failover (region inteira down — eventos raros mas reais: AWS us-east-1 Dec 2021, GCP us-central1 Jun 2022): manual em 95% dos casos production. Auto-failover cross-region é split-brain risk — quorum loss + latency partition pode causar promotion errada. Playbook: documentado em runbook (04-04 §2.30), promote command testado mensalmente, traffic switch via Route53 weighted record (gradual 10%→50%→100%).
# Aurora Global Database — promote secondary region (manual, runbook step)
aws rds failover-global-cluster \
--global-cluster-identifier app-global \
--target-db-cluster-identifier arn:aws:rds:eu-west-1:...:cluster:app-eu \
--allow-data-loss # RPO < 1s window
Stack Logística aplicada. orders DB Aurora Global Postgres single primary us-east-1 + read replicas sa-east-1 e eu-west-1 (write cross-region tolerated, 80-150ms p99 — orders volume baixo). couriers_location (high-write, 1Hz GPS por courier) em Cloudflare Workers KV cross-region (eventual, LWW — perda de 1 ponto GPS irrelevante). payments LGPD-pinned single region sa-east-1 (Brazil residency hard) — sem cross-region replica. user_data multi-tenant: residency router em Cloudflare Worker (tenant BR → sa-east-1 Postgres, EU → eu-west-1, US → us-east-1). events_log (audit) em DynamoDB Global Tables com strong consistency regional opt-in. Failover region manual via runbook 04-04 §2.30.
Anti-patterns 2026.
us-east-1 por bug no router → GDPR violation → multa €€€.sa-east-1 reader → erro cannot execute INSERT in a read-only transaction em produção.us-east-1 violando residency. Sempre validar residency no app layer também.Cruza com 04-09 §2.8 (geo-distribution intro), §2.17 (eventual consistency em scale), §2.21 (sharding strategies — residency é sharding por region), 04-01 §2.5 (CAP — multi-region é o teatro do CAP), §2.6 (PACELC — latency cross-region é o L), §2.7 (consistency models cross-region), §2.10 (quorum cross-region), §2.18 (CRDT formal), §2.21 (logical clocks pra LWW), 04-04 §2.30 (DR multi-region runbook), 03-05 §2.x (AWS managed multi-region — Aurora Global, DynamoDB Global, Global Accelerator), 02-09 §2.13 (Postgres logical replication base), 04-13 §2.x (CDC cross-region pipelines + lakehouse residency), 02-16 §2.18 (graph DB scaling — Neo4j Fabric, Memgraph in-memory, billion-node distributed via TigerGraph), 02-17 §2.20 (mobile scaling — APNs HTTP/2 batching, FCM topic fan-out, Firebase A/B + Remote Config staged rollout).
Você precisa, sem consultar:
Plano e demo de scaling Logística pra 10x atual com baseline real.
/track/{token} público) em CloudFront Function ou Worker, cached at edge.CAPACITY.md:
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 separar state do app (sessions, cache local) facilita scale horizontal?
Q2Qual é o trade-off principal de uma shard key baseada em range (ex: timestamp)?
Q3Em distributed rate limiting com Redis + Lua, por que o script garante atomicidade entre instâncias?
Q4Qual padrão de invalidação de cache distribuído evita race conditions sem coordenação entre instâncias?
Q5Quando faz sentido sair de single-region multi-AZ para multi-region active-active?
Destrava
04-09 é prereq dos seguintes módulos: