La lección anterior terminó con una tabla en la que Kilómetro Cero asignaba un modelo de consistencia distinto a cada dato: linealizabilidad para la última unidad de queso-curado, garantías de sesión para el carrito de Ana, consistencia eventual para el contador de visitas. Quedó pendiente la pregunta obvia: si la linealizabilidad es la garantía más cómoda para el programador, ¿por qué no dársela a todo? La respuesta es que tiene un precio que no se paga en euros sino en disponibilidad cuando la red falla y en latencia cuando no falla, y ese precio está formalizado en dos resultados: el teorema CAP y su extensión PACELC.
Pocos resultados de la informática se citan tanto y se entienden tan poco como CAP. Esta lección lo enuncia con la precisión con que lo demostraron Gilbert y Lynch, explica por qué la "P" no es una opción que se pueda descartar y por qué la elección real es qué sacrificar durante una partición, y desmonta los malentendidos más extendidos ("elige dos de tres", "AP significa sin consistencia"). Después presentaremos PACELC, que añade lo que a CAP le falta (el compromiso entre latencia y consistencia cuando todo funciona), y clasificaremos con él los sistemas que aparecerán en el resto del curso. Aplicaremos ambos a los datos de Kilómetro Cero con una tabla de decisiones justificadas, y una simulación en Python mostrará el mismo par de réplicas inv-bcn/inv-vlc comportándose en modo CP (rechazando escrituras sin quórum) y en modo AP (aceptando, divergiendo y perdiendo una reserva al reconciliar). Terminaremos con las críticas modernas al teorema, que no lo invalidan pero sí obligan a usarlo con más cuidado. Los mecanismos concretos con los que un sistema CP se pone de acuerdo (consenso) y los quórums de lectura y escritura son las dos lecciones siguientes.
Contenido
- El enunciado preciso de CAP
- Por qué la partición no es opcional
- La elección real: CP o AP durante una partición
- Malentendidos comunes
- PACELC: lo que pasa cuando no hay partición
- Tabla de sistemas clasificados
- Decidir por caso de uso en Kilómetro Cero
- Simulación: el mismo sistema en modo CP y en modo AP
- Críticas y matices: harvest, yield y "dejad de llamar CP o AP a las bases de datos"
- Errores comunes y consejos
- Ejercicios
- Conclusión
- El enunciado preciso de CAP
Eric Brewer presentó CAP como conjetura en una charla del año 2000; Seth Gilbert y Nancy Lynch lo demostraron formalmente en 2002. La demostración es sencilla, pero solo si las tres letras están bien definidas, y aquí es donde empieza la confusión, porque en el uso coloquial cada una significa algo más vago de lo que el teorema dice:
| Letra | Nombre coloquial | Definición en Gilbert-Lynch |
|---|---|---|
| C | Consistencia | Linealizabilidad (03-01): existe un orden total de las operaciones que respeta el tiempo real y en el que cada lectura devuelve la última escritura. Nada que ver con la C de ACID. |
| A | Disponibilidad | Toda petición recibida por un nodo que no ha fallado debe producir una respuesta (no un error, no un timeout), en tiempo finito. Observa que es una propiedad de cada nodo vivo, no del sistema "en conjunto": si un nodo vivo responde "no puedo atenderte ahora", el sistema no es disponible en el sentido CAP, aunque otro nodo sí pudiera. |
| P | Tolerancia a particiones | La red puede perder arbitrariamente mensajes entre nodos (una partición: dos grupos de nodos vivos que no pueden comunicarse). El sistema debe seguir cumpliendo sus garantías aunque esto ocurra. |
Con esas definiciones, el teorema dice: en un sistema distribuido asíncrono en el que puede haber particiones, es imposible garantizar simultáneamente linealizabilidad y disponibilidad.
La demostración cabe en un párrafo. Sea un registro con valor inicial v0 replicado en dos nodos, N1 y N2, y una partición que separa a ambos. Un cliente escribe v1 en N1. Por disponibilidad, N1 debe responder "hecho" sin esperar a N2 (con quien no puede hablar). Otro cliente lee después en N2. Por disponibilidad, N2 debe responder; pero N2 no ha recibido nada de N1, así que solo puede responder v0. Ese historial (escritura de v1 terminada, lectura posterior que devuelve v0) es exactamente el historial 1 de 03-01: no es linealizable. Luego, o N1 no responde (sacrifica A), o N2 devuelve un valor antiguo (sacrifica C). No hay tercera opción.
sequenceDiagram
participant Ana
participant N1 as inv-bcn
participant N2 as inv-vlc
participant Marc
Note over N1,N2: Partición: ningún mensaje pasa entre inv-bcn e inv-vlc
Ana->>N1: escribir stock = 0
N1-->>Ana: ok (si es disponible, no puede esperar a N2)
N1--xN2: replicar stock = 0 (se pierde)
Marc->>N2: leer stock
N2-->>Marc: 1 (si es disponible, solo tiene el valor antiguo)
Note over Ana,Marc: Ana terminó antes de que Marc empezara y Marc leyó el valor viejo: no linealizable
- Por qué la partición no es opcional
Una lectura ingenua del teorema ("elige dos de C, A y P") sugiere que se puede renunciar a P y quedarse con C y A. En un sistema distribuido real esa opción no existe, por dos razones que ya conocemos del Módulo 1:
- La red es imperfecta (falacia 1 de 01-04). Cables cortados, switches que se reinician, tablas de rutas mal aplicadas, un firewall que descarta paquetes, un nodo tan sobrecargado que no responde a tiempo (recuerda que en un sistema asíncrono, 01-02, "lento" y "particionado" son indistinguibles). La pregunta no es si habrá particiones sino cuántas veces al año.
- Renunciar a P significa que, cuando ocurre una partición, el sistema deja de cumplir C, A o ambas de forma no controlada. Es decir, "CA" no es una elección de diseño sino la ausencia de una: el sistema se comportará de alguna manera durante la partición, y si el diseñador no la ha decidido, será la peor.
Lo único que sí se puede hacer es reducir la probabilidad y el alcance de las particiones: nodos en el mismo rack, redes redundantes, o directamente un solo nodo. Una base de datos PostgreSQL en una sola máquina es "CA" en el sentido trivial de que no hay nada que particionar, y por eso el monolito de Kilómetro Cero de 01-06 nunca tuvo que pensar en esto. En cuanto inventario tiene inv-bcn en Barcelona e inv-vlc en Valencia, unidas por 350 km de fibra que no controla, P está decidida.
- La elección real: CP o AP durante una partición
Con P fijada, el teorema se reduce a una decisión: cuando la partición ocurre, ¿qué hace un nodo que no puede hablar con los demás?
- CP (consistencia sobre disponibilidad): el nodo que no puede coordinarse rechaza la operación (o la bloquea hasta que la partición se cure). En el diagrama, N1 respondería a Ana "no puedo confirmar la reserva ahora", y N2 respondería a Marc "no puedo garantizarte un valor actual". Nadie lee datos antiguos, pero parte del sistema, o todo, deja de servir. En la práctica, un sistema CP suele dejar operativo al lado de la partición que tiene mayoría (quórum) y apagar al minoritario; cómo se decide quién tiene mayoría es el consenso de 03-03.
- AP (disponibilidad sobre consistencia): ambos nodos siguen respondiendo con lo que tienen. Ana reserva en N1, Marc lee (y quizá reserva) en N2, cada lado acumula escrituras que el otro no ve, y cuando la partición se cura hay que reconciliar: decidir qué pasa con dos reservas del mismo último queso. La reconciliación puede ser tan simple como "gana la última" (con pérdida de datos) o tan sofisticada como un CRDT de 03-01 (sin pérdida, pero sin invariantes).
Fíjate en que la elección no es global ni permanente: es por operación y durante la partición. Un sistema puede ser CP para las escrituras y AP para las lecturas; puede ser CP para el stock y AP para el carrito; y fuera de la partición, ambas letras se cumplen sin conflicto. Un sistema bien diseñado se comporta de forma idéntica el 99,9 % del tiempo sea CP o AP; la etiqueta solo describe qué hace en el 0,1 % restante.
- Malentendidos comunes
| Malentendido | Por qué es falso |
|---|---|
| "Elige dos de tres" | P no se elige; se sufre. La elección es CP o AP, y solo durante la partición. |
| "AP significa sin consistencia" | AP significa sin linealizabilidad durante la partición. Un sistema AP puede ofrecer consistencia causal, garantías de sesión y consistencia eventual fuerte con CRDTs (todas las de la mitad inferior del diagrama de 03-01). De hecho, la consistencia causal es el modelo más fuerte compatible con A. |
| "CP significa que el sistema se cae en cuanto hay partición" | CP significa que algún nodo rechaza algunas peticiones. El lado mayoritario de la partición sigue funcionando; solo los nodos aislados dejan de servir. Con 5 nodos y uno aislado, el 80 % del sistema sigue siendo C y disponible en el sentido coloquial. |
| "Mi sistema es CA" | Solo si es un único nodo. Si hay dos nodos con red entre ellos, la partición es posible y el sistema tendrá un comportamiento (decidido o no) ante ella. |
| "La A de CAP es la disponibilidad de los 'nueves' de 01-03" | No. La A de CAP es una propiedad binaria y formal ("todo nodo vivo responde"), no un porcentaje de tiempo en servicio. Un sistema CP puede tener un 99,99 % de disponibilidad operativa si las particiones son raras y cortas. |
| "CAP decide la arquitectura entera" | CAP habla de un registro replicado durante una partición. No dice nada sobre latencia, sobre transacciones, sobre particionado de datos ni sobre tolerancia a fallos de nodos (que no son particiones). Por eso hace falta PACELC. |
- PACELC: lo que pasa cuando no hay partición
Daniel Abadi observó en 2012 que CAP ignora el estado normal del sistema. La mayor parte del tiempo no hay ninguna partición, y sin embargo los sistemas siguen tomando decisiones de compromiso: cada escritura linealizable tiene que coordinarse con otras réplicas antes de responder, y esa coordinación cuesta uno o varios viajes de red. PACELC lo formula así:
Si hay Partición, elegir entre Availability y Consistency; Else (si no la hay), elegir entre Latency y Consistency.
La segunda mitad es la que describe el día a día. Con inv-bcn e inv-vlc conectadas y sanas, una reserva linealizable (EC) requiere que inv-bcn espere la confirmación de inv-vlc antes de decir "hecho" a Ana: unos 10 ms de ida y vuelta Barcelona-Valencia, más en el percentil 99. Una reserva en modo EL responde en cuanto se escribe localmente y replica en segundo plano; Ana ve una respuesta en 1 ms, a cambio de que inv-vlc pueda estar unos milisegundos por detrás (la propagación retardada de la simulación de 03-01). Las cuatro combinaciones resultantes:
| Clasificación | Durante partición | Sin partición | Perfil |
|---|---|---|---|
| PA/EL | Disponible, diverge | Rápido, replicación asíncrona | Máxima disponibilidad y velocidad; el programa debe tolerar anomalías siempre |
| PA/EC | Disponible, diverge | Consistente, espera a las réplicas | Poco común: paga latencia normalmente pero renuncia a C bajo partición |
| PC/EL | Rechaza sin quórum | Rápido, replicación asíncrona | Común en bases de datos con líder y réplicas asíncronas: consistente vía el líder, pero los seguidores se retrasan |
| PC/EC | Rechaza sin quórum | Consistente, espera a las réplicas | Máxima seguridad; latencia de coordinación en toda operación |
PACELC no es un teorema sino un marco de clasificación, pero convierte la discusión abstracta en una pregunta concreta que se puede hacer a cualquier almacén: "¿qué haces durante una partición, y cuántos viajes de red haces por escritura cuando no la hay?".
- Tabla de sistemas clasificados
Los sistemas que aparecerán en el resto del curso, clasificados con PACELC. Muchos son configurables, y por eso la clasificación indica la configuración:
| Sistema | Configuración | PACELC | Comentario |
|---|---|---|---|
| PostgreSQL, un líder + réplicas asíncronas | Por defecto | PC/EL | Las escrituras van al líder (consistentes entre sí); las lecturas en réplicas pueden ser antiguas; si el líder queda aislado, deja de aceptar escrituras (o el failover produce split-brain, 03-04) |
| PostgreSQL, replicación síncrona | synchronous_standby_names |
PC/EC | Cada commit espera al standby; si el standby no responde, los commits se bloquean (03-04) |
| Cassandra | ONE/ANY en escritura y lectura |
PA/EL | Cualquier réplica acepta; reconciliación por marca de tiempo (LWW) |
| Cassandra | QUORUM en escritura y lectura |
PC/EC (por operación) | Con quórum se rechaza lo que no alcanza mayoría; latencia de esperar a varias réplicas (04-04) |
| DynamoDB | Lecturas eventualmente consistentes | PA/EL | El modo por defecto y el más barato |
| DynamoDB | Lecturas fuertemente consistentes | PC/EC | El doble de coste por lectura y sin disponibilidad multirregión |
| MongoDB | w:1, lectura del primario |
PC/EL (matizado) | El primario acepta escrituras; durante partición, el lado sin mayoría degrada su primario; escrituras w:1 pueden retroceder en un failover |
| MongoDB | w:majority, readConcern: linearizable |
PC/EC | Coordinación en cada operación |
| Google Spanner | Siempre | PC/EC | Serializabilidad estricta con TrueTime (01-05); Google argumenta que sus particiones son tan raras que "es CA en la práctica" |
| etcd / ZooKeeper / Consul | Siempre | PC/EC | Consenso (Raft/ZAB) en cada escritura; el lado sin mayoría rechaza; base de la coordinación (03-03) |
| Redis (un nodo) | — | Trivialmente C y A | No hay partición posible; con Redis Cluster o Sentinel pasa a ser PA/EL con posibles pérdidas en failover |
| DNS | — | PA/EL | El ejemplo clásico de consistencia eventual (los TTL) |
Dos observaciones sobre la tabla. Primera: la misma base de datos aparece en filas distintas, porque la elección se hace por configuración y a menudo por operación; esto es lo que Kleppmann critica en el apartado 9. Segunda: los sistemas de coordinación (etcd, ZooKeeper) son siempre PC/EC y aceptan el coste con gusto, porque su función es precisamente ser la fuente de verdad sobre quién es líder o cuál es la configuración vigente; un sistema de coordinación AP sería inútil.
- Decidir por caso de uso en Kilómetro Cero
Con el marco en la mano, volvamos a la tabla de 03-01 y justifiquemos cada elección pensando en qué pasa durante una partición entre Barcelona y Valencia (o entre la nube donde viven los servicios y las tiendas físicas de los productores) y en la latencia aceptable el resto del tiempo:
| Dato | Pregunta clave | Decisión | Justificación |
|---|---|---|---|
Stock de queso-curado (última unidad) |
¿Es peor vender lo que no hay o no poder vender durante la partición? | CP para la reserva; lecturas del catálogo AP | Vender un queso inexistente supone cancelar un pedido cobrado y una compensación (03-05); rechazar la reserva 30 segundos durante una partición es un "inténtalo de nuevo". Con muchas unidades, el mismo dato puede tratarse como AP (ver abajo) |
Stock de tomate-rosa (200 unidades) |
¿Qué daño hace una sobreventa? | AP con reconciliación por operaciones | Con margen de stock, aceptar reservas en ambos lados y sumar los descuentos al reconciliar (no LWW) rara vez produce un negativo; Kilómetro Cero puede definir un umbral: por debajo de 5 unidades, el producto pasa a modo CP |
| Carrito de Ana | ¿Qué pasa si Ana no puede añadir al carrito durante 30 segundos? | AP con garantías de sesión | Un carrito indisponible es una venta perdida; un carrito con un artículo duplicado tras reconciliar es una molestia que el usuario corrige. Se reconcilia por unión (nunca perder un artículo añadido, a lo Amazon) y con read-your-writes |
Pagos: cobro de P-2026-000124 |
¿Puede cobrarse dos veces o cobrarse sin registro? | CP | Un cobro es irreversible y regulado. Si pagos no puede confirmar con quórum, el pedido queda "pendiente de pago" (03-05) y se reintenta con la misma clave de idempotencia de 02-05. Latencia extra aceptada |
Posiciones de furgoneta-3 |
¿Importa perder o desordenar una posición? | AP (EL) | Diez posiciones por minuto; perder una es irrelevante y la siguiente la corrige. Coordinar cada posición sería absurdo. Consistencia secuencial por repartidor basta (03-01) |
| Contador de ventas de la "Semana de la Vendimia" | ¿Puede diferir unos segundos entre réplicas? | AP con CRDT (GCounter) |
Convergencia garantizada sin coordinación; los incrementos nunca se pierden |
| Configuración: quién es el relay outbox activo | ¿Puede haber dos a la vez? | CP (etcd) | Dos relays publicando duplican eventos; mejor ninguno unos segundos que dos. Es la elección de líder de 03-03 |
| Catálogo (nombres, precios, fotos) | ¿Cuánto puede tardar un cambio de precio en verse? | AP eventual | Un precio antiguo durante unos segundos es aceptable; el precio que se cobra se fija en pedidos al crear el pedido, no en el catálogo |
La regla que emerge: CP para lo que es irreversible o disputado (dinero, última unidad, liderazgo); AP para lo que se puede corregir, unir o simplemente ignorar (carritos, posiciones, contadores, catálogo). Y la fila de tomate-rosa muestra que la decisión puede depender del estado del dato, no solo de su tipo.
- Simulación: el mismo sistema en modo CP y en modo AP
Vamos a construir dos réplicas de inventario que pueden operar en cualquiera de los dos modos, y una red que se puede partir. En modo CP, una reserva solo se confirma si la aceptan ambas réplicas (con dos nodos, el "quórum" es la unanimidad; en 03-04 veremos quórums mayoritarios con N=3). En modo AP, cada réplica acepta localmente y anota la marca de tiempo; al curarse la partición, se reconcilia con last-write-wins.
# km0/simulaciones/particion_cp_ap.py
from dataclasses import dataclass, field
class SinQuorum(Exception):
"""El nodo no puede coordinarse con suficientes réplicas."""
class SinStock(Exception):
"""No quedan unidades."""
@dataclass
class Registro:
unidades: int
marca: int # instante de la última escritura (reloj de simulación)
origen: str # réplica que hizo la última escritura
@dataclass
class Replica:
nombre: str
modo: str # "CP" o "AP"
stock: dict[str, Registro] = field(default_factory=dict)
reservas: list[tuple[str, str]] = field(default_factory=list) # (cliente, producto) aceptadas aquí
class Red:
"""Conecta las réplicas; se puede partir y curar."""
def __init__(self, replicas: dict[str, Replica]):
self.replicas = replicas
self.particionada = False
def alcanzable(self, desde: str, hasta: str) -> bool:
return desde == hasta or not self.particionada
class Inventario:
def __init__(self, modo: str):
self.replicas = {n: Replica(n, modo) for n in ("inv-bcn", "inv-vlc")}
self.red = Red(self.replicas)
self.reloj = 0
# --- utilidades -------------------------------------------------------
def cargar(self, producto: str, unidades: int) -> None:
for r in self.replicas.values():
r.stock[producto] = Registro(unidades, self.reloj, "carga")
def _otras(self, nombre: str) -> list[Replica]:
return [r for n, r in self.replicas.items() if n != nombre]
# --- operación principal ---------------------------------------------
def reservar(self, cliente: str, replica: str, producto: str) -> str:
self.reloj += 1
local = self.replicas[replica]
if local.stock[producto].unidades <= 0:
raise SinStock(f"{replica}: no queda {producto}")
nuevo = Registro(local.stock[producto].unidades - 1, self.reloj, replica)
if local.modo == "CP":
# Solo confirmamos si TODAS las réplicas aceptan (quórum = unanimidad con N=2)
for otra in self._otras(replica):
if not self.red.alcanzable(replica, otra.nombre):
raise SinQuorum(f"{replica}: no alcanzo a {otra.nombre}; reserva rechazada")
for r in self.replicas.values(): # aplicar en todas, atómicamente
r.stock[producto] = nuevo
local.reservas.append((cliente, producto))
return f"{replica}: reserva de {producto} para {cliente} CONFIRMADA (stock={nuevo.unidades})"
# Modo AP: aceptar localmente y replicar si se puede; si no, ya se reconciliará
local.stock[producto] = nuevo
local.reservas.append((cliente, producto))
for otra in self._otras(replica):
if self.red.alcanzable(replica, otra.nombre):
otra.stock[producto] = nuevo
return f"{replica}: reserva de {producto} para {cliente} aceptada (stock local={nuevo.unidades})"
def leer(self, replica: str, producto: str) -> int:
return self.replicas[replica].stock[producto].unidades
# --- reconciliación tras la partición (solo tiene sentido en AP) -------
def reconciliar_lww(self, producto: str) -> str:
bcn, vlc = self.replicas["inv-bcn"], self.replicas["inv-vlc"]
a, b = bcn.stock[producto], vlc.stock[producto]
ganador = a if a.marca >= b.marca else b
bcn.stock[producto] = vlc.stock[producto] = ganador
return (f"LWW: gana la escritura de {ganador.origen} (marca {ganador.marca}); "
f"ambas réplicas quedan con stock={ganador.unidades}")
def escenario(modo: str) -> None:
print(f"\n===================== MODO {modo} =====================")
inv = Inventario(modo)
inv.cargar("queso-curado", 1) # el último queso curado de la Quesería Montblanc
print("stock inicial:", inv.leer("inv-bcn", "queso-curado"), "/", inv.leer("inv-vlc", "queso-curado"))
inv.red.particionada = True
print("-- partición entre inv-bcn e inv-vlc --")
for cliente, replica in (("Ana", "inv-bcn"), ("Marc", "inv-vlc")):
try:
print(inv.reservar(cliente, replica, "queso-curado"))
except (SinQuorum, SinStock) as e:
print(f"ERROR -> {e}")
print("durante la partición, lecturas:", inv.leer("inv-bcn", "queso-curado"), "/", inv.leer("inv-vlc", "queso-curado"))
inv.red.particionada = False
print("-- partición curada --")
if modo == "AP":
print(inv.reconciliar_lww("queso-curado"))
reservas = [(c, r.nombre) for r in inv.replicas.values() for c, _ in r.reservas]
print("reservas aceptadas:", reservas)
print("stock final:", inv.leer("inv-bcn", "queso-curado"), "/", inv.leer("inv-vlc", "queso-curado"))
if len(reservas) > 1:
print(f"!!! {len(reservas)} reservas de 1 unidad: el stock final debería ser {1 - len(reservas)}; "
f"LWW ha PERDIDO {len(reservas) - 1} reserva(s) y hay sobreventa")
if __name__ == "__main__":
escenario("CP")
escenario("AP")Salida:
===================== MODO CP =====================
stock inicial: 1 / 1
-- partición entre inv-bcn e inv-vlc --
ERROR -> inv-bcn: no alcanzo a inv-vlc; reserva rechazada
ERROR -> inv-vlc: no alcanzo a inv-bcn; reserva rechazada
durante la partición, lecturas: 1 / 1
-- partición curada --
reservas aceptadas: []
stock final: 1 / 1
===================== MODO AP =====================
stock inicial: 1 / 1
-- partición entre inv-bcn e inv-vlc --
inv-bcn: reserva de queso-curado para Ana aceptada (stock local=0)
inv-vlc: reserva de queso-curado para Marc aceptada (stock local=0)
durante la partición, lecturas: 0 / 0
-- partición curada --
LWW: gana la escritura de inv-vlc (marca 2); ambas réplicas quedan con stock=0
reservas aceptadas: [('Ana', 'inv-bcn'), ('Marc', 'inv-vlc')]
stock final: 0 / 0
!!! 2 reservas de 1 unidad: el stock final debería ser -1; LWW ha PERDIDO 1 reserva(s) y hay sobreventaQué está pasando, paso a paso:
- Modo CP: durante la partición,
reservarcomprueba si alcanza a la otra réplica antes de tocar nada. No la alcanza, y lanzaSinQuorum: ni Ana ni Marc pueden reservar. El sistema ha sacrificado disponibilidad (dos nodos vivos que responden con error) y ha conservado la linealizabilidad: no hay historial anómalo posible porque no ha habido escrituras. Al curarse la partición no hay nada que reconciliar. Con N=2, este modo es frágil: cualquier partición apaga todo el sistema. Con N=3 y quórum de mayoría (03-03 y 03-04), el lado con dos nodos seguiría reservando. - Modo AP: cada réplica acepta la reserva de su cliente y descuenta su copia local. Ambas responden, ambas quedan a cero, y ambas creen haber vendido el último queso: la lectura "0 / 0" parece coherente pero esconde dos reservas. Al reconciliar con LWW, el sistema compara marcas, se queda con la escritura de
inv-vlc(marca 2) y descarta la deinv-bcn. El stock final (0) es incorrecto (debería ser -1, lo que significa que uno de los dos clientes no recibirá su queso) y, peor, la reserva de Ana ha desaparecido del estado del stock aunque sigue en la lista de reservas deinv-bcn. Nadie se enterará hasta que la Quesería Montblanc reciba dos pedidos para una unidad.
El experimento muestra las dos caras del teorema con crudeza: CP protege el invariante al precio de rechazar clientes; AP mantiene a todos los clientes contentos durante la partición y les rompe el invariante después. Y muestra también que LWW es la peor reconciliación posible para datos que se disputan: descarta silenciosamente una escritura entera. Una reconciliación por operaciones (sumar los descuentos de ambos lados: 1 - 1 - 1 = -1, detectar el negativo y compensar el pedido de Marc) o un umbral que cambie a modo CP al quedar pocas unidades, como sugería la tabla del apartado 7, serían las alternativas; las estrategias de resolución de conflictos se tratan en 03-04, y la compensación del pedido de Marc, en 03-05.
- Críticas y matices: harvest, yield y "dejad de llamar CP o AP a las bases de datos"
CAP es correcto como teorema, pero su uso como etiqueta de sistemas ha recibido críticas serias que conviene conocer:
- Es demasiado estrecho. Solo cubre un modelo de consistencia (linealizabilidad), un tipo de fallo (partición) y una noción de disponibilidad muy particular. No dice nada de latencia (por eso Abadi propuso PACELC), de fallos de nodos que no son particiones, de transacciones ni de particionado de datos. Un sistema puede ser irreprochablemente "CP" y aun así perder datos por un disco corrupto, o ser "AP" y estar caído por un error de despliegue.
- Kleppmann (2015), "Please stop calling databases CP or AP". Martin Kleppmann argumenta que las definiciones de CAP son tan específicas que casi ningún sistema real encaja limpiamente: MongoDB no es "CP" porque las lecturas de secundarios no son linealizables ni las escrituras
w:1sobreviven a un failover; Cassandra no es "AP" en el sentido formal cuando se usa con quórum; y "disponible" en el sentido de Gilbert-Lynch (todo nodo vivo responde) es una propiedad que casi nadie quiere literalmente (¿un nodo aislado que responde con datos de hace una hora es "disponible" en algún sentido útil?). Su propuesta: describir los sistemas por las garantías concretas que ofrecen (el vocabulario de 03-01) y por su comportamiento ante fallos concretos, en lugar de por una letra. La tabla del apartado 6 se ha construido con ese espíritu: cada fila indica la configuración y el comportamiento, no solo la etiqueta. - Brewer (2012), "CAP twelve years later". El propio Brewer matizó que "dos de tres" es engañoso, que la elección es por operación y durante la partición, y que lo interesante es diseñar la gestión de la partición: detectarla, entrar en un modo explícito de partición (limitar operaciones, registrar lo que se hace), y al curarse, recuperar (reconciliar, compensar). La simulación del apartado 8 en modo AP carece precisamente de esa tercera fase bien hecha.
- Harvest y yield (Fox y Brewer, 1999). Una forma más fina de pensar la disponibilidad. Yield es la fracción de peticiones que se responden; harvest es la fracción de los datos que refleja la respuesta. Un buscador cuyo índice está en 10 fragmentos y pierde uno puede responder al 100 % de las consultas (yield 1) con el 90 % de los datos (harvest 0,9). Aplicado a Kilómetro Cero: durante una partición, la página de un mercado puede mostrar los productores alcanzables y omitir los demás con un aviso, en lugar de fallar entera. Es una forma de degradación elegante que CAP, con su A binaria, no sabe describir.
La lección de estas críticas no es abandonar CAP sino usarlo como lo que es: un recordatorio formal de que la coordinación tiene un precio y de que ese precio hay que decidirlo por dato y por operación, con el vocabulario preciso de 03-01 y no con dos letras.
Errores Comunes y Consejos
- Presentar un sistema como "CA". Si alguien lo dice de un sistema con más de un nodo, o no ha pensado en la partición o está describiendo un solo servidor. Pregunta qué hace cada nodo cuando no puede hablar con los demás.
- Etiquetar toda la plataforma con una letra. Kilómetro Cero no es CP ni AP; el stock de la última unidad es CP y el carrito es AP. La decisión es por dato y por operación, y a veces por estado del dato.
- Elegir AP y reconciliar con LWW sin mirar qué se pierde. LWW es la estrategia por defecto de muchos almacenes (Cassandra, entre otros) y es correcta para datos donde la última escritura realmente es la buena (posición de un repartidor, perfil de usuario editado por una sola persona). Para datos que se acumulan o se disputan, descarta escrituras enteras. Comprueba con una simulación como la del apartado 8 antes de aceptar la configuración por defecto.
- Confundir un nodo lento con una partición... o no confundirlos. En un sistema asíncrono no hay diferencia observable. Un sistema CP tratará a un nodo muy lento como aislado y dejará de contar con él; eso es correcto, pero significa que los timeouts (02-03, 07-04) forman parte de la decisión CAP, y timeouts demasiado cortos convierten la congestión en particiones frecuentes.
- Olvidar la E de PACELC. El coste de la consistencia se paga todos los días en latencia, no solo durante particiones. Si la reserva linealizable de
queso-curadorequiere coordinación Barcelona-Valencia, el percentil 99 deReservarStocklo reflejará. Mide antes de decidir (07-01). - Consejo: diseña explícitamente las tres fases de Brewer para cada dato AP: cómo se detecta la partición, qué operaciones se permiten mientras dura y cómo se reconcilia después. Si no puedes describir la tercera fase, ese dato debería ser CP.
- Consejo: cuando alguien te pregunte "¿esto es CP o AP?", responde con las garantías concretas: "las reservas son linealizables vía quórum y se rechazan si no hay mayoría; las lecturas del catálogo son eventualmente consistentes con un retraso típico de 200 ms".
Ejercicios
Ejercicio 1: Clasificar decisiones
Para cada uno de los siguientes datos de Kilómetro Cero, decide CP o AP durante una partición, indica la elección L o C en ausencia de partición y justifica en dos frases. Después, indica qué estrategia de reconciliación usarías en los AP.
- La lista de deseos de Lucía (productos marcados para más tarde).
- El número de reservas activas de un repartidor (que limita cuántos pedidos más se le pueden asignar; máximo 8).
- La valoración media (1-5 estrellas) de la Bodega Roble Alto.
- El estado de un pedido (
creado→pagado→en_reparto→entregado).
Ejercicio 2: Extender la simulación con un umbral
Modifica Inventario para que el modo se decida por producto y por estado: si el stock del producto en la réplica local es mayor que un umbral (por ejemplo 5), la reserva se procesa en modo AP; si es menor o igual, en modo CP. Carga tomate-rosa con 20 unidades y queso-curado con 1, parte la red y haz que Ana y Marc reserven ambos productos desde réplicas distintas. Muestra la salida y explica qué se ha ganado respecto a los dos modos puros.
Ejercicio 3: Reconciliación por operaciones
Sustituye reconciliar_lww por reconciliar_por_operaciones, que en lugar de elegir una escritura calcule el stock real como stock_inicial - reservas_en_bcn - reservas_en_vlc y, si el resultado es negativo, devuelva la lista de reservas que deben compensarse (las últimas aceptadas, por marca de tiempo). Ejecuta el escenario AP con 1 unidad y con 2 unidades y comenta la diferencia con LWW. ¿Qué información necesita esta reconciliación que LWW no necesitaba?
Soluciones
Solución 1:
- Lista de deseos: AP / EL. Una lista de deseos indisponible es una molestia sin coste, y un artículo que aparece dos veces es trivial de arreglar. Reconciliación por unión (OR-Set, un CRDT de conjunto: nunca se pierde un "añadir"; un "quitar" solo elimina los "añadir" que había visto).
- Reservas activas de un repartidor: CP / EC. El límite de 8 es un invariante del tipo "no descontar por debajo de cero": si dos lados de la partición asignan pedidos al mismo repartidor, puede acabar con 10. Como es una decisión de asignación (no de cara al cliente final), un rechazo temporal es aceptable: el pedido queda "pendiente de asignación" como en el caso 5 del ejercicio 3 de 02-05.
- Valoración media: AP / EL. Es un agregado estadístico; un décimo de estrella de diferencia entre réplicas durante unos segundos es irrelevante. Reconciliación con dos
GCounter(suma de puntuaciones y número de votos), que convergen sin pérdida. - Estado de un pedido: CP / EC para las transiciones, aunque con matiz. El estado avanza por una máquina de estados con transiciones irreversibles (un pedido entregado no vuelve a "pagado") y cada transición la realiza un único servicio (
pagosmarca pagado,repartomarca entregado), así que el conflicto real es raro; pero dos lados aceptando transiciones distintas (canceladoen uno,en_repartoen otro) crearían un estado sin sentido. Las escrituras deben ir al líder dekm0_pedidoscon quórum; las lecturas de "mis pedidos" pueden ser AP con garantías de sesión.
Solución 2:
UMBRAL = 5
def reservar(self, cliente: str, replica: str, producto: str) -> str:
local = self.replicas[replica]
modo = "AP" if local.stock[producto].unidades > UMBRAL else "CP"
for r in self.replicas.values():
r.modo = modo # el resto del método usa local.modo como antes
return self._reservar_con_modo(cliente, replica, producto) # el cuerpo original(Renombrando el reservar original a _reservar_con_modo.) Con tomate-rosa a 20 unidades, ambas reservas durante la partición se aceptan en modo AP y, al reconciliar por operaciones, el stock queda en 18 sin sobreventa; con queso-curado a 1, ambas se rechazan en modo CP. Se ha ganado disponibilidad para el 99 % de los productos (que tienen stock de sobra) manteniendo la protección del invariante solo cuando importa. El coste: la decisión se toma con el stock local, que durante la partición puede ser mayor que el real (el otro lado ha descontado sin que lo sepamos), así que el umbral debe ser mayor que el número de reservas plausibles durante la partición más larga esperada. Es un ejemplo de lo que Brewer llama "gestionar la partición" en lugar de padecerla.
Solución 3:
Primero, reservar debe conservar la marca de cada reserva aceptada (el stock solo guarda la última escritura, así que el historial hay que anotarlo aparte). Cambia el campo de Replica a reservas: list[tuple[int, str, str]] (marca, cliente, producto) y los dos append a local.reservas.append((self.reloj, cliente, producto)) (y, en escenario, la comprensión que lista las reservas pasa a for _, c, _ in r.reservas). Después:
def reconciliar_por_operaciones(self, producto: str, stock_inicial: int) -> str:
reservas = sorted(
(marca, cliente, r.nombre)
for r in self.replicas.values()
for marca, cliente, p in r.reservas if p == producto
)
real = stock_inicial - len(reservas) # sumar TODAS las operaciones de ambos lados
for r in self.replicas.values():
r.stock[producto] = Registro(max(real, 0), self.reloj, "reconciliacion")
a_compensar = reservas[real:] if real < 0 else [] # las últimas |real| por marca de tiempo
return (f"stock real={real}; a compensar: "
f"{[(cliente, replica) for _, cliente, replica in a_compensar]}")Con 1 unidad: stock real=-1; a compensar: [('Marc', 'inv-vlc')]. Se conserva la reserva de Ana (marca 1), el stock queda en 0 y pedidos recibe la orden de cancelar el pedido de Marc (la compensación de una saga, 03-05). Con 2 unidades: stock real=0; a compensar: []. Frente a LWW, que dejaba el stock en 0 "por casualidad" y perdía una reserva sin que nadie lo supiera, esta reconciliación produce un estado correcto y una lista explícita de daños a reparar. Lo que necesita, y LWW no, es el historial de operaciones de cada lado (no solo el último valor) y una regla de negocio para decidir a quién compensar. Es la diferencia entre replicar estados y replicar operaciones, que reaparecerá en 03-04.
Conclusión
El teorema CAP, enunciado con precisión, dice que un sistema distribuido no puede garantizar a la vez linealizabilidad y que todo nodo vivo responda mientras la red está partida; y como la partición no es opcional en cuanto hay dos nodos y un cable, la elección real es qué sacrificar durante la partición: CP rechaza las operaciones que no puede coordinar, AP las acepta y reconcilia después. Hemos desmontado los malentendidos habituales (no se "eligen dos de tres"; AP no es "sin consistencia" sino sin linealizabilidad, y admite garantías causales y de sesión; CP no significa caída total), y hemos añadido PACELC para describir el coste diario de la consistencia en latencia cuando no hay partición, con una tabla que clasifica PostgreSQL, Cassandra, DynamoDB, MongoDB, Spanner y etcd por configuración. Aplicado a Kilómetro Cero, el criterio ha sido claro: CP para lo irreversible o disputado (pagos, última unidad, liderazgo), AP para lo que se puede unir, corregir o ignorar (carrito, posiciones, contadores, catálogo), con la sutileza de que un mismo dato puede cambiar de modo según su estado. La simulación ha hecho visibles ambas caras: el modo CP rechazó a Ana y a Marc durante la partición, y el modo AP los aceptó a los dos y perdió silenciosamente una reserva al reconciliar con last-write-wins. Las críticas de Kleppmann y los conceptos de harvest y yield nos recuerdan que las dos letras son un punto de partida, no una descripción, y que lo que hay que documentar son las garantías concretas y las tres fases de la gestión de la partición.
Hemos dicho varias veces que un sistema CP "rechaza las escrituras sin quórum" y que "el lado con mayoría sigue funcionando", como si fuera evidente cómo un grupo de nodos, con mensajes que se pierden y sin reloj global, decide cuál es la mayoría, quién manda y qué valor es el definitivo. No es evidente en absoluto: es uno de los problemas más difíciles de la informática distribuida, resuelto por algoritmos con nombre propio que Kilómetro Cero necesitará, entre otras cosas, para que una sola instancia del relay outbox de 02-05 publique en cada momento. Ese problema es el consenso, y Paxos y Raft son el tema de la siguiente lección.
Curso de Arquitecturas Distribuidas
Módulo 1: Introducción a los Sistemas Distribuidos
- Conceptos Básicos de Sistemas Distribuidos
- Modelos de Sistemas Distribuidos
- Ventajas y Desafíos de los Sistemas Distribuidos
- Las Falacias de la Computación Distribuida
- Tiempo, Relojes y Ordenación de Eventos
- Del Monolito a la Plataforma Distribuida: el Caso Kilómetro Cero
Módulo 2: Comunicación en Sistemas Distribuidos
- Protocolos de Comunicación
- RPC y RMI
- gRPC y Serialización de Datos
- Mensajería y Colas de Mensajes
- Patrones de Comunicación Asíncrona
Módulo 3: Consistencia y Replicación
- Modelos de Consistencia
- El Teorema CAP y PACELC
- Algoritmos de Consenso
- Replicación de Datos
- Transacciones Distribuidas y Sagas
Módulo 4: Almacenamiento Distribuido
- Particionado de Datos y Hashing Consistente
- Sistemas de Archivos Distribuidos
- Almacenamiento de Objetos
- Bases de Datos Distribuidas
- Cachés Distribuidos
Módulo 5: Computación Distribuida
- Modelos de Computación Distribuida
- MapReduce y Hadoop
- Spark y Computación en Memoria
- Procesamiento de Flujos de Datos
- Planificación de Trabajos y Pipelines de Datos
Módulo 6: Seguridad en Sistemas Distribuidos
- Autenticación y Autorización
- Cifrado y Protección de Datos
- Gestión de Identidades
- Seguridad entre Servicios: mTLS y Gestión de Secretos
- Puertas de Enlace, Limitación de Tasa y Auditoría
Módulo 7: Monitoreo y Mantenimiento
- Monitoreo de Sistemas Distribuidos
- Logs Centralizados y Trazabilidad Distribuida
- Gestión de Fallos y Recuperación
- Patrones de Resiliencia: Timeouts, Reintentos y Circuit Breaker
- Automatización y Orquestación
- Pruebas en Sistemas Distribuidos e Ingeniería del Caos
