Módulo 7: Fiabilidad y el tradeoff de consistencia

8. Proyecto: elige y justifica el modelo de consistencia de Enlace y su SLO

Descripción

Llegó el momento de juntar las siete lecciones en un solo entregable. En este proyecto vas a hacer lo que hace un ingeniero cuando le toca cerrar la parte de fiabilidad y consistencia de un diseño: tomar decisiones y justificarlas con números. No "hagámoslo confiable y ya", sino un documento defendible que responde cuatro preguntas concretas, cada una respaldada por una lección del módulo y por aritmética que se ejecuta:

  1. El plan de fiabilidad: ¿cuáles son los single points of failure de Enlace y cómo los elimino? (lecciones 2 y 3)
  2. La clasificación CAP/PACELC: ¿qué es Enlace bajo partición y qué es el resto del tiempo? (lecciones 4 y 5)
  3. El modelo de consistencia: ¿qué garantía le pongo a cada operación, y por qué la eventual alcanza para resolve? (lección 6)
  4. El SLO: ¿qué SLI mido, qué objetivo apunto, cuánto error budget me da, y me lo permite mi techo de dependencias? (lección 7)

El entregable es un documento de diseño de fiabilidad —un diagrama de la topología con sus SPOF marcados, una tabla de decisiones de consistencia por operación, y una hoja de SLO con los números ejecutados—. Vas a construir una pequeña calculadora en Python que produce la hoja de SLO a partir de tus decisiones, correrla, y defender cada elección. Al terminar tendrás una plantilla que sirve para razonar la fiabilidad de cualquier sistema, no solo la de Enlace.

Conexión con el módulo: este es el capstone. Usa los SPOF y la redundancia de la lección 2, el failover de la 3, el CAP correcto de la 4, el PACELC de la 5, la consistencia eventual medida de la 6 y los nines/error budget de la 7 —todo a la vez— para producir un artefacto real. Es también el puente al capstone de la guía completa (módulo 8, module-08-project-design-enlace-end-to-end): las decisiones de fiabilidad y consistencia que produzcas aquí son uno de los componentes que ese diseño end-to-end integra con la estimación, la caché, las réplicas y el balanceo de los módulos anteriores.

El brief del proyecto

Eres el ingeniero a cargo de la fiabilidad y la consistencia de Enlace. El equipo te da los números-ancla —los mismos de toda la guía— y la topología escalada de los módulos 4 a 6, y te pide un documento de diseño. Estos son los datos de entrada, fijos:

Dato de entradaValorDe dónde sale
Lecturas por segundo (qps_read)~4,000ratio 100:1 (módulo 2)
Escrituras por segundo (qps_write)~40módulo 2
Ratio lectura:escritura100:1requisito canónico
Espacio de códigosbase62⁷ ≈ 3.5×10¹²módulo 3
Replication lag de peor caso~2 smódulo 5
Disponibilidad de dependenciascompute 99.95%, db 99.9%, CDN 99.99%proveedores (dato del brief)

Y estas son las decisiones que tú tomas (las palancas de diseño): qué componentes hacer redundantes, qué configuración de failover para cada uno, la clasificación CAP/PACELC, el modelo de consistencia por operación, y el SLI/SLO/SLA con su objetivo numérico. El proyecto consiste en elegir cada una, justificarla, y calcular las consecuencias.

Los pasos del diseño

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

Paso 1 — Mapea los SPOF y su eliminación

Dibuja la topología de Enlace (balanceador → apps → caché + primary/réplicas) y marca cada single point of failure. Para cada uno, decide la redundancia (lección 2) y la configuración de failover (lección 3: active-active para lo stateless, active-passive para la base de datos). Recuerda la regla N+1: una unidad de repuesto más de las que necesitas.

Paso 2 — Clasifica Enlace en CAP y PACELC

Decide la rama "if Partition" (¿AP o CP?) y la rama "Else" (¿EL o EC?) para el camino de resolve, y justifica cada una con el costo de equivocarse. Recuerda que la clasificación puede ser por operación: resolve (lectura) y la unicidad de shorten (escritura) no tienen por qué ser iguales.

Paso 3 — Elige el modelo de consistencia por operación

Para cada operación de Enlace —resolve, la lectura del creador recién después de shorten, y la unicidad del short_code al generarlo— elige el modelo (eventual, read-your-writes, fuerte-por-construcción) y justifícalo. Apóyate en la cuenta de la ventana de propagación de la lección 6 (cuántos códigos en vuelo, qué probabilidad de colisión).

Paso 4 — Fija el SLI, el SLO y el SLA, y verifica el techo

Elige el SLI (qué mides), el SLO (tu meta interna) y el SLA (lo que prometes, ≤ SLO). Calcula el error budget del SLO. Y —crucial— verifica el techo de dependencias: multiplica la disponibilidad de tus dependencias en serie y comprueba si te da margen para el SLO; si no, propón el cambio de arquitectura que lo suba (lección 2).

Solución de referencia

Aquí está un documento de diseño completo y defendible. No es la única respuesta correcta —otro SLO objetivo o una redundancia distinta también se defienden—, pero es una propuesta sólida con cada decisión justificada.

Entregable 1 — El plan de fiabilidad

La topología con sus SPOF marcados y su remedio:

                       ┌──────────────────┐
   clientes ─────────► │  load balancer   │  SPOF ► redundante: 2 balanceadores
                       │  (x2, VIP flotante) │        con IP virtual flotante,
                       └────────┬─────────┘        failover a nivel de red (rapido)
                    ┌───────────┼───────────┐
                    ▼           ▼           ▼
                ┌───────┐   ┌───────┐   ┌───────┐   no-SPOF: stateless (modulo 6),
                │ app 1 │   │ app 2 │   │ app 3 │   active-active, N+1 (una de mas).
                └───┬───┘   └───┬───┘   └───┬───┘   Failover trivial: el LB saca la
                    └───────────┼───────────┘        caida por health check.
                    ┌───────────┼───────────┐
                    ▼           ▼           ▼
                ┌───────┐   ┌────────┐  ┌────────┐  base de datos: primary + replicas,
                │ cache │   │primary │─►│replica │  active-passive. Failover: promover
                │ (aside)│  │        │  │ (x N+1)│  una replica al dia (lecciones 3, 5).
                └───────┘   └────────┘  └────────┘  Cache-aside: si la cache cae, las
                                                    apps van a la db (degrada, no tumba).
Componente¿SPOF?RemedioConfiguración de failover
Load balancerSí (en la puerta)2 balanceadores + IP virtual flotanteFailover de red, rápido (lo más crítico)
Servidores de appNoYa redundantes (stateless), N+1Active-active: el LB saca la caída (trivial)
CachéParcialCache-aside: si cae, las apps van a la dbDegrada la latencia, no tumba el servicio
Primary de la dbRéplicas listas para promoverActive-passive: promover réplica (a diseñar con cuidado por split-brain → guía de resiliencia)
Réplicas de lecturaNoN+1 réplicasActive-active entre ellas: el LB de lecturas reparte

Nota clave heredada de la asimetría de Enlace: durante un failover del primary, las réplicas siguen sirviendo lecturas, así que solo se degradan las escrituras (shorten, el 1% del tráfico). Un failover del primary es una degradación menor, no una caída total.

Entregable 2 — La clasificación CAP/PACELC

Enlace (camino de resolve) es PA/EL.

  • if Partition → PA (disponible). Bajo una partición de red entre el primary y una réplica, resolve sigue respondiendo con la copia que tenga, aunque pueda estar unos segundos vieja. Costo de la alternativa (CP): negarse a redirigir a un usuario que hizo clic —error o espera—, una experiencia mucho peor que una URL levemente desactualizada (lección 4).
  • Else → EL (baja latencia). Con la red sana —el 99.99% del tiempo— cada resolve se sirve de la réplica más cercana en ~1 ms, sin coordinación. Pagar coordinación (EC) costaría ~14 ms extra por lectura, en 4,000 lecturas/s, para garantizar una exactitud que un redirect casi nunca necesita (lección 5).

Es la misma clase que Cassandra y DynamoDB, y no por casualidad: Enlace tiene el perfil arquetípico —lectura pesada, datos tolerantes al desfase, prioridad en responder siempre y rápido— para el que esos sistemas existen.

Entregable 3 — El modelo de consistencia, por operación

OperaciónModeloJustificación
resolve (lectura general)EventualSolo ~80 short_code en vuelo en cualquier instante; probabilidad 2×10⁻¹¹ de que una lectura al azar pegue a uno; y un código recién creado nadie más lo conoce todavía. La eventual sobra (lección 6).
Lectura del creador tras shortenRead-your-writesEl único caso stale real es el creador resolviendo su enlace antes de que propague. Se sirven sus lecturas desde el primary por un ratito. No hace falta consistencia fuerte para todos, solo esta garantía para el autor.
Unicidad del short_code al generarloFuerte por construcciónAquí la eventual NO alcanza: dos códigos iguales para URLs distintas es corrupción irreversible. Se garantiza por diseño —rangos disjuntos por nodo (id_generator, módulo 3)— para que dos nodos nunca colisionen, sin coordinar en caliente.
Contador de clicks (si existe)EventualUn conteo unos segundos atrasado no le hace daño a nadie; coordinar cada incremento a 4,000/s sería un cuello de botella innecesario.

La lección transversal: la consistencia se elige por operación, no para todo el sistema. Enlace es casi por completo eventual, con read-your-writes para el autor y una garantía fuerte-por-construcción solo en la unicidad.

Entregable 4 — La hoja de SLO, ejecutada

Primero la calculadora que produce los números:

# enlace_reliability_and_slo.py — la hoja de SLO, ejecutada
SECONDS_PER_YEAR  = 365 * 24 * 3600
SECONDS_PER_MONTH = 30  * 24 * 3600

def downtime(a, period):
    return (1 - a) * period

def human(seconds):
    if seconds >= 3600:  return f"{seconds/3600:.2f} h"
    if seconds >= 60:    return f"{seconds/60:.1f} min"
    return f"{seconds:.1f} s"

def parallel(a, n):
    return 1 - (1 - a) ** n

# --- El SLO elegido y su traduccion ---
slo = 0.999                                   # objetivo interno para resolve
print("SLO de Enlace en resolve:")
print(f"  objetivo = {slo*100}%   downtime = {human(downtime(slo, SECONDS_PER_YEAR))}/anio")
print(f"  error budget = {human(downtime(slo, SECONDS_PER_MONTH))}/mes")

# --- El techo de dependencias, antes y despues de redundar la db ---
compute, db, cdn = 0.9995, 0.999, 0.9999
ceiling_before = compute * db * cdn
print(f"\nTecho de dependencias (en serie):")
print(f"  antes:  {ceiling_before*100:.3f}%  ->  {'ALCANZA' if ceiling_before>=slo else 'NO ALCANZA'} para SLO {slo*100}%")

db_redundant = parallel(0.999, 2)             # dos db al 99.9% en paralelo (leccion 2)
ceiling_after = compute * db_redundant * cdn
print(f"  db redundante ({db_redundant*100:.4f}%):")
print(f"  despues: {ceiling_after*100:.3f}%  ->  {'ALCANZA' if ceiling_after>=slo else 'NO ALCANZA'} para SLO {slo*100}% con margen")

Qué esperar. Al correrlo:

SLO de Enlace en resolve:
  objetivo = 99.9%   downtime = 8.76 h/anio
  error budget = 43.2 min/mes

Techo de dependencias (en serie):
  antes:  99.840%  ->  NO ALCANZA para SLO 99.9%
  db redundante (99.9999%):
  despues: 99.940%  ->  ALCANZA para SLO 99.9% con margen

Y la hoja de SLO que resume la decisión:

ElementoValorJustificación
SLI% de resolve con éxito en <100 ms, mensualresolve es el 99% del tráfico y lo que le importa al usuario (ser redirigido rápido)
SLO99.9% (43.2 min/mes de error budget)Serio pero alcanzable; absorbe failovers y despliegues. Perseguir 99.99% multiplicaría el costo sin necesidad para un acortador
SLA99.5% hacia clientes de pago≤ SLO, para tener colchón: un mes malo rompe el SLO (alarma interna) sin romper el SLA (sin multa)
Techo de dependencias99.84% en serie → no alcanzaHay que redundar la db (el eslabón débil al 99.9%): sube el techo a 99.94%, ahora con margen sobre el SLO

El hallazgo más importante del cálculo: el SLO de 99.9% no es sostenible con las dependencias en serie tal como están (techo 99.84%, por debajo del objetivo). La acción que lo arregla es la de la lección 2: hacer redundante la base de datos, el eslabón más débil, que sube el techo a 99.94% y da margen. Un SLO sin verificar el techo de dependencias es una promesa que no controlas; verificarlo convierte el proyecto en un diseño honesto.

Errores comunes

Elegir "todo fuerte" o "todo eventual" para simplificar (de granularidad). Qué pasa: alguien quiere una sola etiqueta para todo Enlace y aplica el mismo modelo a la lectura y a la generación de códigos —abriendo la puerta a colisiones si elige eventual, o encareciendo el resolve sin necesidad si elige fuerte—. Por qué pasa: una etiqueta única es más cómoda de comunicar. Cómo detectarlo: si operaciones con distinto costo de incoherencia (un redirect viejo es inofensivo; un código duplicado es corrupción) reciben el mismo modelo, estás mal calibrado. Cómo corregirlo: el entregable 3 es una tabla por operación, no una etiqueta única.

Prometer un SLO sin verificar el techo de dependencias (de arquitectura). Qué pasa: se fija un SLO de 99.9% y se olvida calcular el producto de las dependencias en serie, garantizando que el sistema romperá el objetivo por causas externas al propio código. Por qué pasa: se mira la disponibilidad del servicio propio y no la cadena completa. Cómo detectarlo: si tu SLO es mayor que el producto de las disponibilidades de todo lo que está en el camino crítico, ya perdiste. Cómo corregirlo: haz el cálculo del techo antes de comprometer el SLO (como en el entregable 4) y, si no alcanza, redunda o saca del camino el eslabón más débil.

Diseñar el failover del primary de forma ingenua (de fiabilidad). Qué pasa: el plan dice "si el primary no responde, promover una réplica automáticamente", sin ningún mecanismo contra el split-brain —el primary podría estar vivo pero aislado, y quedar dos primaries escribiendo—. Por qué pasa: la promoción automática se ve simple. Cómo detectarlo: si tu failover no nombra fencing ni consenso, tiene el agujero del split-brain. Cómo corregirlo: en este nivel de diseño, reconocer que el failover del primary necesita fencing y elección de líder por consenso, y marcarlo como frontera con la guía de resiliencia —no dejarlo como un promover y listo—.

Ejercicios

Ejercicio 1 — Defiende tu SLO. Un directivo te dice: "99.9% permite 8.76 horas de caída al año. Eso me parece muchísimo. Quiero que prometamos 99.999% (cinco nueves)". Responde con dos argumentos técnicos y de costo por los que cinco nueves no es apropiado para Enlace, apoyándote en la tabla de la lección 7 y en el techo de dependencias.

Ver solución
  1. El costo por nueve crece ~10× y no lo compra el caso de uso. Pasar de 99.9% (8.76 h/año) a 99.999% (5.3 min/año) son tres nueves más: cada uno divide el downtime entre diez y multiplica el esfuerzo (failover 100% automático sin intervención humana, redundancia multi-región, guardias 24/7). Para un acortador de URLs —donde una URL que tarda un momento en redirigir, muy de vez en cuando, no causa daño real— ese gasto es desproporcionado. Cinco nueves es el estándar de la telefonía de emergencia, no de un redirect.
  2. El techo de dependencias lo hace imposible sin rearquitectura mayor. Las dependencias en serie de Enlace dan un techo de 99.84% (o 99.94% redundando la db). Prometer 99.999% (que exige un techo de al menos 99.999%) requeriría hacer redundante cada dependencia en múltiples regiones y proveedores, un costo enorme —y aun así, 5.3 min/año no deja margen para ningún error humano—. Prometerlo sin ese trabajo sería firmar un SLA que romperemos el primer mes por causas fuera de nuestro control.

Contrapropuesta: 99.9% como SLO interno con la db redundante (techo 99.94%, con margen), y un SLA hacia clientes por debajo (99.5%) para tener colchón. Serio, alcanzable y honesto.

Ejercicio 2 — Recalcula el techo. El equipo de infraestructura mejora el proveedor de cómputo de 99.95% a 99.99%, pero la base de datos sigue siendo un primary único al 99.9% y el CDN 99.99%. (a) Calcula el nuevo techo de dependencias en serie. (b) ¿Alcanza para un SLO de 99.9%? (c) ¿Qué eslabón sigue mandando y qué harías?

Ver solución
  • (a) Techo = 0.9999 × 0.999 × 0.9999 = 0.998801, o sea 99.88%.
  • (b) No alcanza. 99.88% está por debajo del SLO de 99.9%. Mejorar el cómputo casi no movió el techo.
  • (c) Sigue mandando la base de datos (el eslabón más débil, 99.9%), exactamente como la regla del eslabón débil de la lección 2: en el producto, el término más pequeño domina, así que mejorar el cómputo (que ya era fuerte) apenas cambia el resultado. Lo que hay que hacer es redundar la base de datos: dos al 99.9% en paralelo dan 99.9999%, y el techo sube a 0.9999 × 0.999999 × 0.9999 ≈ 0.99980, o sea 99.98%, ahora con margen sobre el SLO. Siempre redunda primero el eslabón más débil.

Ejercicio 3 — Diseña la fiabilidad de una feature nueva. Enlace agrega enlaces "premium" con expiración: un short_code que deja de funcionar en una fecha expires_at. Un cliente de pago exige que, en el instante exacto en que un enlace expira, deje de redirigir en todo el mundo. Analiza: (a) ¿qué modelo de consistencia exige ese requisito? (b) ¿choca con la clasificación PA/EL de Enlace? (c) propón un diseño que cumpla el requisito sin volver fuertemente consistente todo el resolve.

Ver solución
  • (a) "En el instante exacto, en todo el mundo, deja de redirigir" es un requisito de consistencia fuerte (linearizabilidad): toda réplica, preguntada justo después de la expiración, debe coincidir en que el enlace ya no vale. Es lo contrario de la eventual, donde distintas réplicas podrían diferir por unos segundos.
  • (b) Sí, choca. Enlace es PA/EL justamente porque resolve tolera el desfase; un requisito de expiración instantánea y global pide coordinación que PA/EL evita a propósito. Cumplirlo con consistencia fuerte en cada resolve mataría la baja latencia de las 4,000 lecturas/s.
  • (c) El diseño que reconcilia ambos: no volver fuerte todo el resolve, sino acotar el requisito. Opciones: (1) Aceptar una ventana de tolerancia —negociar con el cliente que la expiración es efectiva "en segundos", no "en el nanosegundo exacto"—, lo que mantiene la eventual y probablemente satisface la intención real. (2) Si el instante exacto es innegociable, tratar la expiración como una escritura fuertemente consistente (invalidar el short_code en el primary y en la caché de forma coordinada) pero mantener el resolve eventual para todo lo demás: solo los enlaces premium con expiración pagan el costo de la coordinación, y solo en el momento de expirar, no en cada lectura. Es, otra vez, elegir la consistencia por operación y por caso: el 99.99% del tráfico sigue siendo eventual y rápido; solo esta feature específica paga por la garantía que de verdad necesita.

Resumen y siguiente paso

En este proyecto juntaste las seis lecciones en un documento de diseño defendible para Enlace. Produjiste los cuatro entregables: el plan de fiabilidad (los SPOF marcados en el diagrama —balanceador y primary— y su remedio con redundancia y failover, recordando que un failover del primary solo degrada las escrituras gracias a la asimetría 100:1); la clasificación CAP/PACELC (PA/EL, disponible bajo partición y rápido el resto del tiempo, justificado con el costo de cada alternativa); el modelo de consistencia por operación (eventual para resolve, read-your-writes para el creador, fuerte-por-construcción para la unicidad); y la hoja de SLO ejecutada (SLI de resolve, SLO de 99.9% con 43.2 min/mes de error budget, SLA por debajo para tener colchón, y la verificación del techo de dependencias que reveló que hay que redundar la base de datos para sostenerlo). El hallazgo que amarra todo: un SLO sin verificar el techo de dependencias es una promesa que no controlas.

Con esto cierras el módulo 7. Ya sabes razonar, con números y sin mitos, la fiabilidad de un sistema (SPOF, redundancia, failover), su tradeoff de consistencia (CAP, PACELC, fuerte contra eventual) y su promesa de disponibilidad (SLI/SLO/SLA, nines, error budget). Sabes encontrar el eslabón más débil, elegir la consistencia por operación, y traducir un porcentaje en horas de caída honestas.

Lo que sigue es el capstone de toda la guía. En el módulo 8 (module-08-project-design-enlace-end-to-end) vas a integrar todo lo que construiste a lo largo de ocho módulos —cómo abordar el problema (M1), la estimación de capacidad (M2), el modelo de datos y la generación de IDs (M3), la caché (M4), las réplicas y el sharding (M5), el balanceo y la falta de estado (M6), y la fiabilidad y consistencia que acabas de cerrar (M7)— en un solo diseño de Enlace de punta a punta: el diagrama, la tabla de capacidad, el camino de lectura y de escritura, y la lista de tradeoffs justificados. Las decisiones de fiabilidad y consistencia de este proyecto son una de las piezas que ese diseño final ensambla. Y desde ahí, la guía te apunta hacia sus hermanas del ecosistema —resiliencia, eventos, estilos arquitectónicos— para seguir profundizando en cada frontera que aquí marcamos.

Recursos