El factor de replicación no es tolerancia a fallos: dimensionar un clúster de Zaris para durabilidad en Kubernetes
Hay una línea de razonamiento tentadora cuando despliegas un almacén replicado en Kubernetes: fijé replicationFactor: 2, así que cada partición tiene un backup, así que sobrevivo a la pérdida de un nodo. Suena hermético. También es, para un tamaño de clúster sorprendentemente común, incorrecto — e incorrecto de la forma silenciosa que solo aparece la primera vez que un pod realmente muere.
Zaris en Kubernetes almacena la data en memoria y la hace durable por replicación, no por disco: el volumen de data del nodo es un emptyDir, y un pod reprogramado reconstruye a partir de la copia que tiene otro pod. Ese diseño es sólido — pero se apoya enteramente en que haya una copia en otro pod. Que esa copia exista no lo decide replicationFactor por sí solo. Lo decide la aritmética entre tu número de pods y tu factor de replicación, y si te equivocas en esa aritmética el almacén correrá perfectamente, servirá cada lectura y escritura, y en silencio sostendrá una partición que no tiene a dónde replicarse.
La forma del despliegue
El chart de Helm de Zaris despliega la capa de nodos como un StatefulSet, una instancia de nodo por pod (zaris-0, zaris-1, …), cada una un propietario de shard en memoria. Un Service headless le da a cada pod una identidad de DNS estable, la readiness se condiciona a la convergencia del clúster, y un PodDisruptionBudget más una ventana de terminación con drenado elegante protegen el conjunto durante las interrupciones voluntarias.
# values.yaml (defaults)
node:
replicas: 4 # pod count — ONE node instance per pod
replicationFactor: 2 # copies of each partition, on distinct pods
Las propias notas del chart son contundentes sobre de dónde viene la durabilidad:
La data del nodo es emptyDir. La durabilidad en memoria es vía replicación (RF), no disco; un nodo reprogramado reconstruye a partir de las réplicas.
Así que un pod de nodo puede ser matado y reprogramado todo el día sin pérdida de data — siempre que sus particiones tengan una réplica viva en un pod diferente del cual reconstruir. Esa condición es todo el partido.
Cómo aterrizan las particiones en los pods
Una partición en Zaris la sostienen RF instancias de nodo distintas — un propietario activo más RF−1 réplicas — y las réplicas siempre se colocan en pods diferentes (una réplica en el mismo pod que su activo no sería protección alguna). El runtime coloca las particiones de forma codiciosa:
El runtime llena una partición hasta su RF completo antes de formar la siguiente.
Esa única frase es la clave de toda la historia del dimensionamiento. Las instancias se consumen de RF en RF. Así que el número de particiones completamente replicadas que obtienes es:
fully-replicated partitions = ⌊ instances ÷ RF ⌋
y cualquier instancia sobrante — instances mod RF de ellas — forma una partición de una sola copia sin par al cual replicarse. Es una partición real, que sirve. Simplemente no tiene backup.
Con cuatro pods a RF 2 obtienes dos particiones, cada una con dos copias en pods distintos — pierde cualquier pod y la copia superviviente continúa mientras la partición se re-replica en el pod reprogramado. Con tres pods a RF 2, los pods 0 y 1 forman la partición A a RF completo, pero el pod 2 queda sobrante y forma la partición B con una sola copia. Añade un cuarto pod y se convierte en la réplica de B. Hasta entonces, B es un pasivo.
La regla de dimensionamiento, como tabla
Como cada partición necesita RF pods distintos y Kubernetes corre una instancia por pod, la regla es simplemente:
Corre un número de pods que sea múltiplo de RF. Con RF 2, usa un número par de pods.
| Pods (1 instancia cada uno) | RF | Particiones completamente replicadas | ¿Sobrevive la pérdida de un pod? |
|---|---|---|---|
| 2 | 2 | 1 | Sí |
| 3 | 2 | 1 (+ 1 de una sola copia) | No — perder el pod de una sola copia pierde esa partición |
| 4 | 2 | 2 | Sí |
| 6 | 2 | 3 | Sí |
El patrón se generaliza: N particiones a RF 2 necesitan 2N pods; a RF 3 necesitan 3N. Escalar hacia afuera significa añadir pods de un RF completo a la vez si quieres que cada nueva partición nazca durable.
Por qué tres pods con RF 2 es la trampa
Tres es el número que muerde, porque tres pods parecen un clúster sano y de alta disponibilidad. Pasa la readiness. Sirve tráfico. kubectl get pods muestra zaris-0..2 todos 1/1 Ready. Nada está obviamente mal.
Lo que está mal es invisible hasta que el pod de una sola copia — el que sostiene la única copia de la partición B — es el pod que muere. En ese instante las claves de B se han ido, permanentemente, porque nunca hubo una segunda copia de la cual reconstruir. Esto no es un bug; es la misma advertencia que correr una sola máquina, solo que acotada a una partición en lugar de a todo el almacén. El mapa solo se regenera cuando se pierden todas las copias de una partición, y para una partición de una sola copia "todas las copias" es un pod.
Esto se midió, no se asumió. En un clúster kind:
- Con
replicas=4(2 particiones × RF 2), matar el pod propietario-activo de una partición perdió 0 de 200 claves — la réplica tomó el relevo y el RF se autorrestauró en el pod reprogramado. - Con
replicas=3, matar el pod de una sola copia perdió las claves de esa partición por completo.
Mismo almacén, mismo ajuste de RF, mismo caos. La única variable fue si el número de pods era múltiplo de RF.
Las barandillas que el chart te da
El chart no deja esto al folclore. Instala un clúster cuyo número de pods no sea múltiplo de RF y helm install imprime una advertencia de durabilidad en sus notas:
⚠ DURABILITY WARNING
node.replicas (3) is not a multiple of node.replicationFactor (2), so at least one
partition is SINGLE-COPY and will LOSE its data if that pod is lost. For fault
tolerance set node.replicas to a multiple of 2 (e.g. 4).
Más allá de la advertencia, varios defaults se fijan para que un clúster correctamente dimensionado se comporte bien bajo interrupción:
- Readiness condicionada a la convergencia.
/readyzpasa solo una vez que el mapa de particiones y la propiedad están vivos, de modo que un podReadyes uno enrutable — Kubernetes no enviará tráfico a un nodo que aún no ha tomado la propiedad. - PodDisruptionBudget. Por defecto, a lo sumo un nodo se interrumpe voluntariamente a la vez, de modo que un drenado o una actualización rodante no puede sacar ambas copias de una partición a la vez.
- Terminación con drenado elegante. Una ventana de gracia de terminación deja que un nodo saliente haga su entrega limpiamente en lugar de ser derribado en pleno vuelo.
- Escalado hacia afuera en caliente. Con el descubrimiento de Kubernetes activado (el default), el roster se sintetiza a partir de los ordinales de pod y el DNS headless, de modo que añadir pods es un simple
kubectl scale statefulset— los nuevos pods se unen y el mapa de particiones crece sin reiniciar los existentes. Añádelos de un múltiplo de RF a la vez y cada nueva partición es durable desde su nacimiento.
Nada de esto ayuda a la partición de una sola copia, sin embargo. Un PDB que mantiene todos-menos-uno de los pods arriba no es protección cuando uno de esos pods es el único hogar de una partición. Las barandillas asumen que dimensionaste el clúster correctamente; no rescatan a un clúster que no lo estaba.
Reglas prácticas
Todo el tema se comprime en unos pocos hábitos:
- Elige RF primero, luego haz que el número de pods sea múltiplo de él. RF 2 → números pares de pods; RF 3 → múltiplos de tres. El chart por defecto entrega 4 pods a RF 2 precisamente porque es la forma tolerante a fallos más pequeña.
- Trata un número impar de pods como solo-evaluación. Tres pods a RF 2 está bien para una demo en
kind; no es una postura de durabilidad de producción, y el chart te lo dice en el momento de la instalación. - Escala por RF. Añadir un pod a un clúster durable crea temporalmente una partición de una sola copia hasta que llega el siguiente pod. Si creces por durabilidad, crece un conjunto de replicación completo a la vez.
- Recuerda que el volumen es emptyDir. No hay disco al que recurrir. La replicación es tu durabilidad, que es exactamente por qué la copia tiene que existir realmente en otro pod.
El factor de replicación te dice cuántas copias quiere una partición. El número de pods decide si puede tenerlas. Alinea esos dos números y la pérdida de un pod es un no-evento — la réplica sirve, el pod reprogramado reconstruye, y no se pierde ni una clave. Deja que se desalineen y has construido un clúster que es de alta disponibilidad justo hasta el momento en que importa.