Módulo 5: Escalar la base de datos

8. Proyecto: plan de sharding y réplicas para Enlace

Descripción

Llegó el momento de juntar las siete lecciones del módulo en un solo entregable: el plan de escalado de la base de datos de Enlace. En este proyecto vas a hacer lo que hace un ingeniero cuando una sola base de datos deja de alcanzar: decidir, con números, cuántos shards, cuántas réplicas por shard, por dónde va cada lectura y cada escritura, cómo tratas el replication lag, y cómo agregas capacidad sin desatar una tormenta de remapeo. No es "pongámosle más máquinas y veamos"; es una topología defendible donde cada número sale de la aritmética de servilleta y de código que se ejecuta.

El entregable tiene cuatro partes, como un design doc real: (1) el diagrama de la topología (shards + réplicas + enrutamiento), (2) la tabla de capacidad (shards por los 6 TB, réplicas por el 100:1, carga por nodo), (3) la demostración ejecutada de que crecer con consistent hashing mueve ~K/N y no casi todo, y (4) la lista de tradeoffs —qué elegiste, qué costó, y qué lo cambiaría—. Al terminar tendrás un plan que sabrías presentar en un design review y que sirve de plantilla para escalar la base de datos de cualquier sistema de lectura pesada, no solo la de Enlace.

Conexión con el módulo: este es el capstone de M5. Usa la presión de la lección 2 (cuándo una BD no alcanza), la replicación de las lecciones 3-4 (primary/replica, lag), el sharding de la lección 5 (clave = short_code, por hash), y el par problema/remedio de las lecciones 6-7 (mod-N remapea casi todo; consistent hashing solo ~K/N). Y cierra el arco de escalado de la guía: la caché (M4) fue la primera línea de defensa; réplicas y sharding son la segunda, para la carga que la caché no absorbe y para los 6 TB que no caben en una máquina.

El brief del proyecto

Eres el ingeniero a cargo de escalar la base de datos de Enlace. Una sola máquina ya no da: los 6 TB no caben cómodos y las lecturas aprietan. El equipo te pide una propuesta de topología. Estos son los datos de entrada, fijos (los números-ancla de toda la guía):

Dato de entradaValorDe dónde sale
Almacenamiento total a 5 años~6 TB~1 KB/registro × 6,000 M (módulo 2)
Lecturas por segundo (qps_read)~3,858 (~4,000)ratio 100:1 (módulo 2)
Escrituras por segundo (qps_write)~39 (~40)módulo 2
Hit ratio de la caché90%proyecto del módulo 4
Clave de shardshort_codelección 5 (la lectura dominante la conoce)
Repartopor hash, consistent hashing + vnodeslecciones 5-7

Y estas son las decisiones que tú tomas (las palancas de diseño): cuántos datos pones por shard (y por tanto cuántos shards), cuántas lecturas presupuestas por réplica (y por tanto cuántas réplicas por shard), cuánta redundancia añades sobre el mínimo aritmético, cómo enrutas lecturas y escrituras, y cómo tratas el lag. El proyecto consiste en elegir cada una, justificarla, y calcular las consecuencias con código.

Los pasos del plan

Antes de ver la solución de referencia, aquí está el proceso. Hazlo tú primero; la solución completa está después, en un bloque desplegable, para que compares.

Paso 1 — Decide cuántos shards por los 6 TB

El sharding resuelve el eje de los datos y las escrituras (lección 5), no el de las lecturas. Elige un presupuesto de datos por shard —cuántos TB quieres que aguante cómodo un nodo, con margen para crecer— y divide los 6 TB entre él. Redondea hacia arriba y añade holgura. Una potencia de 2 (4, 8) es cómoda para razonar el reparto. Ten a mano por qué: shardeas por los 6 TB, no por las lecturas.

Paso 2 — Reparte la carga entre los shards

Con la caché del módulo 4 al 90%, solo el 10% de las ~4,000 lecturas/s llega a la base de datos: (1 − 0.90) × 3,858 ≈ 386/s. Reparte esas lecturas y las ~40 escrituras/s entre tus shards (por hash, van parejas). Calcula la carga por shard: lecturas/s por shard y escrituras/s por primary de shard. Verás que, tras la caché y el sharding, la carga por nodo es diminuta.

Paso 3 — Decide cuántas réplicas por shard

La replicación resuelve el eje de las lecturas (lección 3). Con un presupuesto de lecturas/s por réplica, calcula cuántas réplicas necesita cada shard para su parte de las lecturas. Luego —esto es lo que separa el piso aritmético del diseño de producción— añade redundancia: dimensiona para aguantar con N−1 réplicas, no con N (lección 3). Cuenta los nodos totales.

Paso 4 — Enruta, trata el lag, y demuestra que creces sin tormenta

Define el enrutamiento (escrituras → primary del shard; lecturas → una réplica del shard) y la mitigación del lag (lección 4: la mayoría de las lecturas de Enlace toleran el lag; para "lee tu propia escritura" recién creada, lee del primary por un instante). Y —el clímax— ejecuta la comparación de las lecciones 6-7: cuando agregas un shard, mod-N remapea casi todo y consistent hashing solo ~K/N. Ese número es la prueba de que tu plan crece sin una migración masiva.

Criterios de aceptación

Tu entrega está completa cuando el plan responde, y lo demuestras con la aritmética y la salida ejecutada:

  • Número de shards justificado por los 6 TB y un presupuesto de datos por shard explícito.
  • Carga por nodo calculada: lecturas/s por shard (tras caché) y escrituras/s por primary, con los números.
  • Réplicas por shard justificadas por el presupuesto de lecturas y por la regla de redundancia N−1.
  • Enrutamiento definido: dónde va cada escritura y cada lectura, y cómo se trata el lag.
  • Crecer sin tormenta, medido: la salida ejecutada muestra mod-N remapeando ~88.9% y consistent hashing ~13.1% al pasar de 8 a 9 shards, con los TB movidos en cada caso.
  • Diagrama de topología (shards + primary/réplicas + caché + enrutamiento).
  • Lista de tradeoffs: qué elegiste, qué costó, y qué cambiaría la decisión.

Solución de referencia

No es la única respuesta correcta —otro presupuesto por shard o más réplicas por redundancia también se defienden—, pero es una propuesta sólida con cada número justificado. Intenta el tuyo antes de abrir esto.

Ver solución de referencia completa (código + salida ejecutada)

Primero, la calculadora que produce el plan, incluyendo la demostración de remapeo de las lecciones 6-7:

# enlace_scaling_plan.py — el plan de escalado de la base de datos de Enlace.
import bisect
import hashlib
import math
from collections import Counter

# ---------- Numeros-ancla de Enlace ----------
TOTAL_TB = 6.0              # ~6 TB a 5 anios (modulo 2)
qps_read = 3_858           # ~4,000 lecturas/s
qps_write = 39             # ~40 escrituras/s
hit_ratio = 0.90           # cache del modulo 4
tb_per_shard = 1.0         # DECISION: presupuesto de datos por shard (comodo)
reads_per_replica = 2_000  # DECISION: lecturas/s que aguanta una replica

# ---------- Paso 1: cuantos shards por los 6 TB ----------
shards_min = math.ceil(TOTAL_TB / tb_per_shard)
SHARDS = 8                                    # DECISION: 8 (potencia de 2, margen)
print(f"Datos: {TOTAL_TB} TB / {tb_per_shard} TB por shard -> minimo {shards_min} shards")
print(f"Elegimos SHARDS = {SHARDS}  (~{TOTAL_TB/SHARDS*1000:.0f} GB por shard, con margen)\n")

# ---------- Paso 2: carga por shard ----------
reads_to_db = qps_read * (1 - hit_ratio)      # la cache absorbe el 90%
reads_per_shard = reads_to_db / SHARDS
writes_per_shard = qps_write / SHARDS
print(f"Lecturas a la BD (tras cache 90%): {reads_to_db:,.0f}/s -> {reads_per_shard:,.1f}/s por shard")
print(f"Escrituras: {qps_write}/s -> {writes_per_shard:.1f}/s por primary de shard\n")

# ---------- Paso 3: replicas por shard ----------
replicas_needed = max(1, math.ceil(reads_per_shard / reads_per_replica))
replicas_prod = max(2, replicas_needed + 1)   # +1 por redundancia (aguantar N-1)
total_nodes = SHARDS * (1 + replicas_prod)    # 1 primary + R replicas por shard
print(f"Replicas por shard: minimo {replicas_needed}, en produccion {replicas_prod} (redundancia)")
print(f"Nodos totales: {SHARDS} x (1 primary + {replicas_prod} replicas) = {total_nodes}\n")

# ---------- Paso 4: crecer sin tormenta (lecciones 6-7) ----------
def make_keys(k):
    alphabet = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
    out = []
    for i in range(k):
        num, s = 100_000_000 + i, ""
        while num:
            s, num = alphabet[num % 62] + s, num // 62
        out.append(s.rjust(7, "0"))
    return out

def ring_point(text):
    return int(hashlib.md5(text.encode()).hexdigest(), 16) % (2 ** 32)

def node_modn(code, n):
    return int(hashlib.md5(code.encode()).hexdigest(), 16) % n

def build_ring(nodes, vnodes=100):
    ring = {}
    for node in nodes:
        for i in range(vnodes):
            ring[ring_point(f"{node}#{i}")] = node
    return ring

def node_ring(code, ring, pts):
    p = ring_point(code)
    idx = bisect.bisect(pts, p)
    if idx == len(pts):
        idx = 0
    return ring[pts[idx]]

K, N = 1_000_000, SHARDS
keys = make_keys(K)

# mod-N: remapeo al pasar de N a N+1
moved_modn = sum(node_modn(k, N) != node_modn(k, N + 1) for k in keys)

# consistent hashing: mismo experimento sobre el anillo
nodes = [f"db-{i}" for i in range(N)]
ring = build_ring(nodes)
pts = sorted(ring)
before = {k: node_ring(k, ring, pts) for k in keys}
for i in range(100):                           # agregamos el shard 9 (db-8)
    ring[ring_point(f"db-{N}#{i}")] = f"db-{N}"
pts = sorted(ring)
moved_ring = sum(node_ring(k, ring, pts) != before[k] for k in keys)

print(f"Agregar el shard {N+1} (de {N} a {N+1}), {K:,} claves:")
print(f"  mod-N            remapea {moved_modn:>9,}  ({100*moved_modn/K:5.1f}%)  ~{moved_modn/K*TOTAL_TB:.2f} TB movidos")
print(f"  consistent hash  remapea {moved_ring:>9,}  ({100*moved_ring/K:5.1f}%)  ~{moved_ring/K*TOTAL_TB:.2f} TB movidos")
print(f"  mejora: {moved_modn/moved_ring:.1f}x menos datos movidos\n")

# reparto parejo entre los 9 shards tras crecer (vnodes hacen su trabajo)
dist = Counter(node_ring(k, ring, pts) for k in keys)
print("Reparto entre los 9 shards (vnodes=100):", [dist[f"db-{i}"] for i in range(N + 1)])

Qué esperar. Corriendo python enlace_scaling_plan.py sale, exactamente:

Datos: 6.0 TB / 1.0 TB por shard -> minimo 6 shards
Elegimos SHARDS = 8  (~750 GB por shard, con margen)

Lecturas a la BD (tras cache 90%): 386/s -> 48.2/s por shard
Escrituras: 39/s -> 4.9/s por primary de shard

Replicas por shard: minimo 1, en produccion 2 (redundancia)
Nodos totales: 8 x (1 primary + 2 replicas) = 24

Agregar el shard 9 (de 8 a 9), 1,000,000 claves:
  mod-N            remapea   888,920  ( 88.9%)  ~5.33 TB movidos
  consistent hash  remapea   130,623  ( 13.1%)  ~0.78 TB movidos
  mejora: 6.8x menos datos movidos

Reparto entre los 9 shards (vnodes=100): [107501, 95552, 117241, 110026, 105966, 111195, 117227, 104669, 130623]

Lee la salida contra los criterios de aceptación:

  • 8 shards, ~750 GB cada uno. Con un presupuesto de 1 TB por shard, los 6 TB piden un mínimo de 6; elegimos 8 (potencia de 2, cómoda de razonar) para dejar cada shard en ~750 GB, con holgura para crecer antes del siguiente reshard.
  • Carga por nodo diminuta: tras la caché (90%) solo llegan 386 lecturas/s a la base de datos, que repartidas entre 8 shards son ~48/s por shard; las escrituras son ~5/s por primary. La caché (M4) y el sharding juntos dejan a cada nodo casi ocioso —esa holgura es la que absorbe los picos y el día del miss masivo—.
  • 2 réplicas por shard. El piso aritmético es 1 réplica por shard (48/s cabe de sobra en el presupuesto de 2,000/s), pero desplegamos 2 para aguantar con una caída (regla N−1, lección 3): si una réplica cae, la otra absorbe su carga sin sudar. Total: 8 × (1 primary + 2 réplicas) = 24 nodos.
  • Crecer sin tormenta, medido: agregar el shard 9 con mod-N remapea 888,920 claves (88.9%) ≈ 5.33 TB; con consistent hashing, 130,623 (13.1%) ≈ 0.78 TB —6.8× menos datos movidos—. Esa es la diferencia entre una migración de horas que enfría toda la caché y una operación de rutina que solo toca el arco del nodo nuevo.
  • Reparto parejo tras crecer: con vnodes=100, los 9 shards quedan entre ~95k y ~130k claves —razonablemente parejo, sin el desbalance 10× que tendría el anillo sin vnodes (lección 7)—. El shard nuevo db-8 tiene 130,623, que es exactamente el número de claves remapeadas: al agregar un nodo, las únicas claves que se mueven son las que ese nodo captura de su arco (nadie se mueve entre nodos viejos).

El diagrama de la topología

flowchart TD
    client["Clientes"]
    cache["cache (Redis)<br/>hit ratio 90%<br/>absorbe 3,472/s"]
    router["Capa de enrutamiento<br/>shard = ring(short_code)"]

    client -->|"resolve / shorten"| cache
    cache -->|"miss: 386 lec/s + 39 esc/s"| router

    subgraph shard0["Shard 0 (~750 GB)"]
        p0[("PRIMARY<br/>escrituras")]
        r0a[("replica")]
        r0b[("replica")]
        p0 -->|replication log| r0a
        p0 -->|replication log| r0b
    end

    subgraph shardN["Shard 7 (~750 GB)"]
        pN[("PRIMARY<br/>escrituras")]
        rNa[("replica")]
        rNb[("replica")]
        pN -->|replication log| rNa
        pN -->|replication log| rNb
    end

    router -->|"escritura -> primary"| p0
    router -->|"lectura -> replica"| r0a
    router -->|"escritura -> primary"| pN
    router -->|"lectura -> replica"| rNa

    note["... shards 1..6 iguales<br/>(8 shards x 3 nodos = 24)"]

La tabla de capacidad

MétricaValorJustificación
Shards86 TB / ~750 GB por shard, con margen (mínimo 6, potencia de 2)
Datos por shard~750 GB6 TB / 8, holgado para un nodo
Réplicas por shard2piso 1 + 1 de redundancia (aguantar N−1)
Nodos totales248 × (1 primary + 2 réplicas)
Lecturas/s a la BD386/s(1 − 0.90) × 3,858; la caché absorbe el 90%
Lecturas/s por shard~48/s386 / 8, repartidas por hash
Escrituras/s por primary~5/s39 / 8
Clave de shardshort_codela lectura dominante la conoce (lección 5)
Repartoconsistent hashing + vnodes (100)remapeo ~K/N al crecer (lección 7)
Crecer 8→9 shards~0.78 TB movidos (13.1%)vs 5.33 TB (88.9%) con mod-N

La lista de tradeoffs

  • Por qué 8 shards y no 6 (el mínimo). 6 shards cabrían (1 TB cada uno), pero dejan poco margen antes del siguiente reshard. 8 deja ~750 GB por shard y es potencia de 2. Costo: más nodos que operar. Qué lo cambiaría: si el crecimiento fuera más rápido de lo previsto, arrancaría con más shards; con consistent hashing, agregar el 9º ya no es traumático, así que empezar ajustado es menos arriesgado que antes.
  • Por qué 2 réplicas por shard y no 1. El cálculo dice 1, pero 1 no tolera una caída. Costo: duplica los nodos de lectura (de 8 a 16 réplicas). Qué lo cambiaría: si la caché fuera aún más efectiva o el SLA más laxo, 1 réplica + failover al primary podría bastar; para un servicio siempre disponible, la redundancia vale su costo.
  • Por qué consistent hashing y no mod-N. mod-N reparte igual de bien con N fijo, pero al crecer remapea el 88.9% (5.33 TB, caché fría total). Consistent hashing mueve ~13.1% (0.78 TB). Costo: algo más de complejidad (el anillo, los vnodes, una tabla de posiciones). Qué lo cambiaría: nada realista; el anillo es la elección estándar. Solo un sistema que nunca cambia N podría quedarse con mod-N, y "nunca crecer" no es un plan.
  • Por qué replicación asíncrona (lag) y no síncrona. El dato de Enlace (short_code → long_url) tolera un lag de milisegundos sin drama, y la asíncrona es más rápida. Costo: el problema de "lee tu propia escritura" —un short_code recién creado resuelto desde una réplica atrasada da 404 fugaz—. Mitigación: leer del primary por un instante tras crear (lección 4). Qué lo cambiaría: si Enlace necesitara consistencia fuerte en la lectura inmediata, la síncrona o el read-from-primary permanente, a costa de latencia.

Errores comunes

Shardear para resolver un problema de lecturas. Qué pasa: alguien topa el límite de lecturas y shardea, cargando con toda la complejidad del sharding cuando una réplica lo habría resuelto más simple. Por qué pasa: "más máquinas" se siente como la respuesta a cualquier saturación. Cómo detectarlo: si tu límite es throughput de lectura y los 6 TB y las escrituras caben en una máquina, no necesitas sharding —necesitas réplicas—. Cómo corregirlo: recuerda los dos ejes (lección 2): réplicas para lecturas, sharding para datos y escrituras. Enlace shardea por los 6 TB, no por las 4,000 lecturas/s (que la caché + réplicas ya cubren). En el plan, el sharding aparece por el almacenamiento, y las réplicas por las lecturas —dos decisiones separadas—.

Dimensionar las réplicas con el piso aritmético, sin redundancia. Qué pasa: el cálculo dice "1 réplica por shard basta" y se despliega exactamente 1. El día que una se cae para mantenimiento, ese shard se queda sin capacidad de lectura (o toda su carga cae al primary, que también sirve escrituras). Por qué pasa: se confunde el mínimo aritmético con el diseño de producción. Cómo detectarlo: pregúntate "si una réplica de este shard cae, ¿el shard sigue sirviendo lecturas?". Si la respuesta es no, no tienes margen. Cómo corregirlo: dimensiona para N−1 (lección 3); al menos 2 réplicas por shard aunque el cálculo diga 1. El número de servilleta es el piso, no el objetivo.

Elegir mod-N "porque reparte parejo" sin probar qué pasa al crecer. Qué pasa: se valida el reparto con N fijo (~parejo, se ve bien) y se da por buena la decisión, sin medir el remapeo al agregar un nodo. El día que Enlace necesita el shard 9, la migración mueve el 88.9% de los datos (5.33 TB), enfría toda la caché y arriesga un pico de latencia de horas. Por qué pasa: el buen reparto es visible de inmediato y el problema del remapeo es invisible hasta que creces (lección 6). Cómo detectarlo: si tu plan de sharding no ejecutó el experimento "de N a N+1", no probaste lo que importa. Cómo corregirlo: evalúa el esquema por dos propiedades —equilibrio (reparte parejo) y estabilidad (remapea poco al cambiar N)—; mod-N aprueba la primera y reprueba la segunda, así que el plan usa consistent hashing con vnodes.

Ejercicios

Ejercicio 1 — Redimensiona para el doble de datos. Enlace crece y a 10 años acumula ~12 TB. Con el mismo presupuesto de 1 TB por shard y la misma caché (90% hit ratio, lecturas subiendo a ~8,000/s), recalcula: (a) cuántos shards pide el almacenamiento, (b) las lecturas/s por shard tras la caché, y (c) cuántas réplicas por shard y cuántos nodos totales.

Ver solución
import math
TOTAL_TB, tb_per_shard = 12.0, 1.0
qps_read, hit_ratio, reads_per_replica = 8_000, 0.90, 2_000

shards = math.ceil(TOTAL_TB / tb_per_shard)          # 12 -> redondeamos a 16 (potencia de 2)
SHARDS = 16
reads_to_db = qps_read * (1 - hit_ratio)             # 800/s
per_shard = reads_to_db / SHARDS                      # 50/s
replicas = max(2, math.ceil(per_shard / reads_per_replica) + 1)
print(SHARDS, f"{per_shard:.1f}/s por shard", f"{replicas} replicas",
      SHARDS * (1 + replicas), "nodos")
# 16 50.0/s por shard 2 replicas 48 nodos
  • (a) Shards: 12 TB / 1 TB = 12 mínimo; redondeamos a 16 (potencia de 2, margen), ~750 GB por shard como antes.
  • (b) Lecturas por shard: la caché deja (1 − 0.90) × 8,000 = 800/s a la base de datos, repartidas entre 16 shards son 50/s por shard —igual de diminuto que antes, porque al duplicar datos y lecturas también duplicamos shards—.
  • (c) Réplicas y nodos: 50/s cabe en 1 réplica, pero por redundancia 2 por shard; total 16 × 3 = 48 nodos. La lección: al escalar proporcional (datos y lecturas suben juntos, shards también), la carga por nodo se mantiene constante; el sistema crece "hacia los lados" sin que ningún nodo se caliente. Y gracias a consistent hashing, pasar de 8 a 16 shards se hace en incrementos (9, 10, …16), cada uno moviendo solo ~1/N, no todo de golpe.

Ejercicio 2 — El día del miss masivo, con shards. La caché se reinicia y arranca vacía: el hit ratio cae a 0 por unos minutos. Las 4,000 lecturas/s completas caen sobre la base de datos. (a) ¿Cuántas lecturas/s recibe cada uno de los 8 shards? (b) ¿Aguanta con el presupuesto de 2,000/s por réplica y 2 réplicas por shard? (c) ¿Qué mitigación del plan reduce el golpe?

Ver solución
  • (a) Por shard: 4,000 / 8 = 500 lecturas/s por shard (repartidas por hash). El sharding ya divide el golpe entre 8: sin sharding, un solo nodo recibiría las 4,000.
  • (b) Sí aguanta. Cada shard tiene 2 réplicas con presupuesto de 2,000/s cada una = 4,000/s de capacidad de lectura por shard; recibe 500/s. Incluso si una réplica del shard cayera durante el incidente, la otra sola (2,000/s) absorbe los 500/s con muchísimo margen. El diseño N−1 y el sharding juntos hacen que el miss masivo —que sin ellos tumbaría una BD única— sea un no-evento: 500/s por shard es una fracción del presupuesto.
  • (c) Mitigaciones del plan: (1) precalentar la caché tras un reinicio antes de exponerla al tráfico, para que el hit ratio no arranque en 0; (2) la holgura de capacidad —cada nodo corre casi ocioso en estado estacionario (~48/s) justo para absorber picos como este—; y (3) el sharding, que reparte el golpe entre 8 nodos en vez de concentrarlo. La caché es la primera línea; réplicas + sharding son la red debajo, dimensionada para sobrevivir el día que la caché falla.

Ejercicio 3 — Defiende tu plan ante tres objeciones. Un compañero cuestiona tu topología (8 shards, 2 réplicas por shard, consistent hashing). Responde a cada objeción en un par de frases, con números. (a) "24 nodos para 40 escrituras/s y 386 lecturas/s es un desperdicio brutal." (b) "¿Por qué no mod-N, que es una línea de código y reparte igual de bien?" (c) "El short_code como clave de shard no me convence, ¿por qué no el long_url?"

Ver solución
  • (a) 24 nodos es desperdicio → no, es por los datos y la redundancia, no por el throughput. Tienes razón en que 40 esc/s y 386 lec/s son cargas triviales; los 24 nodos no salen del throughput sino de dos cosas: (1) los 6 TB no caben en una máquina, y partirlos pide 8 shards; y (2) cada shard necesita réplicas para sobrevivir una caída (redundancia N−1), de ahí las 2 por shard. La capacidad de cómputo sobra —cada nodo corre casi ocioso—; lo que no sobra es espacio (6 TB) ni tolerancia a fallos. Podrías usar 6 shards en vez de 8 para ahorrar, pero no bajar mucho más sin quedarte sin margen de almacenamiento ni de redundancia.
  • (b) mod-N en vez de consistent hashing → reparte igual con N fijo, pero al crecer es una tormenta. Cierto que mod-N es una línea y reparte parejo mientras N no cambie. El problema aparece el día que agregas el shard 9: lo medí, remapea el 88.9% (888,920 claves, 5.33 TB) y enfría toda la caché, contra el 13.1% (0.78 TB) de consistent hashing. Como shardear existe para poder crecer, elegir el esquema que hace del crecimiento una catástrofe no tiene sentido. La complejidad extra del anillo se paga una vez; la tormenta de mod-N se paga en cada reshard.
  • (c) long_url como clave de shard → no, la lectura dominante no la conoce. La operación que domina es resolve(short_code) → long_url: llega un short_code y hay que encontrar la long_url. Si shardeara por long_url, resolve no sabría a qué shard ir (no tiene la long_url, justamente la está buscando), así que cada resolución preguntaría a todos los shards —una consulta cruzada, lenta, a 386/s—. El short_code es la clave correcta porque la lectura dominante siempre lo trae en la mano, es de altísima cardinalidad (62⁷) y, hasheado, reparte parejo (lección 5).

La lección de este ejercicio: un plan de escalado no es una pila de máquinas, es un conjunto de decisiones —cuántos shards y por qué, cuántas réplicas y por qué, qué esquema de reparto y por qué— que sabes defender con la aritmética y con los experimentos del módulo. Cada objeción se responde reconociendo lo cierto y anclando la decisión en un número medido.

Resumen y siguiente paso

En este proyecto produjiste el plan de escalado de la base de datos de Enlace, el capstone del módulo. Partiste de los números-ancla y decidiste, con aritmética y código: 8 shards por los 6 TB (~750 GB cada uno), 2 réplicas por shard (piso 1 + redundancia N−1) para un total de 24 nodos, con la caché del módulo 4 dejando solo 386 lecturas/s a la base de datos (~48/s por shard). Enrutaste escrituras al primary de cada shard y lecturas a sus réplicas, trataste el lag con replicación asíncrona + read-from-primary tras crear, y —el clímax— ejecutaste la prueba de que creces sin tormenta: agregar el shard 9 con mod-N remapea 888,920 claves (5.33 TB), con consistent hashing solo 130,623 (0.78 TB), 6.8× menos. Entregaste el diagrama de la topología, la tabla de capacidad y la lista de tradeoffs con sus condiciones —lo que convierte "le pusimos más máquinas" en un diseño de ingeniería—.

Antes de avanzar deberías poder: justificar el número de shards por el almacenamiento y el de réplicas por las lecturas y la redundancia; enrutar cada operación y tratar el lag; poner los dos números de remapeo (88.9% vs 13.1%) lado a lado y traducirlos a TB movidos; y defender cada elección con su número y su condición.

Con esto cierras el módulo 5. La base de datos de Enlace ya escala en sus dos ejes —réplicas para las lecturas, sharding con consistent hashing para los datos y las escrituras—, y crece sin migraciones traumáticas. Pero un sistema con 24 nodos de base de datos, una caché y varios servidores de aplicación necesita algo delante que reparta el tráfico entre esos servidores y sobreviva a que uno se caiga. En el módulo 6 vas a ver el balanceo de carga (round-robin, least-connections, por hash) y la statelessness —por qué los servidores de Enlace no deben guardar estado local para poder escalar horizontal y reemplazarse sin drama—, con los health checks que sacan de rotación al nodo enfermo. Escalar la base de datos fue este módulo; escalar la capa de aplicación que la consulta es el siguiente.

Recursos