Saltar al contenido principal

Leer tus propias escrituras: el modelo de consistencia de Zaris, explicado con honestidad

· 13 min de lectura
Clustron Team
Ingeniería de Sistemas Distribuidos

Leer tus propias escrituras — un propietario por partición

Hay una pregunta que todo desarrollador acaba por hacerle a un almacén distribuido, normalmente justo después de un informe de error confuso: si escribo un valor y luego lo leo de inmediato, ¿tengo la garantía de ver lo que escribí? Suena como si debiera tener una respuesta de una sola palabra. No la tiene — ni para ningún almacén particionado y replicado, ni para Zaris. La respuesta honesta es "sí, dentro de una partición, porque las lecturas van al mismo propietario único al que fue tu escritura" — y lo interesante es todo lo que se esconde en esa salvedad.

Esta entrada reúne en un solo lugar el modelo de consistencia de Zaris. Hemos escrito sobre las piezas por separado — compare-and-swap optimista, reintentos transparentes ante cambios de propiedad, el op-log reproducible que alimenta a las réplicas, transacciones multiclave. Aquí las enlazamos desde el lado del lector y respondemos a la única pregunta que importa frente a un teclado: cuando leo, ¿qué se me promete en realidad? Incluyendo, sobre todo, dónde se detiene la promesa.

El modelo en una frase​

Zaris es un almacén con tendencia AP cuyo límite de consistencia se traza en torno a la propiedad, no en torno a cada escritura. El plano de datos se mantiene disponible — un nodo solitario sigue sirviendo los segmentos que posee — y la disciplina de escritor único que mantiene las lecturas coherentes se aplica en el plano de propiedad: hay exactamente un propietario activo por partición en cada instante. Las escrituras a una partición se serializan a través de ese único nodo. Deliberadamente no hay quórum ni consenso en la ruta de escritura, y Zaris no afirma tener linealizabilidad estricta global.

Casi todo lo que observa un lector se desprende de ese único hecho estructural, así que conviene hacer explícita la cadena:

key → segment → partition → one runtime active owner

Una clave se asigna determinísticamente por hash a un segmento; un segmento pertenece a una partición; una partición tiene exactamente un propietario activo que acepta sus escrituras y sirve sus lecturas. La propiedad es un arrendamiento (lease) que viaja en el mapa de particiones como (LeaderTerm, OwnershipGeneration) — otorgado por el líder del clúster, duradero entre reinicios y propagado a cada nodo. Ese arrendamiento es lo que garantiza el "exactamente uno" — reemplazó a un esquema anterior de "gana la época más alta" que podía reiniciarse al arrancar y producir brevemente dos nodos creyendo cada uno que poseía la misma partición.

Por qué las lecturas enrutadas al propietario te dan leer-tus-escrituras​

Aquí está la parte que responde a la pregunta inicial. En Zaris, las lecturas se enrutan al propietario activo, el mismo nodo al que fue tu escritura. Los clientes no leen de las réplicas por defecto. Así que cuando haces PutAsync de una clave y luego GetAsync de ella, ambas solicitudes se resuelven — a través de la misma tabla de enrutamiento, construida a partir del mapa de particiones más la instantánea de propiedad en tiempo de ejecución — al mismo nodo propietario, y ese nodo ya tiene tu escritura en su almacén local.

Ese es todo el mecanismo. Leer-tus-escrituras no está añadido como una garantía de sesión ni como una caché del lado del cliente; se desprende del enrutamiento. Lees tus escrituras dentro de una partición porque un único nodo es la autoridad tanto para las lecturas como para las escrituras de esa partición, y las escrituras a él se serializan de una en una. La réplica de ese diagrama podría ir retrasada unos milisegundos respecto al propietario — la replicación tiene una cola asíncrona — pero por defecto nunca lees la réplica, así que ese retraso te resulta invisible.

La salvedad "dentro de una partición" es determinante, y volveremos a lo que excluye.

La versión que se te permite ver​

Cada valor que sirve el propietario lleva dos números monótonos, y son los que hacen de "el mismo valor" una afirmación precisa en lugar de una sensación:

  • una Revision propiedad del almacén — un número de secuencia estrictamente creciente que el propietario estampa en cada mutación en orden de confirmación, y
  • un ItemVersion por elemento — la versión de esa clave concreta, que es lo que IfMatchVersion protege en una escritura compare-and-swap.

Como el propietario los asigna en orden de confirmación y sirve las lecturas desde ese mismo estado confirmado, las lecturas sucesivas de una clave desde el propietario avanzan la versión hacia adelante, nunca hacia atrás. No leerás la versión 42 y luego la versión 40 un momento después. Esa monotonía es exactamente lo que permite que el bucle CAS sea correcto: un lector ve una versión, calcula a partir de ella y vuelve a escribir condicionado a que siga siendo esa versión. Si el modelo permitiera que las lecturas oscilaran entre versiones, la concurrencia optimista estaría construida sobre arena. Un propietario, revisiones en orden de confirmación, lecturas servidas por el propietario — ese es el fundamento sobre el que se apoya toda la historia de coordinación de la entrada sobre CAS.

La ventana en la que una lectura dice honestamente "todavía no"​

Las lecturas enrutadas al propietario son limpias en estado estable. Los momentos interesantes son las transiciones — una conmutación por error (failover), una migración, un primario preferido recuperándose — porque durante un traspaso el nodo al que enrutarías podría no ser la autoridad en ese momento. La elección de Zaris aquí es la honesta: en lugar de servir una lectura desde un nodo que no es el propietario asentado, devuelve un estado reintentable. Una lectura (o escritura) durante una ventana así puede volver como:

  • Moved — tu tabla de enrutamiento está obsoleta; la clave vive ahora en un nodo distinto.
  • OwnershipChanging — la propiedad de ese segmento está en transición activa ahora mismo; es un traspaso, no un error de enrutamiento asentado.
  • Migrating — el segmento está en plena migración a un nuevo propietario.
  • ReplicaSyncPending — este nodo está poniendo al día la copia de un segmento y todavía no servirá una lectura posiblemente incompleta desde él.
  • Unavailable — la partición no tiene un propietario alcanzable en este instante.

Ninguno de estos es una excepción que tengas que extraer de un rastreo de pila — son resultados legibles por máquina, y el cliente .NET los trata como transitorios y reintenta reenrutando, refrescando su mapa de clúster y dirigiéndose a quien el mapa ahora nombre como propietario. El efecto es que tu GetAsync simplemente tarda unos milisegundos más y luego devuelve el valor correcto. La decisión de diseño que hay debajo vale la pena enunciarla con claridad: Zaris prefiere hacerte esperar un instante que entregarte una lectura de la que no está seguro de que sea autoritativa. Un "todavía no" reintentable es mejor respuesta que una errónea con aplomo.

Por esto también sobrevive leer-tus-escrituras a una conmutación por error: el trabajo de vallado de escritura (write-fence) implica que una escritura confirmada no se queda atrás en la ventana de traspaso (llegaremos abajo al borde honesto de eso), y la promoción solo convierte a un nodo en propietario una vez que es elegible — de modo que la lectura que se reenruta tras una conmutación por error se reenruta a un nodo que tiene tus datos.

El ajuste que puedes girar — y lo que realmente hace hoy​

Zaris expone una superficie de consistencia de lectura. GetOptions lleva un ReadConsistency de Leader, Quorum o Any, y un almacén tiene una bandera AllowReplicaReads (por defecto desactivada). La intención es el compromiso clásico: dejar que las lecturas se sirvan desde réplicas para repartir la carga de lectura, a cambio de aceptar que una réplica pueda ir retrasada respecto al propietario — una obsolescencia acotada que eliges deliberadamente.

Aquí está el estado actual honesto, porque es exactamente el tipo de cosa que el texto de marketing difuminaría: hoy, las lecturas son solo del propietario. AllowReplicaReads y ReadConsistency.Any son una superficie de configuración y de modelado de solicitudes que existe y fluye a través de la definición del almacén, pero todavía no hay una ruta de código en el servidor que realmente sirva una lectura desde una réplica. En la práctica eso significa que ahora mismo obtienes el extremo fuerte del ajuste — cada lectura es una lectura del propietario, así que leer-tus-escrituras se sostiene — y no obtienes actualmente el comportamiento de lectura-de-réplica-obsoleta, aunque actives la bandera.

¿Por qué mencionar un ajuste que en efecto está fijado en un solo valor? Porque te dice qué permitirá intercambiar el contrato de consistencia, y porque la asimetría importa cuando razonas sobre obsolescencia. Cuando lleguen las lecturas servidas por réplica, activar AllowReplicaReads será el momento en que dejarías de tener garantizado leer-tus-escrituras: una lectura enrutada a una réplica puede observar una versión por detrás de la del propietario, durante todo el tiempo que la cola de replicación asíncrona vaya rezagada. Hasta entonces, la garantía es más simple y más fuerte de lo que sugiere el mando. Preferimos que lo sepas a que supongas que ya estás balanceando lecturas entre réplicas cuando no es así.

Qué significa "confirmado" para el siguiente lector​

Leer-tus-escrituras trata de que tú leas tu propia escritura en el propietario. Una pregunta distinta y más afilada es: una vez que el propietario te dice que una escritura está confirmada, ¿está esa escritura a salvo si el propietario muere después, de modo que el lector que se reenruta al nuevo propietario siga viéndola?

El contrato honesto de confirmación (ack) hoy: una escritura se confirma cuando aterriza en el almacén local del propietario activo. La replicación a las RF−1 réplicas viaja en una cola asíncrona por detrás de esa confirmación. Vale la pena planificar en torno a las consecuencias en lugar de pasarlas por alto:

  • Con redundancia 1 — sin réplica sincronizada — una escritura es genuinamente de mejor esfuerzo. No debe tratarse como duradera ante un solo fallo, porque hay exactamente una copia.
  • Incluso con réplicas, si se pierde el único propietario activo en la estrecha ventana después de que confirmara una escritura pero antes de que esa escritura llegara a una réplica, la escritura confirmada puede perderse.

La propiedad más fuerte — confirmar solo una vez que la escritura esté en al menos una réplica sincronizada, de modo que una escritura confirmada sobreviva a la pérdida de cualquier nodo — es el objetivo de diseño (la compuerta de confirmación "W1"), todavía no la semántica de confirmación entregada. Es la dirección hacia la que converge el trabajo del backbone de durabilidad, y esa entrada es cuidadosa con la misma distinción: la afirmación fuerte que valida está acotada a escrituras confirmadas, cuando existe una réplica sincronizada, bajo las clases de fallo que induce la ejecución de caos — no un "nada puede perderse nunca" incondicional. Los mecanismos de op-log y migración son cómo una réplica o un nuevo propietario se pone al día con el estado confirmado del propietario en primer lugar; la compuerta de confirmación trata de cuándo se le permite al propietario decir "hecho".

Así que la afirmación precisa de cara al lector es: leer-tus-escrituras en el propietario es sólido; la durabilidad ante fallos cruzados de una escritura confirmada es fuerte-y-cada-vez-más-fuerte, con la salvedad actual expuesta arriba, y un suelo duro en RF1 que ningún almacén puede sortear por ingeniería — una copia es una copia.

Entre particiones, no hay reloj compartido​

El último lugar donde muerde la salvedad "dentro de una partición": no hay ordenamiento ni instantánea global entre particiones. Cada partición se serializa a través de su propio propietario; dos particiones distintas son dos propietarios distintos con dos secuencias de revisión independientes. Si escribes la clave A (partición 1) y luego la clave B (partición 2), un lector concurrente puede observar el nuevo valor de B y el viejo de A, o viceversa — no hay un instante a nivel de clúster en el que "ambas ocurrieron". Las operaciones en bloque (bulk) y por lotes (batch) son por clave por debajo; son cómodas, no atómicas.

Cuando genuinamente necesitas que un invariante abarque varias claves — mover un saldo de A a B, mantener un índice sincronizado con el registro al que apunta — eso es exactamente para lo que existen las transacciones multiclave. Proporcionan aislamiento Serializable sobre una confirmación coordinada por el cliente a través de versiones optimistas, y son la única superficie de atomicidad entre claves. Las lecturas y escrituras multiclave ordinarias no son una transacción y no deben confundirse con una.

Qué se garantiza y qué no​

Dicho con claridad, para que puedas abarcar el modelo entero de una vez:

PropiedadZaris hoy
Propietario por particiónExactamente un propietario activo, mantenido como arrendamiento duradero.
Ruta de lecturaSe enruta al propietario activo; las réplicas no se leen por defecto.
Leer-tus-escriturasSí, dentro de una partición — el mismo propietario sirve tu escritura y tu lectura.
Lecturas monótonas de una claveSí — el propietario sirve revisiones en orden de confirmación; las versiones avanzan.
Lecturas durante el traspasoEstado reintentable (Moved / OwnershipChanging / Migrating / ReplicaSyncPending); el cliente reintenta y reenruta.
Ajuste de lecturas de réplica / obsolescenciaAllowReplicaReads + ReadConsistency.Any existen como superficie; solo del propietario en la práctica hoy (aún no hay ruta de servicio desde réplica).
Durabilidad de confirmaciónLocal al propietario hoy; duradera-ante-un-fallo-con-réplica-sincronizada es el objetivo (compuerta de confirmación W1).
Orden entre particionesNinguno — sin instantánea global; usa transacciones para atomicidad entre claves.
Linealizabilidad globalNo se afirma.

La forma honesta de todo esto​

Un buen modelo de consistencia no es el que tiene la lista más larga de garantías; es aquel cuyas garantías puedes nombrar de verdad y cuyos bordes puedes ver de verdad. Zaris traza su límite en torno a la propiedad y es franco sobre el compromiso: obtienes leer-tus-escrituras y lecturas monótonas dentro de una partición gratis, porque un propietario sirve ambos lados y te entrega versiones en orden de confirmación; obtienes un "todavía no" reintentable en lugar de una respuesta errónea con aplomo durante un traspaso; y renuncias a la linealizabilidad global y al orden entre particiones, que recuperas explícitamente con una transacción cuando un invariante concreto lo necesita.

Las dos salvedades que vale la pena llevar en la cabeza son las que esta entrada expuso en lugar de suavizar: el ajuste de lectura de réplica está, hoy, en efecto fijado en el valor fuerte, así que eres más consistente de lo que el mando implica; y "confirmado" significa actualmente en el propietario, con la durabilidad completa ante un solo fallo como objetivo que el trabajo de durabilidad está aterrizando, y un suelo genuino en redundancia 1. Conoce esas dos cosas y el resto del modelo es exactamente tan simple como parece: encuentra al propietario, lee tus escrituras.

Para el diseño más profundo, consulta Consistencia y Particionado y replicación en la documentación.