Trade-offs
En resumen: El teorema CAP dice que, cuando la conexión entre nodos se rompe, un sistema elige entre consistencia (CP, rechaza) y disponibilidad (AP, acepta y concilia más tarde).
Problema
La última cafetera y dos compradores
La tienda ahora tiene dos almacenes, en Sofía y en Varna. Cada uno guarda su propia copia de las existencias: las páginas responden rápido, y un almacén sigue trabajando cuando el otro va lento. Queda una cafetera de 349 €.
El viernes por la noche la conexión entre los dos almacenes se cae durante diez minutos. En esos minutos María pide la cafetera y su pedido llega a Varna. Pablo pide la misma cafetera y el suyo llega a Sofía.
¿Qué debe hacer la tienda? Elijas lo que elijas, alguien pierde. O los dos reciben un rechazo mientras la cafetera sigue en el estante. O los dos pagan por una cafetera. No hay una tercera opción, y de eso trata esta lección.
La idea
CAP en la práctica
Cuando la red parte el sistema en trozos (una partición, P), cada trozo responde por su cuenta. Entonces elige una de dos cosas:
- consistencia (C): cada respuesta es la última verdad. Si el trozo no puede estar seguro, rechaza;
- disponibilidad (A): cada petición recibe una respuesta, aunque sea de una copia que puede ser vieja.
Una partición no se elige: las redes fallan, sea la red entre dos almacenes o entre dos zonas en la nube. Lo único que eliges es qué haces cuando ocurre. Esto es el trade-off entre consistencia y disponibilidad, conocido como el teorema CAP. Lo viste desde el lado de las bases de datos en la lección «Replicación, sharding y CAP». Aquí lo ves desde el lado de la tienda: quién pierde y cuánto.
En los programas un almacén es un objeto, la conexión es una bandera linkUp y el tiempo es un ciclo. Así las dos opciones corren una junto a la otra, sin red.
@dataclassclass Sale: buyer: str tick: int
@dataclassclass Warehouse: name: str stock: int sales: list = field(default_factory=list)Míralo
CP: mejor un rechazo que una mentira
Un almacén CP vende solo si el otro también confirma. Sin conexión no puede comprobarlo, así que rechaza:
# CP: vende solo si el otro almacén también confirmadef sell_cp(here, other, link_up, buyer): if not link_up: return (f"{buyer}: rechazado, {here.name}" f" no ve a {other.name}") if here.stock == 0: return buyer + ": agotado" here.stock -= 1 other.stock -= 1 return buyer + ": vendido"sofia = Warehouse("Sofía", 1)varna = Warehouse("Varna", 1)link_up = False # la conexión está caídaprint(sell_cp(varna, sofia, link_up, "María"))print(sell_cp(sofia, varna, link_up, "Pablo"))link_up = True # la conexión vuelveprint(sell_cp(varna, sofia, link_up, "María"))print(f"existencias: {sofia.name} {sofia.stock}, {varna.name} {varna.stock}")María: rechazado, Varna no ve a SofíaPablo: rechazado, Sofía no ve a VarnaMaría: vendidoexistencias: Sofía 0, Varna 0Mientras la conexión está caída, nadie compra. En cuanto vuelve, María compra y los dos almacenes dicen 0. Los datos son correctos todo el tiempo. El precio está en la otra cualidad: en esos diez minutos la tienda no vende nada y los clientes ven un error.
Míralo
AP: vender ahora, conciliar después
Un almacén AP vende desde su propia copia y anota la venta. Cuando la conexión vuelve, la conciliación reúne las ventas de los dos almacenes, las ordena por tiempo y reparte las unidades reales:
# AP: vende desde su propia copia, sin preguntardef sell_ap(here, buyer, tick): if here.stock == 0: return buyer + ": agotado" here.stock -= 1 here.sales.append(Sale(buyer, tick)) return f"{buyer}: vendido ({here.name})"
# conciliación: las ventas de ambos almacenes, por tiempodef reconcile(real, a, b): sales = sorted(a.sales + b.sales, key=lambda s: s.tick) for s in sales: ok = real > 0 if ok: real -= 1 result = "confirmado" if ok else "vale de 20 €" print(f"{s.buyer} (ciclo {s.tick}): {result}") a.stock = b.stock = realsofia = Warehouse("Sofía", 1)varna = Warehouse("Varna", 1)# la conexión está caída: cada almacén vende desde su propia copiaprint(sell_ap(varna, "María", 3))print(sell_ap(sofia, "Pablo", 5))# la conexión vuelvereconcile(1, sofia, varna)print("existencias:", sofia.stock)María: vendido (Varna)Pablo: vendido (Sofía)María (ciclo 3): confirmadoPablo (ciclo 5): vale de 20 €existencias: 0Los dos oyen «vendido». Después resulta que hay una cafetera. María llegó primero y la recibe. Pablo recibe una disculpa y un vale de 20 €. AP paga con una compensación y con código para conciliar.
Las dos copias diferían durante diez minutos y se encontraron otra vez en cuanto volvió la conexión. Esto es la consistencia eventual: las copias pueden diferir durante un tiempo, pero cuando dejan de llegar cambios nuevos, llegan al mismo estado. Lo que se promete es que se encuentran, no cuándo.
Míralo
Las dos opciones, una debajo de la otra
# AP: vende desde su propia copia, sin preguntardef sell_ap(here, buyer, tick): if here.stock == 0: return buyer + ": agotado" here.stock -= 1 here.sales.append(Sale(buyer, tick)) return f"{buyer}: vendido ({here.name})"
# conciliación: las ventas de ambos almacenes, por tiempodef reconcile(real, a, b): sales = sorted(a.sales + b.sales, key=lambda s: s.tick) for s in sales: ok = real > 0 if ok: real -= 1 result = "confirmado" if ok else "vale de 20 €" print(f"{s.buyer} (ciclo {s.tick}): {result}") a.stock = b.stock = real# CP: vende solo si el otro almacén también confirmadef sell_cp(here, other, link_up, buyer): if not link_up: return (f"{buyer}: rechazado, {here.name}" f" no ve a {other.name}") if here.stock == 0: return buyer + ": agotado" here.stock -= 1 other.stock -= 1 return buyer + ": vendido"# AP: vende desde su propia copia, sin preguntardef sell_ap(here, buyer, tick): if here.stock == 0: return buyer + ": agotado" here.stock -= 1 here.sales.append(Sale(buyer, tick)) return f"{buyer}: vendido ({here.name})"
# conciliación: las ventas de ambos almacenes, por tiempodef reconcile(real, a, b): sales = sorted(a.sales + b.sales, key=lambda s: s.tick) for s in sales: ok = real > 0 if ok: real -= 1 result = "confirmado" if ok else "vale de 20 €" print(f"{s.buyer} (ciclo {s.tick}): {result}") a.stock = b.stock = realPaso a paso, en palabras
- La última cafetera por 349 €. Dos almacenes guardan cada uno una copia de las existencias: 1 y 1. Dos tiendas, una debajo de la otra: la de arriba elige CP, la de abajo AP.
- La conexión entre Sofía y Varna se cae. María está en Varna, Pablo en Sofía. Cada almacén tiene que decidir por su cuenta.
- CP vende solo si el otro almacén confirma. No hay conexión, así que los dos reciben un rechazo. Los datos siguen siendo correctos, pero la tienda no vende.
- AP vende desde su propia copia. María compra en el ciclo 3, Pablo en el ciclo 5. Las dos copias dicen 0, pero se vendieron dos de una.
- La conexión vuelve. La conciliación reúne las ventas, no los números, y las ordena por tiempo.
- María llegó primero y recibe la máquina. Pablo recibe una disculpa y un vale de 20 €. CP pagó con ventas perdidas, AP con una compensación.
La tienda de arriba es CP, la de abajo es AP. La misma partición, los mismos compradores. Arriba pierde la tienda (ventas); abajo pierde un cliente (la unidad), y la tienda paga con un vale y con tiempo de soporte.
Pruébalo
Predice la salida
Hay dos unidades. La conexión está caída y los almacenes venden AP: Pablo en Sofía en el ciclo 1, María en Varna en el ciclo 4, Elena en Sofía en el ciclo 2. ¿Qué imprime la conciliación?
sofia = Warehouse("Sofía", 2)varna = Warehouse("Varna", 2)sell_ap(sofia, "Pablo", 1)sell_ap(varna, "María", 4)sell_ap(sofia, "Elena", 2)reconcile(2, sofia, varna)La conciliación ordena por ciclo, sin importar qué almacén hizo la venta: Pablo (1), Elena (2), María (4). Las dos unidades van a los dos primeros, y María recibe un vale, aunque en Varna las existencias le parecían 2.
Pruébalo
Completa
¿Por qué campo se ordenan las ventas durante la conciliación?
sales = sorted(a.sales + b.sales, key=lambda s: s.)Por el ciclo: el momento de la venta. Es justo con los clientes y no depende del orden en que los almacenes envían sus ventas.
Pruébalo
Encuentra el error
Un compañero escribe una conciliación más corta: cada almacén toma el menor de los dos números. Después de la partición los dos almacenes dicen 0, nadie recibe un vale, y Pablo espera una cafetera que no existe. ¿Qué línea tiene el error?
Elige la línea con el error.
Los números son un resultado, no una historia. Con 0 y 0 no puedes saber que se vendieron dos unidades de una: cada copia restó por su cuenta. Se concilian las operaciones (las ventas) y el número se calcula a partir de ellas.
La idea
Cada cualidad se paga con otra cualidad
CAP es el trade-off más conocido, pero no el único. La arquitectura es elegir qué cualidad paga a cuál:
- consistencia contra disponibilidad durante una partición, como viste;
- consistencia contra latencia incluso sin partición: la confirmación del otro almacén es un viaje más por la red en cada venta;
- disponibilidad contra corrección: AP necesita compensaciones, un proceso de conciliación y personas en soporte;
- seguridad contra velocidad y comodidad: una comprobación más es más tiempo, un paso más al iniciar sesión;
- escala contra simplicidad: dos almacenes con copias son más complejos que una base de datos.
La elección no es una para toda la tienda, sino una para cada tipo de datos. El carrito va bien con AP: si se pierde un artículo, el cliente lo añade otra vez, y el carrito siempre está ahí. Las últimas unidades, el pago y las cuentas son para CP: un artículo vendido que no existe o un pago doble cuestan dinero y confianza. El catálogo con sus descripciones tolera datos de hace minutos.
Decide el negocio, y el arquitecto le da el precio de cada opción en números: cuántos minutos al año sin ventas con CP, cuántos vales con AP. La decisión y sus consecuencias van a un ADR.
La idea
Cuándo duele
- Consistencia eventual cuando los usuarios no ven su propia acción. El cliente paga, la página lee de una copia que todavía no lo sabe y dice «no tienes pedidos». Paga otra vez. Viste lo mismo con las réplicas en la lección sobre escalado. Cuando una persona lee lo que acaba de escribir, lee de donde se escribió.
- Cuando no hay compensación. Dinero ya cobrado dos veces, un medicamento con cantidad limitada, un billete para un asiento en un avión. Aquí AP no es una opción, es un error.
- Una conciliación más compleja que su beneficio. Si una partición ocurre una vez al año durante un minuto, un rechazo sale más barato que un código de conciliación que alguien tiene que entender.
- Datos distribuidos cuando no los necesitas. Una base de datos con transacciones no se hace esta pregunta (la lección sobre transacciones). Una copia en otro almacén o en otro país se hace cuando un NFR la pide con un número.
Más precisamente, para curiosos
CAP con más precisión. El teorema (Brewer, demostrado por Gilbert y Lynch) habla de un valor, muchas copias y una partición. «Elige 2 de 3» engaña: P no es una elección. Dicho con más precisión: durante una partición eliges C o A. PACELC añade la segunda mitad: Else, eliges entre Latency y Consistency.
Tiempo. Aquí todos ven el mismo contador de ciclos. Los relojes de las máquinas reales se desajustan, así que «quién fue primero» se decide con relojes lógicos: relojes de Lamport, relojes vectoriales, relojes lógicos híbridos. Spanner de Google usa relojes con referencias atómicas y GPS y espera hasta que pasa la incertidumbre.
CRDTs (conflict-free replicated data types) son estructuras que siempre se fusionan en el mismo resultado sin coordinación: contadores, conjuntos, listas. El carrito de Amazon en el artículo de Dynamo es un ejemplo de AP donde añadir nunca pierde artículos.
Quórums. Con N copias, escribiendo en W y leyendo de R, si W + R > N una lectura siempre ve la última escritura. Los conociste en la lección sobre replicación. Cassandra y DynamoDB te dejan elegir el nivel de consistencia para cada petición.
Varios servicios. La conciliación de aquí es para un artículo en dos almacenes. Cuando una operación de negocio pasa por varios servicios y, si falla, hay que deshacerla paso a paso, se convierte en un patrón con nombre propio. Viene más adelante en este módulo.
sorted(..., key=lambda s: s.tick) es estable y devuelve una lista nueva.Ejercicio
CP para las últimas unidades, AP para el resto
Resuelve la tarea en tu propio editor.
Los dos almacenes venden cafeteras (1 cada uno) y filtros de café (500 cada uno). Escribe sell, que vende las cafeteras solo con la confirmación del otro almacén (CP), y los filtros desde su propia copia (AP). La conexión está caída. María, en Varna, quiere una cafetera y filtros; Pablo, en Sofía, quiere filtros.
María, cafetera: rechazado, Varna no ve a SofíaMaría, filtros: vendido (Varna)Pablo, filtros: vendido (Sofía)Más tareas
- Un umbral. En lugar de una lista de productos «importantes», usa un umbral: AP mientras la copia tenga más de 3 unidades, CP para las últimas 3. ¿Qué pasa si los dos almacenes venden 3 cada uno durante la partición?
- El carrito. El carrito tiene dos copias. En una el cliente añadió tazas, en la otra quitó los filtros. Escribe una conciliación de las operaciones «añadir» y «quitar», ordenadas por ciclo.
Test
Ponte a prueba
Lecciones relacionadas
Sección «Lecciones relacionadas»- Se basa en: Ordenamiento estable y el ordenamiento incorporado · Escalado · ADR: registros de decisiones de arquitectura · Requisitos no funcionales que puedes medir
- Continúa en: Una transacción entre servicios (Saga) · Tu primera tarea de arquitectura
- Ejercicios sobre el tema: El arquitecto en acción