Módulo 8: Proyecto — diseña Enlace de punta a punta
7. Paso 4c — Escalar hacia afuera: réplicas, sharding, balanceo y consistencia
Descripción
Esta es la lección más densa del capstone, porque cierra el paso 4 juntando los tres módulos de escala distribuida en un solo profundizar: el escalado de datos (módulo 5: réplicas y sharding con consistent hashing), el escalado de cómputo (módulo 6: balanceo de carga sobre servicios sin estado), y el tradeoff de consistencia (módulo 7: CAP/PACELC, eventual vs fuerte, y cuál le toca a Enlace). Es donde el diseño deja de ser "una caja rápida" y se vuelve un sistema distribuido de verdad —muchas máquinas colaborando, con datos repartidos, tráfico balanceado, y una decisión explícita y justificada sobre qué garantías de consistencia da y cuáles sacrifica—.
Vas a recoger el cabo que la lección 6 dejó suelto —los 386 lecturas/s residuales (y ~1,157/s en el pico) que la caché no absorbe— y verlo absorbido por las réplicas; vas a repartir los 6 TB entre shards por short_code, y a ejecutar el número que hace el sharding seguro para crecer: consistent hashing remapea 130,623 claves (13.1%) al agregar un nodo, no 888,920 (88.9%) como mod-N. Vas a balancear el tráfico entre los servidores sin estado con health checks, y —el remate del capstone— vas a elegir y justificar el modelo de consistencia de Enlace: eventual para resolve, con los números que demuestran por qué esa elección es correcta y qué compra. Al terminar, tendrás la última pieza del entregable: la lista de tradeoffs justificados.
Conexión con el módulo: es la tercera y última lección de "profundizar", y la que convierte el diseño de Enlace en distribuido. Toma la carga residual de la lección 6 como entrada, y produce la lista de tradeoffs que la lección 8 pone en el entregable final. Junta tres módulos porque, en el capstone, réplicas + balanceo + consistencia son una sola historia: escalar hacia afuera y razonar qué garantías se conservan al hacerlo. La frontera con las guías hermanas —failover a fondo (resiliencia), event sourcing/CQRS (eventos), estilos arquitectónicos— se marca en cada tramo.
La cadena de restaurantes y su carta compartida
Piénsalo así. Un restaurante que triunfa quiere crecer, y hay tres formas distintas de crecer que resuelven tres problemas distintos —y conviene no confundirlas—.
La primera: abrir más sucursales idénticas de la misma cocina popular. La receta del plato estrella es una sola, decidida en la casa matriz, pero se copia a cada sucursal, y cada sucursal sirve a los comensales de su barrio. Diez sucursales sirven diez veces más comensales del mismo plato. Eso es replicación: una copia completa de los datos en cada nodo, muchos nodos sirviendo lecturas en paralelo. Resuelve el problema de demasiados comensales (demasiadas lecturas).
La segunda: cuando el menú se vuelve enorme —mil platos, imposible que una sola cocina los prepare todos bien—, repartir el menú entre cocinas especializadas: la cocina italiana, la asiática, la de postres, cada una con su parte del menú. Ningún comensal encuentra todo en una cocina; un coordinador lo manda a la que tiene su plato. Eso es sharding: repartir los datos entre nodos cuando ya no caben (ni las escrituras ni el volumen) en uno solo. Resuelve el problema de demasiado menú (demasiados datos).
La tercera: un anfitrión en la puerta que reparte a los comensales entre las mesas libres, sin mandar a nadie a una mesa ocupada o a un mesero enfermo. Eso es el balanceo de carga: repartir el tráfico entre los servidores disponibles y evitar los caídos. Resuelve el problema de repartir bien a quien llega.
Y hay una tensión de fondo en toda cadena: cuando la casa matriz cambia una receta, pasa un rato hasta que la nueva llega a la última sucursal. Durante ese rato, una sucursal sirve la receta vieja. ¿Es un problema? Depende del plato. Para el plato del día, un desfase de minutos no importa a nadie (consistencia eventual, y está bien). Para el precio que se cobra en caja, sí importa que todas las sucursales coincidan al instante (consistencia fuerte). Elegir qué garantía dar a cada dato —y aceptar lo que sacrificas— es el tradeoff de consistencia. Enlace es, casi entero, "plato del día": el mapeo short_code → long_url no cambia, así que un desfase de milisegundos entre réplicas no molesta a nadie, y esa tolerancia es lo que compra la escala de lectura.
Conviene decirlo con todas las letras:
Escalar hacia afuera son tres decisiones distintas para tres problemas distintos: replicar (copiar los datos para servir muchas lecturas), shardear (repartir los datos cuando no caben en un nodo) y balancear (repartir el tráfico entre servidores sanos). Y una decisión transversal: qué consistencia dar a cada dato. Para Enlace, la consistencia eventual alcanza —el dato es casi inmutable—, y esa tolerancia es la que hace posible la escala.
Escalar los datos: réplicas para las lecturas
La caché deja pasar 386 lecturas/s en promedio (y ~1,157/s en el pico), más las ~40 escrituras/s. ¿A dónde van? Al patrón primary/replica: un nodo (el primary) acepta todas las escrituras y copia cada cambio, por el replication log, a otros nodos (las réplicas) que sirven lecturas. Encaja como anillo al dedo con el 100:1 de Enlace: pocas escrituras al primary, muchas lecturas repartidas entre réplicas. Calculemos cuántas réplicas hacen falta:
# replicas.py — cuantas replicas para la carga residual de Enlace.
import math
reads_per_replica = 2_000 # lecturas/s que aguanta una replica (presupuesto)
for label, reads in [("promedio (386/s)", 386), ("pico (1,157/s)", 1157)]:
n = math.ceil(reads / reads_per_replica)
print(f"{label:>18}: ceil({reads}/{reads_per_replica}) = {n} replica(s) minimas")
print("\nEn produccion: dimensionar para N-1 (tolerar la caida de una)")
print(" -> con el pico de 1,157/s: 1 minima -> desplegar 2-3 por redundancia")
Qué esperar. Con python replicas.py:
promedio (386/s): ceil(386/2000) = 1 replica(s) minimas
pico (1,157/s): ceil(1157/2000) = 1 replica(s) minimas
En produccion: dimensionar para N-1 (tolerar la caida de una)
-> con el pico de 1,157/s: 1 minima -> desplegar 2-3 por redundancia
El piso aritmético es 1 réplica —incluso en el pico, 1,157/s cabe bajo el presupuesto de 2,000/s de una réplica—. Pero el número de servilleta es el mínimo, no el diseño: en producción despliegas 2 o 3 réplicas por shard, por dos razones que la aritmética mínima no captura. Redundancia: si una réplica se cae (o se saca para mantenimiento), las otras absorben su carga —dimensionas para que el sistema aguante con N−1—. Y margen para el pico y el día malo: si la caché se vacía (el miss masivo de la lección 6), a la base de datos le llegan 3,858/s de golpe, y quieres réplicas suficientes para no colapsar mientras la caché se repuebla. La regla: cuenta el mínimo, despliega con margen.
Un asterisco honesto que el diseño debe nombrar: las réplicas van un poco atrasadas respecto al primary (el replication lag). Consecuencia concreta para Enlace: un short_code recién creado que se resuelve desde una réplica que aún no lo recibió devuelve un 404 fugaz de milisegundos. Para Enlace esto casi nunca importa (el que acaba de crear un link no lo visita al instante desde otra máquina), y cuando importa se mitiga leyendo del primary justo después de escribir. Que Enlace tolere ese lag es, precisamente, la decisión de consistencia que veremos al final —y la que permite repartir las lecturas—.
Escalar los datos: sharding, y por qué consistent hashing
Las réplicas escalan las lecturas, pero no los datos: cada réplica es una copia completa de los 6 TB, y llega el día en que 6 TB no caben cómodos en una máquina (ni sus escrituras en un solo primary). La respuesta es sharding: partir los datos entre varios shards, cada uno un primary con sus réplicas. La clave de shard de Enlace es el short_code, y se reparte por hash (no por rango) para evitar hotspots —un reparto por rango o por fecha concentraría todas las escrituras nuevas en un shard; el hash las esparce parejo—.
Pero cómo hashear importa muchísimo, y es la decisión que hace el sharding seguro o peligroso para crecer. La forma ingenua, shard = hash(short_code) % N, reparte perfecto... hasta que agregas un nodo, y entonces N cambia y casi todas las claves se remapean. La forma correcta, consistent hashing (el anillo), remapea solo el arco del nodo nuevo. Ejecutemos los dos, lado a lado, sobre un millón de claves al pasar de 8 a 9 shards:
# sharding_growth.py — mod-N contra consistent hashing, medido.
import bisect, hashlib
def ring_point(t): return int(hashlib.md5(t.encode()).hexdigest(), 16) % (2**32)
def node_modn(sc, n): return int(hashlib.md5(sc.encode()).hexdigest(), 16) % n
def build_ring(nodes, vnodes=100):
r = {}
for nd in nodes:
for i in range(vnodes):
r[ring_point(f"{nd}#{i}")] = nd
return r
def node_ring(sc, ring, pts):
p = ring_point(sc); idx = bisect.bisect(pts, p)
return ring[pts[0 if idx == len(pts) else idx]]
def make_keys(k):
ab = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
out = []
for i in range(k):
num, s = 100_000_000 + i, ""
while num: s, num = ab[num % 62] + s, num // 62
out.append(s.rjust(7, "0"))
return out
K, N = 1_000_000, 8
keys = make_keys(K)
moved_modn = sum(node_modn(k, N) != node_modn(k, N + 1) for k in keys)
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): 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"K = {K:,} claves, de N={N} a N={N+1} shards\n")
print(f" hash(key) % N {moved_modn:>9,} ({100*moved_modn/K:4.1f}%) <- casi todas")
print(f" consistent hashing {moved_ring:>9,} ({100*moved_ring/K:4.1f}%) <- ~K/(N+1)={K//(N+1):,}")
Qué esperar. Con python sharding_growth.py:
K = 1,000,000 claves, de N=8 a N=9 shards
hash(key) % N 888,920 (88.9%) <- casi todas
consistent hashing 130,623 (13.1%) <- ~K/(N+1)=111,111
Ahí está el número que hace el sharding de Enlace seguro para crecer. Agregar el noveno shard con mod-N remapea 888,920 claves (88.9%) —traducido a Enlace, mover ~5.3 TB entre máquinas, vaciar casi toda la caché, y arriesgar inconsistencia durante horas: una tormenta—. Con consistent hashing remapea 130,623 (13.1%), cerca del ideal K/(N+1) = 111,111 —mover solo ~0.8 TB, enfriar solo el arco del nodo nuevo—. Casi 7 veces menos. La misma operación —crecer— pasa de evento de riesgo mayor a mantenimiento rutinario. Y los vnodes (cada shard colocado en ~100 puntos del anillo) mantienen el reparto parejo, evitando que un shard reciba 10× más datos que otro. Para Enlace: sharding por short_code, con consistent hashing y vnodes —reparte parejo los 6 TB, enruta cada lectura a un shard en O(log N), y crece sin tormenta—.
Escalar el cómputo: balanceo sobre servicios sin estado
Los datos ya escalan; falta el cómputo. Un solo servidor de aplicación es cuello y punto único de fallo, así que Enlace corre varios servidores sin estado detrás de un balanceador de carga. Que sean sin estado (el estado vive en la caché y la base de datos, no en el servidor) es lo que hace posible el balanceo: cualquier servidor atiende cualquier petición, así que el balanceador reparte con libertad, agregar capacidad es enchufar otro servidor idéntico, y si uno se cae, se saca de rotación sin que nadie pierda nada.
El balanceador reparte con alguna estrategia, y hay tres comunes, cada una con su lógica:
- Round-robin: reparte por turnos, uno a cada servidor en orden. Simple y justo cuando todas las peticiones cuestan parecido —el caso de Enlace, donde un
resolvecuesta casi lo mismo que otro—. - Least-connections: manda cada petición al servidor con menos conexiones activas. Mejor cuando las peticiones duran tiempos muy distintos (unas largas, otras cortas), para no cargar a un servidor que ya tiene trabajo pesado.
- Por hash (de la IP o de la clave): manda siempre el mismo cliente (o la misma clave) al mismo servidor. Útil si hay caché local por servidor —no es el caso de Enlace, cuya caché es compartida (Redis)—.
Para Enlace, round-robin basta: las peticiones son homogéneas y la caché es compartida, así que no hace falta la sofisticación de least-connections ni la afinidad del hash. Y la pieza que hace el balanceo tolerante a fallos son los health checks: el balanceador pregunta periódicamente a cada servidor "¿estás vivo?", y saca de rotación al que no responde, mandando su tráfico a los sanos. Esto tapa la última grieta de la caja única del módulo 1 —el punto único de fallo del cómputo— y es lo que sostiene el 99.9% de disponibilidad del lado del servidor de aplicación. La frontera: los patrones avanzados de resiliencia (circuit breaker, bulkhead, retry con backoff) son de resilience-and-reliability-patterns-guide; aquí basta el balanceo con health checks.
El tradeoff de consistencia: por qué eventual le basta a Enlace
Llegamos al remate del capstone, la decisión que corona la lista de tradeoffs. Cuando tienes datos repartidos y replicados en muchas máquinas, aparece una pregunta ineludible: si dos réplicas pueden estar momentáneamente desincronizadas (por el lag), ¿qué garantía de consistencia le das al usuario? La teoría que enmarca esto es CAP: ante una partición de red (P), un sistema distribuido debe elegir entre consistencia (C: todas las réplicas muestran el mismo dato) y disponibilidad (A: el sistema responde aunque no pueda garantizar que el dato sea el más reciente). No puedes tener las dos durante una partición. Y PACELC lo extiende con lo que pasa cuando no hay partición (E, else): incluso en operación normal, eliges entre latencia (L) y consistencia (C) —esperar a que todas las réplicas confirmen es más consistente pero más lento—.
Para Enlace, la elección es clara y se justifica con la naturaleza del dato, no con una preferencia:
- El dato es casi inmutable. Un
short_code → long_url, una vez creado, no cambia. No hayUPDATEs que puedan divergir entre réplicas; soloINSERTs de registros nuevos. Un dato que no cambia no puede volverse "inconsistente" en el sentido grave —dos réplicas nunca mostrarán destinos distintos para el mismo código—. - La única inconsistencia posible es benigna y fugaz. Un
short_coderecién creado que se resuelve desde una réplica atrasada da un 404 de milisegundos hasta que el replication log llega. No devuelve el dato equivocado; devuelve "todavía no lo tengo". Y en Enlace eso casi nunca ocurre (el creador no visita su link al instante desde otra máquina) y se recupera solo en cuanto la réplica se pone al día. - La tolerancia compra escala. Aceptar la consistencia eventual es lo que permite repartir las lecturas entre réplicas atrasadas —si exigieras que cada lectura viera la escritura más reciente al instante, tendrías que leerlas todas del primary, y adiós a las réplicas y a la escala de lectura—. La consistencia eventual no es una concesión: es la decisión que hace posible el diseño entero.
Por eso Enlace elige consistencia eventual para resolve (leer de réplicas atrasadas está bien), con replicación asíncrona (el primary confirma sin esperar a las réplicas, priorizando latencia —la "L" de PACELC—). En términos de CAP, Enlace se inclina hacia disponibilidad: prefiere responder rápido (aunque una réplica esté un pelín atrasada) a bloquearse esperando consistencia perfecta. Es la elección correcta para un servicio de redirección, y está justificada por el número (el dato no cambia) y la consecuencia (un 404 fugaz que nadie nota), no por gusto.
Y para cerrar el lado de fiabilidad, el requisito no funcional se pone como número: el SLO (objetivo de nivel de servicio) de disponibilidad es 99.9%, que significa un máximo de ~8.76 horas de caída al año (0.001 × 365 × 24). Ese número es lo que justifica la redundancia (varios servidores, varias réplicas por shard, failover): una sola máquina falla más que eso, así que hacen falta copias. El diseño distribuido de Enlace es, en buena parte, la respuesta a ese 99.9%.
La lista de tradeoffs (el último artefacto)
Todo lo anterior se condensa en la lista de tradeoffs de Enlace —el artefacto que demuestra que el diseño entendió el problema, no solo que dibujó cajas—. Cada uno con su "gano / pago", justificado con un número:
| Decisión | Gano | Pago | Justificación numérica |
|---|---|---|---|
| Caché delante de la BD | 5.90 ms vs 50 ms; BD ve 386/s vs 4,000/s | Posible dato viejo (mitigado: dato casi inmutable + TTL) | Hit ratio 0.90; working set 333 MB |
| Réplicas de lectura | Reparten las lecturas; redundancia | Replication lag (404 fugaz) | 386/s residuales; N−1 tolerancia |
Sharding por short_code | Reparte 6 TB y escrituras; sin hotspots | Consultas cruzadas más complejas (Enlace no las tiene) | 6 TB / N shards; hash evita hotspots |
Consistent hashing (no mod-N) | Crecer mueve ~13.1%, no 88.9% | Tabla del anillo (vnodes) en memoria | 130,623 vs 888,920 al ir de 8 a 9 |
| Servidores sin estado + balanceo | Escala horizontal; tolera fallos | El estado compartido (caché/BD) es el cuello | Health checks; 99.9% disponibilidad |
| Consistencia eventual (async) | Latencia baja; escala de lectura | Lecturas pueden ver un dato de hace ms | Dato casi inmutable; 404 fugaz benigno |
| Redirección 302 (no 301) | Deja la puerta a la analítica futura | Cada visita pasa por Enlace (no cachea el navegador) | Alcance v1: analítica diferida |
Esa tabla es el corazón del entregable. Fíjate en que ninguna fila dice "elegí X porque es lo mejor"; cada una dice "gano esto, pago aquello, y el número lo justifica". Diseñar no es elegir lo óptimo (no existe); es elegir tradeoffs para requisitos concretos, y saber defender cada uno.
Errores comunes
Confundir replicación con sharding (creer que las réplicas escalan las escrituras o los datos). Qué pasa: alguien topa el límite de almacenamiento (6 TB) o de escritura y agrega réplicas esperando alivio, pero las réplicas son copias completas (no reparten los datos) y las escrituras siguen yendo todas al único primary (no las reparten). Nada mejora. Por qué pasa: "más máquinas" se confunde con "más capacidad de todo". Cómo detectarlo: si tu cuello es el volumen de datos o el throughput de escritura y agregaste réplicas, no vas a ver mejora. Cómo corregirlo: recuerda los dos ejes —replicar escala lecturas (copias que sirven en paralelo); shardear escala datos y escrituras (repartir entre nodos)—. Enlace usa los dos: réplicas para las 386 lecturas/s residuales, sharding para los 6 TB. Son herramientas distintas para problemas distintos.
Shardear con mod-N y descubrir el remapeo al crecer. Qué pasa: alguien elige hash(short_code) % N porque reparte perfecto en las pruebas (~125k por shard con N=8), y da la decisión por buena. El día que agrega un shard, se remapean 888,920 de 1,000,000 de claves —mover ~5.3 TB, vaciar la caché, arriesgar inconsistencia—, justo cuando el sistema es grande y eso más duele. Por qué pasa: el buen reparto es visible de inmediato; el mal remapeo es invisible hasta que creces. Cómo detectarlo: pregúntate "¿qué pasa cuando agrego el nodo N+1?". Si no lo probaste, no probaste lo que importa. Cómo corregirlo: evalúa el sharding por dos propiedades —equilibrio (reparte parejo) y estabilidad (poco remapeo al cambiar N)—; mod-N aprueba la primera y reprueba la segunda. Usa consistent hashing con vnodes: reparte parejo y remapea solo ~K/N (13.1%).
Elegir consistencia fuerte por defecto "para ir seguros". Qué pasa: alguien, por prudencia, exige que cada resolve lea del primary (consistencia fuerte, dato siempre fresco), y con eso desperdicia las réplicas —el primary se satura con las 4,000 lecturas/s, y las réplicas quedan ociosas—. La "seguridad" costó toda la escala de lectura. Por qué pasa: la consistencia fuerte suena más responsable, y el lag da miedo. Cómo detectarlo: si tus réplicas tienen 5% de CPU y el primary 95%, no estás repartiendo nada por miedo al lag. Cómo corregirlo: elige la consistencia que el dato necesita, no la máxima disponible. El dato de Enlace es casi inmutable y su única inconsistencia (un 404 fugaz) es benigna, así que la eventual es correcta y compra la escala. La consistencia fuerte tiene un costo (latencia, o saturar el primary); solo se paga donde el dato lo exige —y en Enlace, casi nunca lo exige—.
Ejercicios
Ejercicio 1 — Los dos ejes de escala. Para cada problema de crecimiento de Enlace, di si se resuelve con réplicas, con sharding, o con balanceo de cómputo, y por qué: (a) las lecturas residuales suben de 386/s a 4,000/s (la caché empeora); (b) los datos crecen de 6 TB a 20 TB y no caben en un nodo; (c) el servidor de aplicación se satura de CPU procesando peticiones; (d) un solo primary no aguanta el volumen de escrituras.
Ver solución
- (a) Lecturas residuales suben → réplicas. Más lecturas se resuelven con más réplicas que las repartan (el eje de las lecturas).
ceil(4,000/2,000) = 2réplicas mínimas, 3 en producción. - (b) Datos de 6 a 20 TB → sharding. Los datos no caben en un nodo; se reparten entre más shards (el eje de los datos). Las réplicas no ayudan aquí —cada réplica es una copia completa, seguiría sin caber—.
- (c) Servidor de aplicación saturado de CPU → balanceo de cómputo. Más servidores sin estado detrás del balanceador reparten la carga de CPU. No es un problema de datos (réplicas/sharding), es de cómputo.
- (d) Un primary no aguanta las escrituras → sharding. Escalar escrituras es sharding (más primaries, cada uno con su parte de los datos), no replicación (las réplicas no aceptan escrituras). Para Enlace esto no pasa (~40/s), pero si pasara, más shards = más primaries = más capacidad de escritura.
La lección: hay tres ejes de escala —lecturas (réplicas), datos y escrituras (sharding), cómputo (balanceo)— y cada problema pertenece a uno. Confundirlos lleva a agregar la herramienta equivocada (réplicas para un problema de datos, por ejemplo) y no ver mejora.
Ejercicio 2 — Traduce el remapeo a Enlace. Enlace tiene 8 shards con 6 TB repartidos, y agrega el noveno. Con los números medidos (mod-N: 888,920 remapeos; consistent hashing: 130,623; sobre 1,000,000 de claves), calcula: (a) qué fracción de los 6 TB mueve cada estrategia; (b) cuántas veces menos mueve consistent hashing; (c) por qué la diferencia crece si Enlace tuviera 100 shards en vez de 8.
Ver solución
- (a) mod-N mueve
88.9% × 6 TB ≈ 5.33 TB. Consistent hashing mueve13.1% × 6 TB ≈ 0.78 TB. Para la misma operación —agregar un shard— uno mueve ~5.3 TB y el otro ~0.8 TB. - (b)
888,920 / 130,623 ≈ 6.8 vecesmenos remapeo con consistent hashing (casi 7×). - (c) Porque
mod-NremapeaN/(N+1), que crece hacia el 100% con más nodos (de 8 a 9: 88.9%; de 100 a 101: 99.0%), mientras que consistent hashing remapea~1/(N+1), que decrece hacia 0% (de 100 a 101: ~1%). Cuanto más grande el sistema,mod-Nempeora y consistent hashing mejora, así que la brecha se ensancha. Justo cuando Enlace es más grande —y mover sus datos es más caro—,mod-Ncastiga más y consistent hashing más ayuda. Por eso el diseño elige consistent hashing: convierte "crecer" de tormenta en rutina, y la ventaja aumenta con la escala.
Ejercicio 3 — Justifica (o rechaza) la consistencia fuerte. Un compañero propone que Enlace use consistencia fuerte: cada resolve lee del primary del shard para garantizar el dato más reciente. (a) ¿Qué gana con eso? (b) ¿Qué pierde, con números? (c) ¿Le conviene a Enlace? Responde con la naturaleza del dato.
Ver solución
- (a) Gana: garantía de que toda lectura ve la escritura más reciente al instante —cero replication lag, cero 404 fugaces por un link recién creado—.
- (b) Pierde, con números: todas las lecturas irían al primary del shard, no a las réplicas. El primary, que solo tenía que aguantar ~40 escrituras/s repartidas, ahora recibiría también las 386 lecturas/s residuales (y ~1,157/s en el pico, y 3,858/s el día del miss masivo) —se convertiría en el cuello de botella, y las réplicas quedarían ociosas—. Se pierde toda la escala de lectura que las réplicas daban. Además, la "L" de PACELC: leer con confirmación fuerte añade latencia.
- (c) No le conviene a Enlace, y la razón es la naturaleza del dato: el mapeo
short_code → long_urles casi inmutable (no hayUPDATEs que diverjan), así que la única inconsistencia posible es un 404 fugaz de milisegundos para un link recién creado —benigno (no es el dato equivocado, es "todavía no lo tengo") y rarísimo (el creador no visita su link al instante desde otra máquina)—. Pagar toda la escala de lectura para evitar un 404 de milisegundos que casi nunca ocurre es un pésimo tradeoff. La consistencia eventual es correcta porque el dato no cambia: no hay nada grave que la consistencia fuerte proteja. Donde el dato sí cambia (un precio, un saldo), la conversación sería otra; en Enlace, no.
La lección: la consistencia fuerte no es "más responsable", es más cara, y solo se paga donde el dato lo exige. Elegir la consistencia según la naturaleza del dato —y no por prudencia genérica— es lo que separa un diseño que escala de uno que se estrangula por miedo al lag.
Resumen y siguiente paso
En esta lección cerraste el paso 4 juntando las tres decisiones de escala distribuida de Enlace. Escalaste los datos en dos ejes: réplicas para absorber los 386 lecturas/s residuales (1–2 mínimas, 2–3 en producción por redundancia y por el pico), con el asterisco del replication lag; y sharding por short_code con consistent hashing, que ejecutaste —remapea 130,623 claves (13.1%) al crecer, no 888,920 (88.9%) como mod-N—, más los vnodes para el reparto parejo. Escalaste el cómputo con balanceo (round-robin basta para Enlace) sobre servidores sin estado, con health checks que tapan el último punto único de fallo. Y coronaste el capstone con el tradeoff de consistencia: eventual para resolve, justificado por la naturaleza casi inmutable del dato (CAP → disponibilidad, PACELC → latencia), con el SLO de 99.9% (~8.76 h/año) como número que exige la redundancia. Todo se condensó en la lista de tradeoffs, el artefacto que demuestra que el diseño entendió el problema.
Con esto, los cuatro artefactos del entregable están completos: requisitos (lección 2), tabla de capacidad (lección 3), diseño de alto nivel y diagrama (lección 4), y el profundizar con la lista de tradeoffs (lecciones 5, 6, 7). El recorrido de vuelta llegó al final del mapa.
Antes de avanzar deberías poder: distinguir los tres ejes de escala (réplicas, sharding, balanceo); ejecutar el contraste consistent hashing vs mod-N y explicar por qué la brecha crece con N; elegir la estrategia de balanceo para Enlace; y justificar la consistencia eventual con la naturaleza del dato y los números.
Lo que sigue es el proyecto final. En la lección 8 juntas los cuatro artefactos en un solo entregable: el enunciado del proyecto, la rúbrica con la que se evalúa, y una solución de referencia completa —el diagrama, la tabla de capacidad ejecutada, y la lista de tradeoffs justificados— para que compares tu diseño con uno defendible. Y cierras la guía: el resumen de los ocho módulos, y hacia dónde seguir en el ecosistema de guías hermanas.
Recursos
- Designing Data-Intensive Applications, Kleppmann — Capítulos 5 (Replication) y 6 (Partitioning) — el tratamiento de referencia de los dos ejes de escala de datos: replicación (primary/replica, el lag, síncrona vs asíncrona) y particionado/sharding (por rango vs por hash, el rebalanceo). El fundamento teórico de toda la primera mitad de esta lección.
- Consistent Hashing and Random Trees — Karger et al., 1997 (paper original, MIT/Princeton) — el paper que inventó consistent hashing precisamente para resolver el remapeo de
mod-N; su formalización del anillo y de la propiedad K/N es la fuente del experimento que ejecutaste. - Martin Kleppmann — "Please stop calling databases CP or AP" y el marco PACELC — la discusión honesta sobre los matices de CAP y por qué PACELC (que añade el tradeoff latencia-vs-consistencia en operación normal) es un marco más útil para decidir la consistencia de un sistema como Enlace. El fundamento de la elección "eventual, asíncrona".