wandres.dev
POR QUÉ SINCRONIZAR ES DIFÍCIL · particiones y orden

CAP con precisión

El teorema CAP no dice elige dos de tres: dice que ningún sistema puede ser a la vez linealizable y disponible mientras la red pierde mensajes, y por eso la única elección real ocurre durante la partición.

⏱ 20 min

Pocas ideas de la informática han sido tan citadas y tan maltratadas como el teorema CAP. La versión que circula —consistencia, disponibilidad y tolerancia a particiones: elige dos— es pegadiza, cabe en una diapositiva y es engañosa en los tres términos a la vez: confunde lo que significa cada letra, presenta como elegible algo que no se elige, y sugiere una decisión permanente de arquitectura cuando lo que hay es una decisión momentánea que se toma mientras dura un fallo. Enunciado con precisión, el teorema dice algo mucho más modesto y mucho más útil, y ese enunciado exacto es la herramienta que necesitas para justificar por qué una aplicación local-first renuncia deliberadamente a garantías que otros sistemas consideran innegociables.

🎯 Al terminar esta lección sabrás
  • Enunciar el teorema con su modelo, sus tres definiciones formales y su demostración en cuatro pasos.
  • Identificar los cuatro errores concretos que comete la formulación popular.
  • Reconocer que la tolerancia a particiones no es una opción de diseño sino una propiedad del entorno.
  • Reformular la elección real como una política operativa durante la partición y una compensación tras ella.
  • Situar la latencia en el cuadro mediante PACELC, que es donde local-first se juega de verdad su tesis.

El enunciado tal y como se demuestra

La conjetura la formuló Eric Brewer en el año 2000 y la demostraron formalmente Seth Gilbert y Nancy Lynch en 2002. Su versión precisa exige fijar primero el modelo: una red asíncrona, sin relojes compartidos, en la que los nodos solo pueden razonar a partir de los mensajes que reciben. Sobre ese modelo se implementa un objeto simple, un registro de lectura y escritura replicado. Y entonces se definen las tres propiedades con un rigor que la versión popular pierde por completo.

Consistencia significa aquí exactamente linealizabilidad: debe existir un orden total de todas las operaciones tal que cada una parezca haber ocurrido de forma instantánea en un punto entre su invocación y su respuesta, y ese orden debe respetar el tiempo real. Si una escritura terminó antes de que empezara una lectura, la lectura tiene que verla. Disponibilidad significa que toda petición recibida por un nodo que no ha fallado debe terminar en una respuesta; no se exige que sea rápida, se exige que llegue. Tolerancia a particiones significa que el sistema debe seguir comportándose según su especificación aunque la red pierda un número arbitrario de mensajes entre nodos.

El teorema afirma que ninguna implementación satisface las tres. Y la demostración es tan corta que conviene tenerla entera en la cabeza, porque quien la recuerda deja de repetir la versión vulgarizada. Supón un sistema que las cumple las tres. Parte los nodos en dos grupos, G1 y G2, y haz que se pierda todo mensaje entre ellos. Un cliente escribe un valor nuevo en un nodo de G1; por disponibilidad, esa escritura tiene que completarse. Después, otro cliente lee de un nodo de G2; por disponibilidad, esa lectura tiene que responder algo. Pero G2 no ha recibido ninguna información sobre la escritura, así que devuelve el valor anterior. Existe entonces una lectura que empezó después de que terminara una escritura y que no la refleja: la historia no es linealizable. Contradicción.

// La demostracion entera, escrita como un guion de prueba.
particionar(G1, G2);            // se pierde todo mensaje entre los grupos

await escribir(G1.nodo, "v2");  // por disponibilidad, esta llamada TIENE que volver
const leido = await leer(G2.nodo);
// G2 no ha recibido nada sobre la escritura, asi que solo puede devolver "v1".
// Pero la lectura empezo despues de que la escritura terminara:
// no existe orden total compatible con el tiempo real. No es linealizable.
assert(leido === "v1");         // disponible y no consistente
// La unica alternativa era que G2 no respondiera: consistente y no disponible.

Hay una elegancia en la minimalidad del planteamiento que conviene apreciar, porque es lo que le da su fuerza. El objeto sobre el que se razona es lo más pobre imaginable: un registro con leer y escribir, dos clientes, dos grupos de nodos. Si la imposibilidad ya aparece ahí, aparecerá en cualquier sistema más rico, porque cualquier sistema más rico puede simular ese registro. Los resultados de imposibilidad se demuestran siempre así, sobre el caso más pequeño posible, y por eso resisten cualquier objeción del tipo pero mi base de datos hace cosas más listas: hacer cosas más listas nunca elimina una imposibilidad, como mucho la disimula en los casos frecuentes.

Fíjate en lo que la demostración no dice, que es tanto como lo que dice. No habla de rendimiento, ni de porcentajes, ni de bases de datos, ni de arquitecturas. Es un resultado de imposibilidad sobre un registro con dos operaciones. Toda la riqueza —y toda la confusión— aparece cuando se intenta aplicarlo a sistemas reales que no son registros y a garantías que no son linealizabilidad.

🌍

P no se elige

La partición no es una propiedad que concedas al sistema: es algo que el mundo te hace. Renunciar a P no significa diseñar distinto, significa no tener plan para cuando ocurra.

⚖️

Las tres no son comparables

C y A son garantías que el sistema ofrece; P es una hipótesis sobre el entorno. Ponerlas en una misma lista de tres elegibles es un error de categoría, no un atajo didáctico.

🔤

C no es la C que crees

La C de CAP es linealizabilidad, no la C de ACID ni la idea vaga de que los datos estén bien. Un sistema puede violar la C de CAP y ser perfectamente correcto según su especificación.

⏱️

No es una elección permanente

La disyuntiva solo existe mientras dura la partición, se toma operación por operación y puede resolverse distinto para cada tipo de dato del mismo producto.

El primero es el más grave y el que más arquitecturas ha estropeado. Un sistema descrito como CA no es un sistema que haya cambiado consistencia por disponibilidad renunciando a las particiones: es un sistema que se comportará mal cuando ocurra una partición, porque nadie decidió de antemano qué debía hacer. En cuanto la red pierda mensajes, ese sistema perderá C o perderá A igual que todos los demás. La única diferencia es que lo hará sin diseño, en el peor momento y con un comportamiento que nadie especificó.

El daño práctico de ese primer error es fácil de reconocer en el trabajo real: aparece cada vez que un equipo elige una pieza de infraestructura por su letra en lugar de por su comportamiento documentado bajo fallo. La pregunta útil ante cualquier componente nunca es qué dos letras dice tener, sino tres cuestiones concretas y verificables: qué ocurre exactamente con una escritura aceptada en el lado minoritario de una partición, si el sistema puede volver a servir un valor anterior a otro ya servido, y qué hace al reunificarse cuando los dos lados aceptaron cambios sobre el mismo dato. Un producto serio responde a las tres en su documentación; si no las responde, la letra que se atribuya no significa nada.

El segundo error explica por qué la etiqueta AP o CP dice tan poco de un sistema real. La disponibilidad de CAP es absoluta: toda petición a cualquier nodo no caído debe responderse. Bajo esa definición un sistema de quórum no es disponible, porque la minoría aislada rechaza peticiones, y sin embargo lo llamamos altamente disponible sin que nadie proteste. A la inversa, muchos sistemas etiquetados AP entregan garantías bastante más fuertes que la mera consistencia eventual. Las etiquetas son un titular, no una especificación.

El tercero es puramente terminológico y aun así hace estragos, porque la palabra consistencia se usa en la industria para al menos tres cosas incompatibles. En CAP significa linealizabilidad, una propiedad sobre el orden observable de las operaciones. En ACID significa que una transacción lleva la base de datos de un estado que cumple las restricciones declaradas a otro que también las cumple, lo cual no tiene nada que ver con réplicas ni con tiempo. Y en el habla cotidiana significa los datos están bien. Un sistema puede violar la C de CAP a plena luz y ser impecable en las otras dos acepciones; de hecho, esa es la descripción exacta de una aplicación local-first bien construida. Cuando alguien afirma que un sistema sacrifica la consistencia, la primera pregunta debe ser cuál de las tres.

El cuarto merece una precisión que ordena el resto del track. La disyuntiva no se aplica al producto entero sino a cada invariante. El cuerpo de un documento puede aceptar escrituras durante una partición sin ningún problema; el saldo de una cuenta no; la unicidad de un nombre de usuario tampoco. Un diseño maduro no elige una letra para toda la aplicación, elige una por cada clase de dato y asume que convivirán.

ℹ️
El matiz de la sincronía parcial

El resultado tal y como se demuestra vive en el modelo asíncrono puro. Los mismos autores estudian también el modelo parcialmente síncrono, en el que existe alguna cota sobre los retardos, y allí se puede tener un algoritmo que sea linealizable y disponible en toda ejecución sin particiones, y que durante una partición devuelva datos posiblemente obsoletos. Ese resultado es el que de verdad describe a los sistemas que usamos: no eligen letras para siempre, se comportan de un modo cuando la red va bien y de otro cuando se rompe. Todo el diseño interesante está en esa segunda modalidad.

La elección real ocurre durante la partición

Puesta en estos términos, la pregunta útil deja de ser qué dos letras eliges y pasa a ser una pregunta operativa con tres tiempos. Brewer la reformuló así en su revisión del teorema doce años después, y esta formulación es infinitamente más productiva que la del titular.

flowchart LR
N[modo normal] --> D[se sospecha una particion]
D --> M[modo de particion]
M --> L[limitar que operaciones se aceptan]
M --> R[registrar intenciones para el futuro]
L --> V[vuelve la conectividad]
R --> V
V --> F[fusionar estados divergentes]
F --> C[compensar lo que no se pudo evitar]
C --> N
style M fill:#f38ba8,color:#11111b
style F fill:#cba6f7,color:#11111b
style C fill:#fab387,color:#11111b

Esta lectura tiene una virtud que la formulación popular no tiene en absoluto: es accionable. De elige dos de tres no se deduce ninguna tarea concreta. De este ciclo se deducen cuatro, y las cuatro se pueden asignar, estimar y probar.

El primer tiempo es detectar, y la lección anterior ya te dejó sin ilusiones al respecto: detectar es apostar. Lo que sí puedes hacer es que la apuesta sea explícita y que el sistema sepa en qué modalidad cree estar. El segundo tiempo es decidir qué se permite mientras tanto, y aquí es donde la granularidad rinde: editar texto, sí; crear una entidad con identificador local, sí; consumir una plaza de un aforo limitado, probablemente no, o sí pero registrándolo como intención revocable en lugar de como hecho consumado.

La distinción entre hecho consumado e intención revocable es la que hace que esta modalidad sea aprovechable y no un simple modo degradado. Registrar una intención cuesta lo mismo que registrar un hecho —una escritura local— pero cambia por completo lo que la interfaz puede prometer y lo que la recuperación puede hacer sin traicionar a nadie.

async function ejecutar(operacion) {
  if (modo === "particion" && operacion.requiereInvariantesGlobales) {
    // No la rechazamos: la registramos como intencion, que es revocable.
    await registrarIntencion(operacion);
    return { estado: "pendiente", revocable: true };
  }
  // Lo que converge por diseno se aplica igual en los dos modos.
  await aplicarLocalmente(operacion);
  return { estado: "aplicada", revocable: false };
}

El tercer tiempo es el que casi nadie diseña y el que más importa: la recuperación. Cuando la conectividad vuelve, hay que fusionar estados que divergieron y, en los casos en los que se rompió un invariante que no podía romperse, compensar. Y compensar no siempre es una operación técnica. Si dos réplicas particionadas vendieron la última entrada, ninguna estructura de datos va a inventar un asiento; lo que hay es un reembolso, una disculpa y una política de negocio. Aceptar que existe esa categoría de problemas, en lugar de fingir que un algoritmo la absorbe, es parte del rigor de este nivel.

Vale la pena notar que la compensación no es una derrota ni un parche: es un mecanismo perfectamente respetable, y el mundo lleva siglos funcionando así. Las aerolíneas sobrevenden con conocimiento de causa y compensan al pasajero que se queda fuera porque el coste esperado de la compensación es menor que el de coordinar. Los cajeros automáticos siguen entregando efectivo cuando pierden la conexión con el banco, limitando el importe para acotar el daño. Lo que estos ejemplos enseñan es que la elección entre coordinar y compensar es económica antes que técnica, y que se decide comparando el coste de la latencia y la indisponibilidad con el coste esperado de reparar los casos raros.

💡
Nombra la modalidad en tu código, no solo en tu cabeza

Un síntoma fiable de que un equipo no ha interiorizado esta lección es que el sistema no tiene ninguna representación explícita de en qué modalidad cree estar. La lógica de partición aparece dispersa en capturas de error, cada componente decide por su cuenta qué hacer con un fallo de red y no existe ningún sitio donde consultar la política vigente. Haz lo contrario: que la modalidad sea un estado con nombre, que el paso de una a otra ocurra en un solo lugar, que las operaciones declaren en qué modalidades son admisibles, y que la recuperación sea una función que se pueda invocar y probar a voluntad. La diferencia práctica es que en el primer diseño la modalidad de partición no se prueba nunca y en el segundo se prueba en cada integración.

PACELC: el coste que sigue ahí sin partición

Queda un hueco que CAP no cubre y que resulta ser el decisivo para local-first. El teorema solo habla de lo que ocurre durante una partición, pero las particiones son minoritarias en el tiempo: la mayor parte de la vida del sistema transcurre con la red funcionando. Y en ese régimen normal la coordinación sigue costando algo, porque garantizar linealizabilidad exige hablar con otros antes de responder, y hablar con otros cuesta al menos un viaje de ida y vuelta.

Este hueco no es una omisión descuidada del teorema, sino una consecuencia de su enunciado: CAP habla de lo que es posible, no de lo que es rápido. Un sistema que tardase una hora en responder cada lectura seguiría siendo disponible según la definición formal, porque disponible significa que la respuesta llega, no que llegue a tiempo. Esa laxitud es imprescindible para que la demostración funcione en un modelo sin relojes, y a la vez es lo que hace que el teorema, por sí solo, no sirva para decidir arquitecturas de producto.

Daniel Abadi formalizó esa extensión con el acrónimo PACELC, que se lee como una frase con dos condicionales: si hay partición (P), elige entre disponibilidad (A) y consistencia (C); si no la hay (E, de else), elige entre latencia (L) y consistencia (C). La segunda mitad es la que explica por qué existe este track. Una aplicación local-first no adopta su arquitectura solo para sobrevivir al metro: la adopta porque incluso con red perfecta, preguntar antes de actuar impone un suelo de latencia que ninguna optimización elimina, y ese suelo es precisamente lo que arruina la sensación de que el documento es tuyo.

Esta segunda mitad tiene además la virtud de discriminar entre sistemas que CAP mete en el mismo saco. Dos bases de datos pueden coincidir en preferir la disponibilidad bajo partición y diferir por completo en lo que hacen el resto del tiempo: una replica de forma síncrona a varias regiones y paga ese viaje en cada escritura, la otra confirma en local y propaga después. Ambas serían AP en la clasificación popular y la diferencia entre ellas —decenas o centenas de milisegundos en cada operación— es la que decide si tu producto puede existir. La letra que de verdad importa es la que CAP no tiene.

Dicho en la nomenclatura completa, una aplicación local-first canónica es PA/EL: bajo partición prefiere seguir aceptando escrituras, y sin partición prefiere responder de inmediato desde la réplica local. Esa doble elección no se toma por gusto ni por moda técnica; se toma porque el producto que se quiere construir tiene la respuesta instantánea como requisito, y esa exigencia se propaga hacia abajo hasta obligar a renunciar a la linealizabilidad. La arquitectura no precede al requisito: se deduce de él.

CAP no te dice qué construir: te dice qué facturas vas a pagar y cuándo

El error de fondo de la versión popular no es la aritmética de las letras, es tratar el teorema como un menú de arquitecturas cuando en realidad es un resultado de imposibilidad sobre la información. Lo que dice, despojado de siglas, es esto: la certeza sobre lo que sabe otro solo se obtiene recibiendo mensajes suyos, y cuando los mensajes no llegan no hay ingeniería que fabrique esa certeza. De ahí se sigue todo lo demás sin necesidad de memorizar nada. Si quieres responder sin haber recibido noticias, responderás con información posiblemente vieja: eso es renunciar a la linealizabilidad. Si no quieres responder con información vieja, tendrás que callar hasta que lleguen noticias: eso es renunciar a la disponibilidad. Y como el silencio del otro es indistinguible de su muerte, ni siquiera puedes saber con seguridad en cuál de los dos regímenes estás operando. Visto así, el teorema deja de ser una restricción antipática y se convierte en una herramienta de honestidad: te obliga a nombrar, para cada dato de tu aplicación, qué prefieres perder cuando lo que llega es el silencio. La aplicación local-first responde a esa pregunta siempre igual —prefiero seguir funcionando— y esa respuesta uniforme tiene un precio exacto que las tres lecciones siguientes van a desglosar: si aceptas escrituras sin coordinar, tendrás réplicas que divergen; si tienes réplicas que divergen, necesitarás que vuelvan a coincidir; y si necesitas que vuelvan a coincidir sin árbitro, tendrás que renunciar a los invariantes que exigen verlo todo a la vez. CAP no elige por ti, pero no te deja fingir que no elegiste.

⚔️ Enuncia el teorema sobre tu propia aplicación
  1. Escribe con tus palabras la demostración en cuatro pasos, sin mirar, y compárala con la del texto.
  2. Lista los invariantes que tu aplicación promete hoy y marca cuáles exigen ver todas las escrituras a la vez.
  3. Para cada uno de los marcados, decide qué ocurre durante una partición: se rechaza, se acepta como intención revocable, o se acepta y se compensa después.
  4. Busca en tu sistema alguna pieza que asumas CA sin decirlo, y describe qué hace exactamente cuando la red pierde mensajes.
  5. Clasifica tu producto en la nomenclatura de PACELC y justifica por separado cada una de las dos elecciones.
  6. Mide el suelo de latencia que impondría coordinar cada escritura, y compáralo con el tiempo de respuesta que tu producto necesita.