Cómo se forma un clúster de Zaris: membresía, el mapa y la disponibilidad condicionada a la convergencia
La mayor parte de lo que se escribe sobre almacenes distribuidos trata del estado estable —lecturas, escrituras, replicación— o de los fallos dramáticos: un failover cuando muere un propietario, un cut-over cuando se mueve una partición. Pero hay un momento más silencioso que decide si algo de eso funciona: el momento en que el clúster se forma. Antes de poder enrutar una sola clave, los nodos tienen que ponerse de acuerdo sobre quién está en el clúster, quién posee qué porción del espacio de claves y —el paso que la mayoría de los sistemas resuelven mal— si es seguro empezar a servir siquiera.
Este artículo sigue esa secuencia de arranque en el orden en que Zaris realmente la ejecuta: membresía, el mapa de particiones y las compuertas de disponibilidad que se interponen entre "el proceso está en marcha" y "el nodo está aceptando tráfico". También traza una línea, con honestidad, entre lo que Zaris implementa por sí mismo y lo que hereda del runtime de clúster subyacente sobre el que está construido.
Quién está en el clúster: membresía
Lo primero honesto que hay que decir es dónde está la frontera. El transporte de membresía de bajo nivel —heartbeats entre pares, detección de fallos y elección de líder— lo provee la capa Clustron.Nodus sobre la que está construido Zaris. Zaris la consume a través de una interfaz INodusNode: le pregunta a Nodus quiénes son los miembros, cuál ostenta el liderazgo y recibe notificaciones cuando se pierde o falla el heartbeat de un par. Zaris no reimplementa Raft ni un protocolo de descubrimiento basado en gossip; superpone su propio plano de control de particiones y propiedad sobre esa base.
Lo que Zaris añade es una máquina de estados de membresía explícita que traduce los eventos de heartbeat en bruto a decisiones relevantes para la propiedad. Vive en MembershipStateTracker, y sus estados son deliberadamente más matizados que "arriba / abajo":
Los estados caen en dos grupos, y esa división es todo el objetivo. Alive, Suspected y Unreachable afectan únicamente a la propiedad en tiempo de ejecución —pueden disparar la promoción de una réplica o marcar una partición como temporalmente no disponible, pero nunca mutan el mapa de particiones. Solo Lost, Evicted y Decommissioned hacen que un nodo sea elegible para la regeneración del mapa.
Esa distinción existe porque regenerar el mapa de particiones es caro y, hecho con demasiada prisa, peligroso: puede reorganizar innecesariamente particiones sanas. Así que cuando falla el heartbeat de un nodo, Zaris no reescribe el mapa de inmediato. Mueve el nodo a Unreachable e inicia una ventana de gracia (30 segundos por defecto). Durante esa ventana el clúster solo hace failover en tiempo de ejecución —promover una réplica para que las lecturas y escrituras sigan fluyendo— mientras le da al nodo la oportunidad de volver. Un flapeo, una pausa de GC, un breve corte de red: ninguno de ellos debería reescribir la propiedad. Solo si la ventana de gracia expira el nodo pasa a Lost, y solo cuando un grupo de partición completo ha sido expulsado se dispara la única ruta de regeneración del mapa impulsada por fallo. La regla de diseño, enunciada sin rodeos en el código que la conecta, es: un nodo que se va nunca regenera el mapa por sí solo.
Cómo se encuentran los nodos
El descubrimiento es basado en roster, no en descubrimiento por gossip. La lista autorizada de nodos es configuración, no algo que el clúster olfatee en tiempo de ejecución. En una configuración escrita a mano es el arreglo nodes —cada entrada declara un id, un host, un puerto de clúster (de pares) y un puerto de cliente. En Kubernetes, ese roster se sintetiza a partir de los ordinales de los pods del StatefulSet en lugar de mantenerse a mano, con el rol de primario preferido de un nodo derivado directamente de su ordinal.
Sí existe un mecanismo de gossip en Zaris, pero cumple una función distinta: es cómo los nodos mantienen convergente su vista del estado del clúster, no cómo descubren que los pares existen. Volveremos a él cuando hablemos de la disponibilidad.
Quién posee qué: el mapa de particiones
Una vez conocida la membresía, el clúster necesita un plan para el espacio de claves. Ese plan es el mapa de particiones —un único documento versionado contra el que enruta cada nodo.
De una clave a un propietario, en dos saltos
Una clave no se mapea directamente a una partición. Zaris usa un mapeo de dos niveles que está deliberadamente alineado byte a byte con Redis Cluster para que la misma aritmética de enrutamiento funcione tanto para clientes nativos como para el front-end RESP:
key --CRC16--> slot (0..16383) --proportional--> segment (0..255) --SegmentToPartition[]--> partition
El primer salto hace hash de la clave con CRC16 (CCITT/XMODEM, polinomio 0x1021) módulo 16384 —la función exacta que usa Redis Cluster, incluyendo el respeto a la co-ubicación por hash-tag, donde una clave escrita como user:{42}:profile solo hace hash de la porción dentro de las llaves para que las claves relacionadas caigan juntas. Los 16384 slots se mapean luego proporcionalmente sobre los segmentos del clúster —256 por defecto, fijos durante toda la vida del clúster— y una tabla SegmentToPartition asigna cada segmento a una partición. La partición, finalmente, es lo que tiene propietarios.
¿Por qué la capa adicional de segmentos en lugar de hacer hash directo a particiones? Porque los segmentos son una unidad estable y de grano fino que puedes mover entre particiones cuando el clúster crece o se rebalancea, sin rehashear cada clave. El paso de CRC16 a slot nunca cambia; solo cambia la asignación de segmento a partición.
Qué contiene un mapa de particiones
El mapa en sí es un PartitionMap —un registro serializado con MessagePack cuya forma te dice con cuánto cuidado se rastrea la propiedad. Los campos que vale la pena conocer:
Version— un contador monótono que se incrementa en cada cambio del mapa. Este es el número que hace seguro el commit (más abajo).TopologyVersion— se incrementa solo ante un cambio estructural real (un nodo o partición que entra o sale, un re-tile), no ante un cut-over de migración rutinario. Separar "cambió la forma" de "cambió un propietario" permite que el resto del sistema reaccione de forma proporcional.TotalSegments— 256 por defecto, constante durante toda la vida del clúster.Partitions— la lista de asignaciones, cada una con unPrimaryNodeId(el primario de ubicación),ReplicaNodeIdsy unaPartitionIdentityestable (un GUID acuñado en su nacimiento que sobrevive a la renumeración) más los campos de arrendamiento de propiedad:ActiveOwnerNodeId, unOwnershipGenerationacuñado por el líder y elLeaderTermque concedió el arrendamiento —que es lo que permite al clúster aislar (fence) a un ex-líder obsoleto para que no autorice propiedad.
Nota la división deliberada entre PrimaryNodeId (dónde el mapa prefiere que viva una partición) y ActiveOwnerNodeId (quién la está sirviendo realmente ahora mismo). Esos pueden diferir durante un failover o una migración, y mantenerlos como campos separados es lo que hace esas transiciones representables sin mentir sobre el plan en estado estable. El artículo sobre el modelo de consistencia explica por qué las lecturas enrutan a ese único propietario activo.
Cómo se planifica el mapa
El mapa no se arma de forma codiciosa. Lo produce un planificador puro y determinista —DesiredMapPlanner— que toma los ids de los nodos miembros, el conteo de segmentos, un conteo deseado de particiones y el factor de replicación, y devuelve un plan. Dos propiedades merecen mención:
- Divisiones de segmentos equilibradas y contiguas. Los segmentos se dividen tan uniformemente como sea posible, en tramos contiguos, con las primeras particiones absorbiendo el resto cuando no divide de forma limpia.
- Ubicación de grupos disjuntos. Los primeros N miembros se convierten en los primarios distintos de partición; los miembros restantes rellenan los espacios de réplica por turnos (round-robin), de modo que ningún nodo termina en dos grupos de partición distintos.
La razón por la que el planificador está escrito como una función pura de sus entradas es ganada con esfuerzo: hace que toda una clase de mapas defectuosos —una partición con un primario duplicado, un rango de segmentos no contiguo, una asignación degenerada— sea irrepresentable por construcción, en lugar de algo que detectas después de los hechos con un validador. Vale la pena ser honestos también sobre la limitación actual: la ubicación hoy es posicional y de grupos disjuntos; una ubicación estilo rendezvous que minimice el churn y mueva la menor cantidad posible de datos en cada regeneración es un refinamiento futuro explícito, no algo que ya se entregue. Lo que sí se entrega es un paso de ubicación pegajosa (sticky) que, en una regeneración por churn de membresía, mantiene en su sitio a un propietario vivo que está sirviendo, para que una partición sana no se mueva sin razón.
Qué significa "commit"
El mapa lo autoriza un nodo —el líder del plano de control— y nadie más. El líder genera un mapa candidato, lo pasa por una proyección del log de propiedad (el líder es el único escritor del log de propiedad, y el mapa servido es un plegado de ese log), y luego lo confirma localmente antes de avisar a nadie. El commit es una única regla estricta impuesta en TopologyManager.ApplyMap:
// A map is accepted only if it is strictly newer.
if (_currentMap != null && map.Version <= _currentMap.Version)
return false;
Un mapa con una versión menor o igual a la que ya tienes es rechazado, punto. Solo después de que el líder confirma difunde un MapChangedEvent, envía instantáneas directas del mapa a los pares y publica el cambio de enrutamiento a los clientes. Los seguidores extraen (pull) el mapa (en arranque en frío lo solicitan a pares ya conocidos, reintentando hasta que uno responda) y lo aplican a través de la misma guarda monótona. Como cada nodo solo acepta una versión estrictamente superior, dos nodos nunca pueden asentarse sobre mapas conflictivos de la misma versión —el número de versión es el punto de serialización.
Un matiz honesto de arranque en frío: un nodo sin líder que arranca solo puede acuñar un único mapa provisional de un solo nodo para no quedar paralizado, marcado como provisional y luego reemplazado por el mapa autoritativo multinodo una vez que el clúster se forma. Es una concesión a la disponibilidad, no la ruta de estado estable.
¿Es seguro servir?: disponibilidad condicionada a la convergencia
Aquí está el paso que separa un almacén en el que puedes confiar de uno que meramente arranca. Tener un proceso en marcha, e incluso tener un mapa, no significa que un nodo deba aceptar tráfico. Si un nodo empieza a servir antes de que su propiedad en tiempo de ejecución se haya puesto al día con el mapa que acaba de aplicar, puede enrutar una solicitud a una partición que en realidad todavía no posee. Zaris se niega a hacer eso, con dos compuertas distintas.
Compuerta uno: estabilidad de la formación del clúster
La primera compuerta decide cuándo se le permite al líder generar el primer mapa real. ClusterReadinessGate espera a que la membresía sea estable —al menos un número mínimo de nodos presentes de forma continua durante una ventana de duración-estable (3 segundos por defecto)— antes de confirmar un mapa. Esto evita que el líder autorice un mapa contra un clúster a medio formar donde los nodos todavía se están uniendo uno a uno, lo que significaría una regeneración inmediata un segundo después. Si la estabilidad no se alcanza dentro del timeout (30 segundos por defecto), la compuerta fuerza el arranque (force-bootstraps) de todos modos: la disponibilidad gana frente a esperar para siempre. El clúster prefiere arrancar un poco pronto a quedarse atascado.
Compuerta dos: la compuerta de tráfico por nodo
La segunda compuerta es la que realmente decide si un nodo abre su listener de cliente. ClusterTrafficReadinessGate.IsReady() devuelve verdadero solo cuando se cumplen todas las siguientes condiciones:
- existe un mapa confirmado,
- su tabla segmento-a-partición está completamente poblada (
length == TotalSegments), - existe un mapa de propiedad en tiempo de ejecución,
runtime.MapVersion == map.Version— la propiedad en tiempo de ejecución del nodo está alineada con la versión exacta del mapa que ha confirmado,- y cada partición tiene una entrada en tiempo de ejecución.
Esa cuarta condición es el corazón del asunto. Un nodo no se anunciará como listo para servir hasta que su propio mapa confirmado y su propia propiedad en tiempo de ejecución estén en la misma versión. Esto es lo que señala la imagen principal en la parte superior. Hay un respaldo (backstop) de 60 segundos para que un nodo que no puede converger no cuelgue el arranque para siempre —pasado ese punto procede con una advertencia en lugar de quedarse atascado— pero en el caso normal, la compuerta mantiene la puerta cerrada hasta que los dos planos coinciden.
Mantenerse convergente: digests y detectar-y-sanar
La disponibilidad es una propiedad del momento de arranque; mantenerse convergente es una del estado estable, y aquí es donde el gossip de Zaris realmente se gana el sustento. Cada heartbeat lleva un pequeño ClusterStateDigest —el epoch del nodo, quién cree que es el líder, su versión del mapa, su conteo de nodos y las generaciones de propiedad por partición. Sin RPC adicionales; viaja sobre el tráfico que el clúster ya envía.
Un DigestAccumulator recopila los digests de los pares y, cada pocos segundos, los compara. Si un nodo diverge de la mayoría durante dos ciclos de evaluación consecutivos, levanta una señal de deriva (drift), y un HealingOrchestrator actúa sobre ella —el líder envía directivas correctivas (con un enfriamiento para que no se descontrole), y un nodo que se encuentra como el único atípico se auto-sana re-extrayendo el mapa. Esto es convergencia por detectar-y-reparar, no por una votación de quórum previa al servicio.
Esa distinción importa para la honestidad, así que déjame ser preciso al respecto. En producción, el acuerdo sobre la versión del mapa en todo el clúster se mantiene mediante la compuerta de tráfico por nodo más este bucle de sanación impulsado por digests —no mediante una barrera global que bloquee todo el servicio hasta que cada nodo haya votado sobre una versión. La afirmación más fuerte —que cada instantánea de nodo coincida sobre un líder, un epoch de líder y una versión de mapa— es lo que el arnés de certificación de caos de 12 horas verifica continuamente como un invariante de aprobado/reprobado. La certificación prueba que la propiedad se sostiene; el runtime la logra mediante compuertas y sanación en lugar de una ronda de consenso bloqueante en la ruta de servicio.
Factor de replicación, y un valor por defecto honesto
Una última pieza de la historia de la formación, porque cambia lo que "listo" significa para tu durabilidad. El factor de replicación por defecto es 1. Con RF=1, cada partición tiene un primario y ninguna réplica —una sola copia. El planificador coloca RF - 1 réplicas por partición, así que en el valor por defecto eso son cero. Un clúster con RF=1 se forma, converge y sirve perfectamente bien, pero la pérdida de un nodo es una pérdida de datos para las particiones que poseía, por diseño, no por accidente. Si quieres que una partición sobreviva al fallo de un nodo, pones RF >= 2, y necesitas suficientes nodos para colocar realmente esas réplicas en máquinas distintas —el artículo sobre dimensionamiento de durabilidad en Kubernetes trabaja exactamente cuántos pods hacen falta. El conteo de particiones tiene un valor por defecto igualmente honesto: si no se establece, el clúster usa una partición por nodo.
La secuencia, de principio a fin
En conjunto, un clúster de Zaris se forma así:
- La membresía se resuelve a partir del roster de configuración (o los ordinales de k8s), y Nodus elige un líder del plano de control.
- El líder espera detrás de la compuerta de formación a tener una membresía estable, luego planifica un mapa con
DesiredMapPlannery lo confirma localmente bajo la estricta guarda de versión monótona. - El líder difunde el mapa; los seguidores lo extraen y lo aplican bajo la misma guarda, aceptando solo versiones estrictamente superiores.
- Cada nodo se mantiene detrás de su compuerta de tráfico hasta que
runtime.MapVersion == map.Versiony el enrutamiento está completamente poblado —solo entonces abre su listener de cliente. - En estado estable, los digests transportados por heartbeat mantienen convergente la vista de cada nodo, con detección de deriva y sanación impulsada por el líder cerrando cualquier brecha.
Ninguno de estos pasos es dramático por sí solo. Juntos son la diferencia entre un clúster que está corriendo y uno que está listo —y la brecha entre esas dos palabras es donde un almacén pierde datos silenciosamente si no se toma la pregunta en serio.
Zaris es el almacén clave-valor distribuido de Clustron. Si quieres ver el plano de control desde el lado del operador, el recorrido en PowerShell crea, arranca e inspecciona un clúster en vivo; y el escalado elástico cubre qué le pasa al mapa cuando añades un servidor a un clúster que ya está sirviendo.