El espectro de consistencia
Recorrido preciso por la consistencia fuerte, la causal, la eventual y la fuerte eventual, con lo que garantiza cada una y por qué solo la última promete acuerdo como propiedad de seguridad y no como esperanza.
Una vez aceptado que bajo partición hay que renunciar a la linealizabilidad, aparece la pregunta que de verdad importa: renunciar, ¿a cambio de qué? Porque entre el sistema se comporta como si fuera una sola máquina y el sistema no promete absolutamente nada hay un espacio enorme, poblado de garantías intermedias que se definen con precisión matemática y que se pagan a precios muy distintos. Ese espacio es el espectro de consistencia, y recorrerlo con cuidado es lo que te permitirá dejar de decir consistencia eventual como si fuera una respuesta y empezar a decir exactamente qué promete tu sistema, cuándo lo promete y qué clase de promesa es. La distinción final de esta lección, entre consistencia eventual y consistencia fuerte eventual, parece un matiz de nomenclatura y es la frontera que separa a los sistemas que convergen de los que confían en converger.
- Definir un modelo de consistencia como un contrato sobre historias admisibles y no como una cualidad difusa.
- Enunciar linealizabilidad, consistencia secuencial y serializabilidad separando lo que cada una ordena.
- Entender por qué la consistencia causal es el techo alcanzable sin coordinación y por qué no basta para converger.
- Distinguir la consistencia eventual de la fuerte eventual mediante la separación entre liveness y safety.
- Enunciar las tres condiciones formales de la consistencia fuerte eventual, que es la que persiguen los CRDT.
Qué es exactamente un modelo de consistencia
Un modelo de consistencia no es una medida de calidad ni un adjetivo comercial: es un contrato entre el sistema de almacenamiento y quien programa contra él. Formalmente, delimita el conjunto de historias —secuencias de invocaciones y respuestas de operaciones— que el sistema tiene permitido exhibir. Un modelo más fuerte permite menos historias, y por eso deja menos casos raros que considerar al razonar sobre el programa. Un modelo más débil permite más historias, y por eso deja más trabajo del lado de quien escribe la aplicación.
De esa definición se sigue inmediatamente por qué el espectro existe y por qué nadie elige siempre el extremo fuerte. Reducir el conjunto de historias admisibles significa impedir que ciertas cosas ocurran, y para impedir que una réplica haga algo hay que informarla, es decir, hay que enviarle mensajes y esperar. Cada bit de fuerza en el modelo se paga en comunicación. El espectro no es una escala de mejor a peor: es una escala de cuánta comunicación estás dispuesto a exigir antes de responder.
Conviene notar también que el contrato es una cota superior sobre lo que puede pasar, no una descripción de lo que suele pasar. Un sistema que ofrece consistencia eventual puede comportarse durante meses como si fuera linealizable, porque la red va bien y nadie edita a la vez, y eso no le añade ni un ápice de garantía: lo único que ha ocurrido es que las historias raras que el contrato permite no se han materializado todavía. Programar contra el comportamiento observado en lugar de contra el contrato declarado es la forma más común de acumular fallos que solo aparecen el día de más carga, que es exactamente el día en que las historias raras dejan de ser raras.
Hay una segunda dimensión que casi nunca se explicita y que resultará decisiva al final de esta lección. Las propiedades que un sistema puede prometer se dividen en dos familias. Una propiedad de seguridad —safety— afirma que algo malo no ocurre nunca, y se puede violar en un instante concreto y observable. Una propiedad de viveza —liveness— afirma que algo bueno acaba ocurriendo, y ninguna ejecución finita puede demostrar que se ha incumplido, porque siempre cabe decir que aún no ha pasado suficiente tiempo. Guarda esta distinción: es la que separa las dos últimas garantías del espectro.
Consistencia fuerte y sus vecinas
Bajo la etiqueta consistencia fuerte se agrupan modelos que conviene no confundir, porque ordenan cosas distintas. Todos comparten la ambición de que exista un solo relato de lo ocurrido, y se diferencian en con qué debe ser compatible ese relato y sobre qué unidad se define.
La linealizabilidad exige que exista un orden total de todas las operaciones sobre un objeto tal que cada una parezca ocurrir instantáneamente en algún punto entre su invocación y su respuesta, y que ese orden sea compatible con el tiempo real: si una operación terminó antes de que otra empezase, tiene que ir antes. Es la garantía más fuerte de las habituales y la que hace que un sistema replicado sea indistinguible de una sola máquina. Tiene además una propiedad valiosísima, la composicionalidad: si cada objeto por separado es linealizable, el sistema completo también lo es. Eso es lo que permite razonar por partes.
La consistencia secuencial relaja exactamente un requisito: pide un orden total compatible con el orden de programa de cada proceso, pero no con el tiempo real. Dos procesos pueden estar de acuerdo en una historia que contradice lo que un observador externo con un cronómetro vio ocurrir. A cambio de esa relajación se pierde la composicionalidad, lo cual la hace bastante menos cómoda de lo que su cercanía a la linealizabilidad sugiere.
Conviene entender de dónde sale exactamente el coste de la linealizabilidad, porque no es obvio. Lo caro no es el orden total, que se podría acordar con calma: lo caro es la cláusula del tiempo real, la que obliga a que una operación ya terminada sea visible para cualquiera que empiece después. Esa cláusula obliga a que cada operación deje huella en un conjunto de nodos suficientemente grande antes de devolver el control, porque de lo contrario un lector podría preguntarle a alguien que no se enteró. De ahí sale el viaje de ida y vuelta obligatorio, y de ahí que ninguna caché, ningún índice y ninguna optimización local lo eliminen. Se está pagando por una promesa sobre el tiempo, y el tiempo solo se puede constatar comunicándose.
La serializabilidad es una propiedad de otra naturaleza y por eso no está en la misma escala: habla de transacciones sobre varios objetos y exige que el resultado equivalga a alguna ejecución en serie de esas transacciones, sin decir nada del tiempo real. Cuando se quieren ambas cosas —el orden serial y el respeto al tiempo real— se habla de serializabilidad estricta. Confundir serializabilidad con linealizabilidad es uno de los malentendidos más frecuentes del oficio, y se evita recordando que una ordena transacciones y la otra ordena operaciones sobre un objeto.
Causal: el techo de lo que se puede tener sin coordinar
La consistencia causal abandona la ambición de un orden total y se conforma con respetar el orden parcial de la causalidad potencial, el mismo que Lamport formalizó como relación sucede antes. Su garantía se enuncia así: si una operación a pudo haber influido en otra b, entonces ninguna réplica puede observar b sin haber observado antes a. Las operaciones concurrentes, aquellas entre las que no hay relación de influencia posible, quedan libres: cada réplica puede aplicarlas en el orden que le llegue.
flowchart TD A[alicia escribe una pregunta] --> B[bruno lee la pregunta y responde] A --> C[carla escribe algo sin haber visto nada] B --> D[ninguna replica puede ver la respuesta sin ver la pregunta] C --> E[la escritura de carla es concurrente y puede llegar en cualquier orden] D --> F[la causalidad se respeta sin coordinar con nadie] E --> F style D fill:#89b4fa,color:#11111b style E fill:#fab387,color:#11111b style F fill:#a6e3a1,color:#11111b
// La causalidad se transporta con el mensaje: no hay nada que preguntar.
function puedeEntregarse(mensaje, yaVisto) {
// Solo se entrega cuando ya hemos visto todo aquello de lo que depende.
return mensaje.dependencias.every((d) => yaVisto.has(d));
}
function recibir(mensaje) {
if (!puedeEntregarse(mensaje, yaVisto)) {
enEspera.push(mensaje); // se retiene hasta que lleguen sus precedentes
return;
}
aplicar(mensaje);
yaVisto.add(mensaje.id);
reintentarLosQueEsperaban(); // alguno puede haberse desbloqueado ahora
}
Su virtud es enorme: se puede implementar sin coordinación, porque respetar la causalidad solo exige acompañar cada mensaje con información sobre lo que su emisor había visto, y esa información viaja con el propio mensaje. Nadie tiene que preguntar nada a nadie antes de actuar. Por eso los sistemas siempre disponibles pueden ofrecerla, y por eso resulta especialmente relevante en local-first: es la garantía que hace que nunca veas una respuesta a un mensaje que todavía no ha aparecido.
Por debajo de la causalidad, y como casos particulares suyos, viven cuatro garantías modestas que en la práctica son las que hacen que una interfaz se sienta sensata, y que conviene saber nombrar porque a menudo son todo lo que de verdad necesitas. Lee tus escrituras: una réplica nunca te muestra un estado anterior a algo que tú mismo escribiste. Lecturas monótonas: si ya viste un estado, no volverás a ver uno más antiguo. Escrituras monótonas: tus propias escrituras se aplican en todas partes en el orden en que las hiciste. Las escrituras siguen a las lecturas: si escribes algo después de haber leído algo, tu escritura se verá siempre después de aquello que leíste. Las cuatro juntas equivalen a la consistencia causal restringida a la sesión de un usuario, y las cuatro son gratis en local-first por una razón feliz: si lees y escribes contra tu propia réplica, las obtienes por construcción.
Y hay un resultado que la convierte en algo más que una opción cómoda. Se ha demostrado que la consistencia causal es el modelo más fuerte que puede implementar un sistema que quiera permanecer siempre disponible y converger. No es que sea la mejor opción práctica: es que por encima de ella no hay nada alcanzable sin renunciar a la disponibilidad. Eso convierte a la causalidad en el techo del espacio de diseño en el que se mueve todo este track, y explica por qué los niveles siguientes dedican tanto esfuerzo a representar la causalidad de forma barata.
Aquí hay una trampa que confunde a mucha gente competente. La consistencia causal restringe el orden en el que cada réplica puede observar las operaciones, pero no dice nada sobre el estado al que llegan dos réplicas que han visto exactamente lo mismo. Si dos escrituras son concurrentes, la causalidad permite explícitamente que una réplica aplique primero una y otra la otra, y si esas operaciones no conmutan, ambas réplicas acaban en estados distintos habiendo respetado la causalidad al pie de la letra. Por eso los sistemas serios hablan de causal más convergencia: la causalidad se ocupa de lo observable y hace falta un mecanismo independiente que se ocupe del acuerdo. Ese mecanismo es el objeto del resto de la lección.
Eventual frente a fuerte eventual
Llegamos así al tramo débil del espectro, donde vive la etiqueta más repetida del sector y también la peor entendida. Merece un examen especialmente cuidadoso, porque la distancia entre lo que la gente cree que promete y lo que promete de verdad es la mayor de toda la lección.
La consistencia eventual, en su formulación más citada, dice: si no se realizan nuevas actualizaciones sobre un dato, con el tiempo todos los accesos a ese dato devolverán el último valor escrito. Léela otra vez despacio, porque promete mucho menos de lo que aparenta. Está condicionada a que las escrituras cesen, cosa que en un sistema vivo no ocurre nunca. No dice nada sobre lo que puedes leer mientras tanto. No dice cómo se decide cuál es el último valor cuando hubo dos concurrentes. Y, sobre todo, es una propiedad de viveza: promete algo bueno para el futuro, y por tanto ninguna observación finita puede demostrar que se está incumpliendo.
Esa última observación tiene una consecuencia práctica que conviene explotar cuando alguien te venda un sistema con esta etiqueta. Al ser una promesa de viveza, ninguna prueba puede demostrar que se incumple: si dos réplicas discrepan hoy, la respuesta siempre disponible es que aún no ha llegado el momento del eventualmente. Una garantía irrefutable no es una garantía fuerte, es una garantía vacía de contenido operativo. Por eso la pregunta correcta ante un sistema que se anuncia como eventualmente consistente no es si converge, sino dos cosas mucho más concretas: qué se puede observar mientras tanto, y qué regla decide el resultado cuando hubo escrituras concurrentes. Casi siempre resulta que la segunda respuesta es gana la última según el reloj del que escribió, con todo lo que eso implica.
La consistencia fuerte eventual —enunciada por Shapiro, Preguiça, Baquero y Zawirski en 2011, en el mismo trabajo que sistematiza los CRDT— corrige exactamente esa debilidad con tres condiciones que conviene saber de memoria. Primera, entrega eventual: toda actualización entregada en una réplica correcta acaba entregándose en todas las réplicas correctas. Segunda, terminación: toda ejecución de un método termina. Tercera, y es la decisiva, convergencia fuerte: dos réplicas correctas que han entregado el mismo conjunto de actualizaciones tienen estados equivalentes.
Fuerte
Orden total respetando el tiempo real. El sistema parece una sola máquina. Exige coordinar antes de responder y por tanto es incompatible con la disponibilidad bajo partición.
Causal
Respeta el orden parcial de la influencia. Se implementa sin coordinación y es el techo demostrado de lo alcanzable siempre disponible. No garantiza por sí sola que dos réplicas coincidan.
Eventual
Promete acuerdo si las escrituras cesan. Es viveza pura, no dice nada del presente ni de cómo se arbitran los concurrentes, y ninguna ejecución finita la puede refutar.
Fuerte eventual
Añade convergencia fuerte, que es seguridad: haber visto lo mismo implica estar igual, ya, sin negociar ni deshacer. Es la garantía que persiguen los CRDT.
Observa cómo se reparten las tres condiciones entre las dos familias del principio, porque ahí está la clave de todo. La entrega eventual y la terminación son propiedades de viveza: dependen de que la red acabe funcionando y de que los procesos acaben ejecutando, y ninguna implementación puede garantizarlas contra una red que nunca entregue nada. La convergencia fuerte, en cambio, es de seguridad, y por tanto se cumple siempre, incluso durante una partición eterna. Ese reparto es exactamente lo que hace útil la definición: separa lo que depende del mundo, que es inevitable, de lo que depende del diseño, que sí está bajo tu control. Un sistema con consistencia fuerte eventual no promete que la red funcione; promete que si los mensajes llegan, el acuerdo es inmediato y no hay nada más que negociar.
La diferencia entre las dos últimas es de naturaleza lógica y no de grado. La eventual promete que la divergencia se resolverá; la fuerte eventual prohíbe que exista divergencia entre réplicas que han visto lo mismo. Al ser una propiedad de seguridad, es observable y refutable: puedes coger dos réplicas, comprobar que han entregado el mismo conjunto de actualizaciones, comparar sus estados y saber inmediatamente si el sistema cumple o no cumple. Eso es exactamente lo que hace posible verificar un CRDT con pruebas basadas en propiedades, y lo que hace imposible verificar una promesa de consistencia eventual.
// La convergencia fuerte es una propiedad comprobable, no una esperanza.
function convergenciaFuerte(operaciones) {
const a = replica();
const b = replica();
for (const op of operaciones) a.entregar(op);
for (const op of barajar(operaciones)) b.entregar(op); // mismo conjunto, otro orden
// Sin ninguna comunicacion entre a y b, sin deshacer nada, sin arbitro:
return equivalentes(a.estado, b.estado);
}
Hay un límite de la consistencia fuerte eventual que conviene reconocer ahora para no llevarse un desengaño dentro de veinte niveles. La convergencia fuerte garantiza que todas las réplicas acaben en el mismo estado; no dice absolutamente nada sobre si ese estado es el que los usuarios querían. Una regla que descarte sistemáticamente la escritura de quien tenga el identificador más pequeño converge de manera impecable y destruye trabajo con la misma impecabilidad. Preservar la intención de quien editó es una exigencia distinta, mucho más difícil de formalizar y específica de cada tipo de dato, y es la que explica por qué no basta con un CRDT genérico y por qué hay estructuras distintas para contadores, conjuntos, secuencias y árboles. Acuerdo y acierto son dos objetivos, y solo el primero es un teorema.
Falta un aviso final para no cometer el error simétrico. La consistencia fuerte eventual no es un punto más de la misma recta que va de linealizable a eventual: es una garantía ortogonal a la causalidad. La causal habla de qué órdenes de observación se permiten en una réplica; la fuerte eventual habla del acuerdo entre réplicas dado un mismo conjunto entregado. Un sistema puede ofrecer convergencia fuerte sin respetar la causalidad, y puede respetar la causalidad sin converger nunca. Los sistemas que de verdad quieres construir ofrecen las dos, y por eso el resto del track se dedica a las dos: primero a representar la causalidad, después a construir estructuras que convergen.
Lo que hace tan valioso este espectro no es la taxonomía sino el cambio de perspectiva que instala: un modelo de consistencia no describe cómo de bueno es tu sistema, describe qué razonamientos tienes derecho a hacer dentro de tu código sin comprobarlo. Con linealizabilidad puedes escribir si acabo de guardarlo, al leerlo estará, y esa frase es un teorema. Con consistencia causal esa frase deja de ser cierta en general y solo sobrevive en su versión causal, si yo lo guardé, yo lo veré, que es más débil de lo que parece porque no dice nada de lo que verá otro. Con consistencia eventual pierdes incluso el derecho a afirmar que el estado que estás mirando corresponde a algún instante que haya existido, porque una lectura puede mezclar información de varias réplicas sin que ningún observador haya visto jamás esa combinación. Y con consistencia fuerte eventual recuperas una afirmación distinta y muy poderosa, que no habla del pasado sino de la relación entre pares: si tú y yo hemos recibido las mismas actualizaciones, ya estamos de acuerdo, sin hablar entre nosotros. Fíjate en la forma de esa última frase, porque es el corazón del track. No promete que llegaremos a un acuerdo, promete que el acuerdo es una consecuencia automática de haber recibido lo mismo, y por tanto elimina la negociación, elimina el árbitro y elimina la posibilidad de que a un usuario se le deshaga algo que ya vio aplicado. Ese es el premio, y no es gratis: se paga restringiendo qué operaciones puedes ofrecer, porque la única manera de que el acuerdo sea automático es que el orden de llegada deje de importar. Cómo se consigue eso, y por qué el orden es exactamente el obstáculo, es lo que toca a continuación.
- Escribe la garantía que tu aplicación ofrece hoy en una frase que empiece por si dos réplicas han recibido y termine sin la palabra normalmente.
- Busca en tu código una lectura que asuma ver de inmediato lo que otro usuario acaba de escribir, y decide qué modelo haría falta para que esa suposición fuera legítima.
- Construye un caso concreto que respete la causalidad y aun así deje dos réplicas en estados distintos.
- Determina si tu garantía actual es de viveza o de seguridad, y en el primer caso escribe qué observación finita bastaría para refutarla.
- Implementa la prueba de convergencia fuerte del ejemplo sobre tu propia estructura de datos, barajando el orden de entrega.
- Enumera qué operaciones de tu producto exigirían linealizabilidad de verdad, y comprueba cuántas son realmente.