Jepsen : NATS 2.12.1
(jepsen.io)- Jepsen a vérifié la durabilité et la cohérence du système de messagerie distribuée NATS JetStream dans divers environnements de défaillance
- Les résultats des tests montrent qu’en cas de corruption de fichiers (.blk, snapshot) et de simulation de panne d’alimentation, on observe des pertes de données et des épisodes de split-brain
- JetStream exécute
fsynctoutes les 2 minutes par défaut, si bien que des messages récemment acquittés peuvent rester non écrits sur disque - Une seule panne de système d’exploitation peut déjà provoquer des pertes de données et une incohérence de réplication
- Jepsen recommande soit de changer la valeur par défaut de
fsyncenalways, soit de documenter explicitement le risque de perte de données
1. Contexte
- NATS est un système de streaming populaire permettant de publier et de s’abonner à des messages sous forme de flux
- JetStream utilise l’algorithme de consensus Raft pour répliquer les données et garantit une livraison au moins une fois (at-least-once)
- La documentation de JetStream affirme offrir une cohérence Linearizable et une disponibilité constante, mais selon le théorème CAP, ces deux propriétés ne peuvent pas être assurées simultanément
- Selon la documentation NATS, un flux à 3 nœuds peut tolérer la perte d’1 serveur, et un flux à 5 nœuds peut tolérer celle de 2 serveurs
- Un message est considéré comme « stocké avec succès » dès que le serveur a acquitté (
acknowledge) la requêtepublish - La cohérence des données requiert un quorum (majorité) de nœuds, et dans un cluster à 5 nœuds il faut au minimum 3 nœuds opérationnels pour pouvoir stocker de nouveaux messages
2. Conception des tests
- Jepsen a exécuté les tests avec le client JNATS 2.24.0 dans des conteneurs Debian 12 LXC
- Certains tests utilisent l’image officielle NATS Docker dans un environnement Antithesis
- Un flux JetStream unique (réplication 5) a été configuré, puis des arrêts forcés de processus, plantages, partitions réseau, pertes de paquets et corruptions de fichiers ont été injectés
- Une simulation de panne d’alimentation a été réalisée avec le système de fichiers LazyFS pour provoquer la perte d’écritures non validées par
fsync - Chaque processus publie un message unique, puis vérifie la présence des messages acquittés sur tous les nœuds à la fin du test
- Si un message n’existe que sur certains nœuds, il est classé comme divergence (incohérence de réplication)
3. Principaux résultats
3.1 Perte totale de données sur NATS 2.10.22 (#6888)
- Une perte totale du flux JetStream a été détectée avec un simple crash de processus
- L’erreur
"No matching streams for subject"apparaît, puis la situation ne se résout pas pendant plusieurs heures - La cause est liée à une inversion de snapshot du leader et à la suppression de l’état Raft, et le problème a été corrigé dans la version 2.10.23
3.2 Pertes de données en cas de corruption de fichiers .blk (#7549)
- Lorsqu’un fichier
.blkde JetStream subit une erreur de bit unique ou une troncature, des centaines de milliers d’écritures acquittées peuvent être perdues- Exemple : 679 153 pertes sur 1 367 069 messages
- Même si seuls certains nœuds sont corrompus, des pertes massives de données et un split-brain peuvent se produire
- Exemple : jusqu’à 78 % des messages perdus sur les nœuds
n1,n3etn5
- Exemple : jusqu’à 78 % des messages perdus sur les nœuds
- NATS enquête actuellement sur ce problème
3.3 Suppression totale des données en cas de corruption de snapshot (#7556)
- Si le fichier de snapshot sous
data/jetstream/$SYS/_js_/est corrompu, le nœud considère le flux comme orphaned et supprime toutes les données - La corruption d’une minorité de nœuds empêche d’atteindre le quorum et rend le flux permanemment indisponible
- Exemple : corruption des nœuds
n3etn5→ élection den3comme leader et suppression complète dejepsen-stream - Jepsen souligne le risque qu’un nœud corrompu puisse être élu leader
3.4 Perte de données due au réglage fsync par défaut (#7564)
- Par défaut, JetStream n’effectue un
fsyncque toutes les 2 minutes, alors que les messages sont acquittés immédiatement- En conséquence, les messages récemment acquittés peuvent ne pas avoir été persistés sur disque
- Une panne d’alimentation ou un crash kernel peut entraîner la perte de plusieurs dizaines de secondes de messages acquittés
- Exemple : 131 418 messages perdus sur 930 005
- Des pannes de nœud enchaînées peuvent aussi provoquer la suppression complète du flux
- Ce comportement est presque absent de la documentation
- Jepsen recommande de modifier la valeur par défaut en
fsync=alwaysou d’ajouter un avertissement explicite sur le risque de perte de données
3.5 Split-brain après un seul crash OS (#7567)
- Une seule panne d’alimentation ou crash kernel d’un nœud peut provoquer des pertes de données et une incohérence de réplication
- Dans une architecture leader-suiveur, si certains nœuds ont confirmé une écriture seulement en mémoire puis sont tombés en panne,
la majorité des nœuds perd cette écriture et poursuit avec un nouvel état - Les tests ont observé un split-brain persistant après un seul événement de panne d’alimentation
- Des pertes de messages acquittés sur des plages différentes ont été constatées selon le nœud
- Jepsen cite des cas similaires chez Kafka pour souligner que ce risque existe aussi dans des systèmes basés sur Raft
4. Discussion et conclusion
- Le problème de perte totale de données dans la version 2.10.22 est résolu en 2.10.23
- En 2.12.1, des pertes de données et des split-brain subsistent en cas de corruption de fichiers et de crash OS
- Une corruption des fichiers
.blkou snapshot peut provoquer des omissions de messages sur certains nœuds, ou la suppression complète du flux - L’intervalle de
fsyncpar défaut, s’il reste long, crée un risque de perte de données acquittées en cas de pannes simultanées sur plusieurs nœuds - Jepsen propose
fsync=alwaysou une mise en garde explicite dans la documentation - L’affirmation de JetStream selon laquelle il est « toujours disponible » est impossible selon CAP, ce qui implique une mise à jour de la documentation
- Jepsen précise qu’il est possible de démontrer l’existence de bugs, mais pas d’impossibilité de sûreté
4.1 Rôle de LazyFS
- Utilisation de LazyFS pour simuler la perte d’écritures non validées par
fsync - Diverses erreurs de stockage, dont les écritures partiellement corrompues (torn write), sont reproduites lors d’un crash d’alimentation
- Selon les travaux associés When Amnesia Strikes (VLDB 2024), des bugs similaires ont été signalés sur PostgreSQL, Redis, ZooKeeper, etc.
4.2 Pistes futures
- Les pertes au niveau d’un seul consommateur, l’ordre des messages ainsi que les garanties Linearizable/Serializable n’ont pas encore été vérifiés
- La garantie exactly-once est aussi identifiée comme sujet de recherche futur
- Des erreurs de documentation et l’absence d’étapes de health check obligatoires lors de l’ajout/retrait de nœuds ont été trouvées (#7545)
- Une procédure sûre de reconfiguration de cluster reste encore floue
1 commentaires
Avis sur Hacker News
Je me demande maintenant si une IA pourrait lire la documentation d’un projet et prédire la possibilité de perte de données rien qu’à partir du discours marketing
Les gens disent toujours que « la théorie est surestimée » ou que « hacker vaut mieux que les études », mais au final ils se tirent eux-mêmes une balle dans le pied dans un espace de problèmes déjà documenté
Il gérait aussi très bien les détails subtils de montée en charge
Mais je n’ai jamais utilisé la persistance, et je ne pensais pas que ce serait à ce point fragile
C’est déconcertant que ce soit vulnérable à une corruption d’un simple bit dans un fichier
C’est une très bonne ressource de référence → Jepsen Glossary
J’ai découvert aphyr.com récemment, et j’attends beaucoup des analyses qu’on y trouve
Ensuite, jepsen.io est devenu un projet professionnel, exploité sérieusement depuis environ dix ans
Est-ce pour gonfler les performances dans les benchmarks ? Dans les petits clusters, ce type de réglage est souvent la cause du problème
Beaucoup d’applications n’exigent pas une durabilité totale, donc un lazy fsync peut être utile
En revanche, en faire la valeur par défaut est discutable
On dirait qu’un traitement par lots (batch), comme avec TCP corking, pourrait suffire
Les pannes dues à lazy fsync ne surviennent généralement pas sur la majorité des nœuds en même temps
Avantage : prise en charge de flux illimités avec une durabilité au niveau du stockage objet
Inconvénient : il n’y a pas encore de consumer group
Si plusieurs nœuds tombent en panne en même temps, cela peut entraîner une perte de données validées
Ça rappelle le marketing « web scale » des débuts de MongoDB
À mon avis, les valeurs par défaut devraient toujours être l’option la plus sûre
C’est justement ce que j’appréciais, car cela permettait de concevoir le système au-dessus en connaissance de cause
Quand je l’ai utilisé en 2018, c’était performant et facile à administrer
Par exemple, le niveau d’isolation transactionnelle par défaut de PostgreSQL est read committed
Redis aussi fait un fsync par défaut toutes les secondes
Même avec Redis standalone, on peut configurer un ack après fsync, mais à cause du buffering de l’OS, une garantie totale reste difficile
Au final, l’important est de bien comprendre ce que signifie un ack
Si on impose uniquement des valeurs par défaut sûres, les performances chutent fortement et cela augmente la charge de réglage côté utilisateur
Par exemple, même le niveau d’isolation par défaut de Postgres est faible et peut provoquer des race conditions
Référence : article sur le test Hermitage
À l’ère des SSD, les étapes intermédiaires comme le group-commit ont disparu, et désormais le coût du passage en syscall constitue le goulet d’étranglement
Deux minutes, c’est beaucoup trop long comme intervalle (il faut aussi prendre en compte la différence entre fdatasync et fsync)
Autant utiliser Redpanda
Je me dis qu’un batch flush à intervalle régulier pourrait augmenter la latence tout en maintenant le débit
C’est similaire à la façon de regrouper des tours de Paxos
Il faudrait démarrer le lot suivant immédiatement après la fin d’un tour