- Jepsen verificó la durabilidad y la consistencia del sistema de mensajería distribuida NATS JetStream en diversos escenarios de fallos
- En los resultados de las pruebas se detectó pérdida de datos y split-brain durante la corrupción de archivos (.blk, snapshot) y la simulación de cortes de energía
- JetStream ejecuta
fsync de forma predeterminada cada 2 minutos, por lo que algunos mensajes acknowledged recientemente pueden quedar sin registrar en disco
- Un crash de OS en un solo nodo puede disparar pérdida de datos y divergencias de réplicas
- Jepsen recomienda que NATS cambie el valor predeterminado a
fsync=always o documente explícitamente el riesgo de pérdida de datos
1. Contexto
- NATS es un sistema de streaming popular para publicar y suscribirse a mensajes en forma de stream
- JetStream replica datos usando un algoritmo de consenso Raft y garantiza entrega al menos una vez (at-least-once)
- JetStream afirma en su documentación brindar consistencia linearizable y disponibilidad siempre, pero por el teorema CAP no puede cumplir ambos simultáneamente
- Según la documentación de NATS, un stream de 3 nodos tolera la pérdida de 1 servidor y uno de 5 nodos tolera la pérdida de 2
- Un mensaje se considera “almacenado correctamente” cuando el servidor responde con acknowledge a una solicitud de
publish
- Para la consistencia de datos se necesita una mayoría (quorum) de nodos: en un clúster de 5 nodos, al menos 3 deben estar activos para guardar un nuevo mensaje
2. Diseño de pruebas
- Jepsen ejecutó las pruebas con el cliente JNATS 2.24.0 en un entorno de contenedores Debian 12 LXC
- Algunas pruebas usaron la imagen oficial de Docker de NATS en el entorno de Antithesis
- Se configuró un único stream de JetStream (réplica 5) e inyectando parada de proceso, crashes, particiones de red, pérdida de paquetes y corrupción de archivos
- Se realizó una simulación de caída de energía con el sistema de archivos LazyFS para perder escrituras no
fsynceadas
- Cada proceso publicó mensajes únicos, y tras finalizar la prueba se verificó la existencia de mensajes acknowledged en todos los nodos
- Cuando un mensaje solo existe en algunos nodos, se clasificó como divergence (inconsistencia de replicación)
3. Resultados principales
3.1 Pérdida total de datos en NATS 2.10.22 (#6888)
- Se detectó que un crash de proceso simple podía causar la desaparición de todo el stream de JetStream
- Tras el error
"No matching streams for subject", no se recuperó durante varias horas
- La causa fue inversión de snapshot de líder, borrado de estado de Raft, entre otros, y se corrigió en la versión 2.10.23
3.2 Pérdida de datos por corrupción de archivo .blk (#7549)
- Cuando hay un error de un solo bit o truncation en el archivo
.blk de JetStream, se pierden cientos de miles de escrituras acknowledged
- Ejemplo: 679,153 de 1,367,069 registros perdidos
- Incluso si solo algunos nodos se corrompen, ocurre pérdida masiva de datos y split-brain
- Ejemplo: pérdida de hasta el 78% de los mensajes en
n1, n3 y n5
- NATS aún está investigando este problema
3.3 Eliminación total de datos por corrupción de archivo snapshot (#7556)
- Si un archivo snapshot en
data/jetstream/$SYS/_js_/ se corrompe, el nodo marca el stream como huérfano (orphaned) y borra todos los datos
- Aun si solo se afecta a unos pocos nodos, puede haber falta de mayoría del clúster y
jepsen-stream queda permanentemente no disponible
- Ejemplo: al corromperse
n3 y n5, n3 fue elegido líder y se borró todo jepsen-stream
- Jepsen señala el riesgo de que un nodo corrupto sea electo como líder
3.4 Pérdida de datos por configuración predeterminada de fsync (#7564)
- JetStream hace
fsync de forma predeterminada solo cada 2 minutos, mientras los mensajes se reconocen de inmediato
- Como resultado, los mensajes recién acknowledged pueden quedar sin persistir en disco
- En cortes de energía o crashes de kernel se perdieron decenas de segundos de mensajes acknowledged
- Ejemplo: 131,418 de 930,005 mensajes perdidos
- La eliminación completa del stream es posible incluso con fallos consecutivos de un solo nodo
- Esta conducta casi no se menciona en la documentación
- Jepsen recomienda cambiar el valor predeterminado de NATS a
fsync=always o incorporar una advertencia explícita sobre el riesgo de pérdida de datos
3.5 Split-brain por crash de OS en un solo nodo (#7567)
- La pérdida de datos y la inconsistencia de replicación pueden ocurrir por un corte de energía o crash de kernel de un único nodo
- En la arquitectura líder-seguidor, si algunos nodos ackean una escritura solo comprometida en memoria y luego fallan,
la mayoría de los nodos pierde esa escritura y avanza con un estado nuevo
- En las pruebas, tras un único corte de energía se observó split-brain persistente
- Se verificó pérdida de mensajes acknowledged en rangos distintos según el nodo
- Jepsen cita un caso similar de Kafka y enfatiza que el mismo riesgo también existe en sistemas basados en Raft
4. Discusión y conclusión
- El problema de pérdida total de datos en 2.10.22 fue resuelto en 2.10.23
- En 2.12.1, la pérdida de datos y split-brain por corrupción de archivos y crash de OS todavía ocurren
- Con corrupción de archivos
.blk y snapshot, pueden ocurrir mensajes faltantes en algunos nodos o eliminación total del stream
- Un periodo de
fsync predeterminado largo implica riesgo de pérdida de datos acknowledged si fallan simultáneamente varios nodos
- Jepsen propone
fsync=always o una advertencia clara de riesgo en la documentación
- La promesa de “disponibilidad siempre” de JetStream es imposible por CAP, y requiere ajustes en la documentación
- Jepsen especifica que se puede demostrar la existencia de bugs, pero no se puede probar la ausencia de inseguridad
4.1 Rol de LazyFS
- Se simula la pérdida de escrituras sin
fsync usando LazyFS
- En una caída de energía permiten reproducir diversos errores de almacenamiento, como escrituras parciales (torn write)
- El trabajo relacionado When Amnesia Strikes (VLDB 2024) reporta bugs similares en PostgreSQL, Redis, ZooKeeper, entre otros
4.2 Próximos pasos
- Aún no se verificó la pérdida de mensajes por consumidor único, el orden de mensajes ni la garantía Linearizable/Serializable
- La garantía exactly-once también queda como un tema para trabajo futuro
- Se detectó un error documental y omisión de steps obligatorios de health check al añadir/eliminar nodos (#7545)
- Los procedimientos seguros de reconfiguración de clúster siguen siendo ambiguos
1 comentarios
Opiniones de Hacker News
Ahora hasta me pregunto si una IA podría leer la documentación de un proyecto y predecir la posibilidad de pérdida de datos solo por el lenguaje de marketing
La gente siempre dice que “la teoría está sobrevalorada” o que “hackear es mejor que la educación formal”, pero al final terminan tropezando solos en un espacio de problemas ya documentado
También resolvía bien los detalles sutiles de escalado
Pero nunca usé la persistencia, y no imaginaba que fuera tan frágil
Sorprende que sea vulnerable incluso a la corrupción de un solo bit en un archivo
Es una referencia excelente → Jepsen Glossary
Descubrí aphyr.com hace poco y tengo expectativas de encontrar muchas ideas valiosas
Después jepsen.io evolucionó hasta convertirse en un proyecto profesional, y empezó a operar en serio hace unos 10 años
¿Es para inflar el rendimiento en benchmarks? En clústeres pequeños, este tipo de configuración suele ser la causa del problema
Muchas aplicaciones no necesitan durabilidad total, así que lazy fsync puede ser útil
Aun así, dejarlo como valor predeterminado es discutible
Parecería que se podría resolver con procesamiento por lotes (batch), como TCP corking
Los fallos provocados por lazy fsync casi nunca ocurren al mismo tiempo en la mayoría de los nodos
Ventaja: soporta streams ilimitados con durabilidad al nivel de object storage
Desventaja: todavía no tiene consumer groups
Si varios nodos fallan al mismo tiempo, puede haber pérdida de datos ya comprometidos
Me recuerda al marketing de “web scale” de los primeros tiempos de MongoDB
Creo que el valor predeterminado siempre debería ser la opción más segura
Justamente eso me gustaba, porque permitía diseñar sistemas encima con esa premisa
Cuando lo usé en 2018, también era rápido y fácil de administrar
Por ejemplo, el nivel de aislamiento predeterminado de PostgreSQL es read committed
Redis también hace fsync por defecto cada 1 segundo
Incluso en Redis standalone se puede configurar ack después de fsync, pero por el buffering del SO sigue siendo difícil garantizarlo por completo
Al final, lo importante es entender con precisión qué significa un ack
Si uno insiste solo en valores predeterminados seguros, el rendimiento cae mucho y además se le deja al usuario la carga de afinar todo manualmente
Por ejemplo, el nivel de aislamiento predeterminado de Postgres también es débil y puede producir race conditions
Referencia: artículo sobre pruebas Hermitage
En la era de los SSD desaparecieron puntos intermedios como group commit, y ahora el cuello de botella es el costo del cambio de syscall
2 minutos es un intervalo demasiado largo (también hay que considerar la diferencia entre fdatasync y fsync)
Mejor usar Redpanda
Si se hace un batch flush periódico, la latencia subiría, pero tal vez se mantendría el throughput
Esto es similar a agrupar rondas de Paxos
Cuando termina una ronda, hay que arrancar de inmediato el siguiente lote