La Idea Central
La arquitectura basada en celdas divide tu sistema en unidades independientes y autónomas llamadas celdas. Cada celda sirve un subconjunto de tenants y contiene toda la infraestructura necesaria para operar independientemente, sus propias bases de datos, caches, colas y compute.
┌─────────────┐
│ Router │
│ (Asignación│
│ de Celdas) │
└──────┬──────┘
│
┌────────────┼────────────┐
│ │ │
┌─────▼─────┐ ┌───▼─────┐ ┌───▼─────┐
│ Celda A │ │ Celda B │ │ Celda C │
│ ┌───────┐ │ │┌───────┐│ │┌───────┐│
│ │API │ │ ││API ││ ││API ││
│ │DB │ │ ││DB ││ ││DB ││
│ │Cache │ │ ││Cache ││ ││Cache ││
│ │Cola │ │ ││Cola ││ ││Cola ││
│ └───────┘ │ │└───────┘│ │└───────┘│
└───────────┘ └─────────┘ └─────────┘
La propiedad crítica: las celdas no comparten nada. Si la base de datos de la Celda A se cae, las celdas B y C siguen operando.
Enrutamiento de Celdas
const CELL_ENDPOINTS = {
0: 'https://cell-a.internal.example.com',
1: 'https://cell-b.internal.example.com',
2: 'https://cell-c.internal.example.com',
3: 'https://cell-d.internal.example.com',
};
function assignCell(tenantId) {
const hash = fnv1a(tenantId);
return hash % Object.keys(CELL_ENDPOINTS).length;
}
export default {
async fetch(request) {
const tenantId = extractTenantId(request);
const cellIndex = assignCell(tenantId);
const cellUrl = CELL_ENDPOINTS[cellIndex];
const cellRequest = new Request(
`${cellUrl}${new URL(request.url).pathname}`,
request,
);
try {
const response = await fetch(cellRequest, { cf: { timeout: 10000 } });
response.headers.set('X-Cell', `cell-${cellIndex}`);
return response;
} catch (error) {
return new Response('Servicio temporalmente no disponible', { status: 503 });
}
},
};
Shuffle Sharding: Reduciendo el Radio de Explosión
import hashlib
import random
def shuffle_shard(tenant_id, num_cells, shard_size=2):
seed = int(hashlib.sha256(tenant_id.encode()).hexdigest(), 16)
rng = random.Random(seed)
cells = list(range(num_cells))
rng.shuffle(cells)
return cells[:shard_size]
# Con 8 celdas y shards de 2 celdas:
# - Fallo de una celda afecta: 25% de tenants como máximo
# - Ese es tenants hacen failover a su celda secundaria
# - Dos fallos simultáneos afectan: ~3.6% de tenants
AWS Route 53 usa este patrón. Cada zona DNS se asigna a 4 de sus 2.048 shards virtuales. La probabilidad de que un "vecino ruidoso" (un DDoS en una zona) afecte a otra zona específica es insignificantemente pequeña.
Infraestructura de Celdas con Terraform
variable "cell_id" { type = string }
variable "cell_index" { type = number }
resource "aws_vpc" "cell" {
cidr_block = "10.${var.cell_index}.0.0/16"
tags = { Name = "cell-${var.cell_id}" }
}
resource "aws_rds_cluster" "cell_db" {
cluster_identifier = "cell-${var.cell_id}-db"
engine = "aurora-postgresql"
engine_version = "16.1"
tags = { Cell = var.cell_id }
}
resource "aws_elasticache_replication_group" "cell_cache" {
replication_group_id = "cell-${var.cell_id}-redis"
node_type = "cache.r7g.large"
num_cache_clusters = 2
tags = { Cell = var.cell_id }
}
Los VPCs están aislados, las celdas no pueden comunicarse accidentalmente entre sí. Esto es deliberado.
Estrategia de Deployment: Celdas Canary
stages:
- name: canary-cell
cells: ["cell-a"]
validation:
- health_check: 5m
- error_rate: < 0.1%
- latency_p99: < 200ms
rollback_on_failure: true
- name: second-wave
cells: ["cell-b"]
depends_on: canary-cell
delay: 15m
- name: remaining
cells: ["cell-c", "cell-d"]
depends_on: second-wave
delay: 30m
Así es exactamente como un incidente de migración de base de datos funciona con celdas: la migración corre en Celda A, se rompe, se hace rollback de Celda A, y las celdas B/C/D nunca ven la migración mala. En vez de 45 minutos de downtime total, son 5 minutos de outage parcial para un cuarto de los usuarios.
Monitoreo Por Celda
groups:
- name: cell_metrics
rules:
- record: cell:request_rate:5m
expr: sum(rate(http_requests_total{job="api"}[5m])) by (cell)
- record: cell:error_rate:5m
expr: |
sum(rate(http_requests_total{status=~"5.."}[5m])) by (cell)
/ sum(rate(http_requests_total[5m])) by (cell)
- name: cell_alerts
rules:
- alert: CellHighErrorRate
expr: cell:error_rate:5m > 0.01
for: 5m
labels:
severity: critical
La label cell en cada métrica no es negociable. Sin ella, no es posible distinguir si un spike de error rate es un problema global o de una sola celda.
Trade-offs y Costos Operacionales
Costo: Correr N copias de todo significa que 4 celdas es aproximadamente 4x el costo de base de datos. En la práctica, las celdas agregan alrededor de 40% a los costos de infraestructura.
Complejidad operacional: Cada procedimiento operacional necesita un paso de "¿qué celda?". Migraciones de base de datos, cambios de schema, actualizaciones de config, todo necesita ser cell-aware.
Migración de datos: Mover un tenant entre celdas es la operación más difícil. Requiere una pausa de escritura coordinada, copia de datos y actualización de routing.
Features globales: Algunas cosas inherentemente cruzan celdas, búsqueda global, analytics cross-tenant. Necesitan una capa de agregación separada que lee de todas las celdas.
Para Cerrar
La arquitectura basada en celdas es la opción nuclear para aislamiento de radio de explosión. Funciona, los sistemas pasan de "un mal deployment tumba todo" a "un mal deployment afecta 25% de usuarios por 5 minutos", pero el costo operacional es real.
Se recomienda comenzar de forma simple: circuit breakers, bulkheads, deployments multi-AZ. Conviene avanzar hacia celdas cuando un dominio de fallo único que afecta a todos los usuarios se convierte en un riesgo existencial para el negocio. Al implementar celdas, es recomendable comenzar con 4, mantenerlas verdaderamente independientes, e invertir fuertemente en observabilidad por celda.
La arquitectura es la parte fácil. Convivir con ella es la parte difícil.