El problema: coordinar réplicas en una red imperfecta
En la lección anterior vimos que los sistemas NoSQL distribuyen y replican datos entre muchos nodos. Pero replicar plantea una pregunta incómoda: si escribo un dato en el nodo A y al mismo tiempo alguien lo lee del nodo B, ¿qué valor ve?
El teorema CAP, formulado por Eric Brewer (2000) y demostrado por Gilbert y Lynch (2002), establece que un sistema de datos distribuido no puede garantizar simultáneamente más de dos de estas tres propiedades:
| Propiedad | Significado |
|---|---|
| C — Consistencia | Toda lectura recibe la escritura más reciente (o un error). Todos los nodos ven el mismo dato al mismo tiempo. |
| A — Disponibilidad | Toda petición recibe una respuesta (aunque no sea el dato más reciente). El sistema nunca rechaza operaciones. |
| P — Tolerancia a particiones | El sistema sigue funcionando aunque la red se parta y los nodos no puedan comunicarse entre sí. |
graph TD C["C — Consistencia"] A["A — Disponibilidad"] P["P — Tolerancia a particiones"] C --- A A --- P P --- C C -.-> CP["Sistemas CP<br/>HBase, MongoDB<br/>(por defecto)"] P -.-> CP A -.-> AP["Sistemas AP<br/>Cassandra, CouchDB"] P -.-> AP C -.-> CA["CA (teórico)<br/>RDBMS en un solo nodo"] A -.-> CA
El matiz clave: la partición no es opcional
El teorema suele malentenderse como “elige dos de tres y olvida la tercera”. En realidad, en un sistema distribuido de verdad, las particiones de red van a ocurrir (cables, switches, congestión, regiones de nube que se aíslan). Renunciar a P significaría renunciar a distribuir.
Por tanto, la decisión real es: cuando ocurre una partición, ¿qué sacrificas: C o A?
- CP (consistencia sobre disponibilidad): si un nodo no puede confirmar que tiene el dato más reciente, rechaza la operación o se pone en modo solo-lectura hasta que la red se recupere. Ejemplos: HBase, MongoDB con escritura mayoritaria, ZooKeeper.
- AP (disponibilidad sobre consistencia): cada lado de la partición sigue aceptando escrituras aunque se desincronicen; al sanar la red hay que reconciliar las versiones. Ejemplos: Cassandra, CouchDB, DynamoDB.
graph TD
subgraph Antes["Red sana"]
N1["Nodo A<br/>x = 1"] <-->|"replicación"| N2["Nodo B<br/>x = 1"]
end
subgraph Particion["¡Partición de red!"]
N3["Nodo A<br/>x = 2 (escritura local)"] -.-|"enlace cortado"| N4["Nodo B<br/>x = 1 (dato viejo)"]
end
Particion --> Decision{"¿Qué hace el Nodo B?"}
Decision -->|"CP: rechaza lecturas<br/>hasta reconectarse"| Resp1["Error / timeout"]
Decision -->|"AP: responde con<br/>lo que tiene"| Resp2["x = 1 (inconsistente)"]
Consistencia eventual
Los sistemas AP no abandonan la consistencia: la posponen. Con consistencia eventual, si dejan de llegar escrituras, todas las réplicas terminan convergiendo al mismo valor mediante procesos en segundo plano (anti-entropy, read repair, hinted handoff en Cassandra).
El problema es decidir qué versión gana cuando dos réplicas divergen:
- Last-write-wins (LWW): gana la escritura con la marca de tiempo más reciente. Simple, pero pierde datos si dos escrituras fueron realmente concurrentes.
- Relojes vectoriales / versiones: detectan conflictos reales y los exponen a la aplicación para que los resuelva (DynamoDB clásico, Riak).
- CRDTs: estructuras de datos diseñadas para fusionarse sin conflicto (contadores, conjuntos).
La consistencia eventual no es “inconsistencia para siempre”: en la práctica la ventana de divergencia suele ser de milisegundos. Pero durante una partición puede durar horas, y la aplicación debe estar preparada para leer datos viejos.
Quórum: consistencia ajustable con R + W > N
Muchos sistemas (Cassandra, DynamoDB) no son CP ni AP de forma absoluta: permiten ajustar la consistencia por operación mediante quórum. Si un dato tiene N réplicas:
- W: en cuántas réplicas debe confirmarse una escritura antes de responder al cliente.
- R: de cuántas réplicas se lee antes de responder.
La regla fundamental:
Si R + W > N, la lectura toca al menos una réplica que vio la escritura: hay consistencia fuerte (sin contar fallos de reloj y reparaciones pendientes).
Ejemplos con N = 3:
| Configuración | Garantía | Perfil |
|---|---|---|
| W=3, R=1 | R+W=4 > 3: fuerte | Escrituras lentas, lecturas rápidas |
| W=1, R=3 | R+W=4 > 3: fuerte | Escrituras rápidas, lecturas lentas |
| W=2, R=2 | R+W=4 > 3: fuerte | Equilibrio (quórum clásico) |
| W=1, R=1 | R+W=2 ≤ 3: eventual | Máxima velocidad y disponibilidad |
¿Qué elegir en la práctica?
- CP cuando un dato incorrecto es peor que un error temporal: saldos bancarios, inventario, configuración de clúster, líderes distribuidos.
- AP cuando responder siempre es lo crítico y el dato tolera converger: carritos de compra, likes, telemetría, feeds.
- Recuerda: fuera de una partición, los sistemas AP suelen comportarse de forma consistente. CAP solo muerde durante fallos de red.
Ejercicio: simula consistencia eventual
🧪 Ejercicio
Réplicas que convergen
Simula 3 réplicas de un contador. Implementa una clase `Replica` con un diccionario de valores y una marca de tiempo por clave, un método `escribir(clave, valor, ts)` y un método `sincronizar(otra)` que fusiona dos réplicas quedándose, por clave, con el valor de mayor marca de tiempo (last-write-wins). Escribe valores distintos en réplicas distintas (simulando una partición), luego sincroniza todas y muestra que convergen.
🔍 Guarda cada clave como (valor, ts). En sincronizar, recorre las claves de ambas réplicas y quédate con la tupla de mayor ts.
class Replica:
def __init__(self, nombre):
self.nombre = nombre
self.datos = {} # clave -> (valor, timestamp)
def escribir(self, clave, valor, ts):
actual = self.datos.get(clave)
if actual is None or ts > actual[1]:
self.datos[clave] = (valor, ts)
def sincronizar(self, otra):
for clave, (valor, ts) in otra.datos.items():
self.escribir(clave, valor, ts)
# Simulación: 3 réplicas aisladas por una partición
a, b, c = Replica("A"), Replica("B"), Replica("C")
a.escribir("likes", 100, ts=1)
b.escribir("likes", 105, ts=3) # escritura más reciente en B
c.escribir("likes", 102, ts=2)
# La partición sana: todas se sincronizan entre sí
for r1 in (a, b, c):
for r2 in (a, b, c):
if r1 is not r2:
r1.sincronizar(r2)
for r in (a, b, c):
print(f"Réplica {r.nombre}: {r.datos['likes']}")
# Las tres convergen a (105, 3): last-write-winsComprueba lo aprendido
Comprueba que lo pillaste
Durante una partición de red, un sistema CP como HBase recibe una escritura en un nodo que no puede contactar a la mayoría. ¿Qué hace?
Un sistema CP sacrifica disponibilidad: si no puede garantizar que el dato será consistente (no hay quórum/mayoría), prefiere devolver un error a aceptar un dato divergente.
Comprueba que lo pillaste
Con N = 5 réplicas, ¿cuál de estas configuraciones de quórum NO garantiza consistencia fuerte (R + W > N)?
Con W=2 y R=2, R+W=4 ≤ 5: lectura y escritura pueden tocar conjuntos de réplicas disjuntos, así que la lectura puede no ver la última escritura. Las demás suman más de 5.
Comprueba que lo pillaste
¿Por qué se dice que, en la práctica, la elección del teorema CAP es entre C y A, y no entre tres opciones?
En cualquier red real los enlaces fallan tarde o temprano. Como P es obligatorio, el diseño decide qué pasa durante la partición: rechazar operaciones (CP) o divergir temporalmente (AP).
Resumen
- El teorema CAP: un sistema distribuido solo puede garantizar 2 de 3 entre Consistencia, Disponibilidad y Tolerancia a particiones.
- Como las particiones son inevitables, la decisión real es CP vs AP durante una partición: rechazar operaciones o aceptar divergencia temporal.
- Los sistemas AP usan consistencia eventual: las réplicas convergen con mecanismos como read repair y resolución de conflictos (LWW, relojes vectoriales, CRDTs).
- El quórum permite ajustar la consistencia: con N réplicas, R + W > N garantiza que la lectura vea la última escritura.
- No hay respuesta única: datos financieros tienden a CP, feeds y telemetría a AP, y muchos sistemas permiten elegir por operación.
📚 Lecturas y fuentes
| Recurso | Tipo | Por qué leerlo |
|---|---|---|
| Teorema CAP (Wikipedia en español) | Artículo | Repaso corto en español del enunciado y de la clasificación CP/AP. Léelo entero, son pocos párrafos. |
| CAP Twelve Years Later: How the “Rules” Have Changed — Eric Brewer (InfoQ) | Artículo | El propio autor del teorema explica que “2 de 3” es engañoso. Fíjate en la sección “Why ‘2 of 3’ is Misleading” (en inglés). |
| Please stop calling databases CP or AP — Martin Kleppmann | Artículo | Entrada de blog de 10 minutos: por qué etiquetar una base como CP o AP suele ser incorrecto. El antídoto contra memorizar el triángulo (en inglés). |
| Designing Data-Intensive Applications (Martin Kleppmann) | Libro | Cap. 9 “Consistency and Consensus”, sección “Linearizability” y su apartado “The Cost of Linearizability”; y del cap. 5, “Quorums for reading and writing” para la regla R + W mayor que N (en inglés). |