- rqlite est une base de données relationnelle distribuée open source légère, écrite en Go et développée sur la base de SQLite et de Raft
- Son développement a commencé en 2014, avec la priorité donnée à la fiabilité et à la qualité ; après plus de 10 ans de développement et de déploiement, moins de 10 cas de panic seulement ont été signalés en production
- Les tests des systèmes distribués exigent une attention minutieuse à plusieurs couches, selon une philosophie qui vise à préserver la qualité dans la simplicité
La pyramide des tests : une approche efficace
- Les tests de rqlite respectent la « pyramide des tests »
- Pyramide des tests : une structure fondée sur les tests unitaires, incluant des tests d’intégration et un minimum de tests end-to-end (E2E)
- Elle fournit une suite de tests efficace, facile à déboguer et orientée vers des objectifs précis
Tests unitaires : le cœur de la qualité
- Les tests unitaires vérifient des composants indépendants et offrent un équilibre entre rapidité et précision
- Grâce à SQLite et à une architecture « shared nothing », la plupart des fonctionnalités peuvent être couvertes par des tests unitaires
- Sur l’ensemble du code de rqlite (environ 75 000 lignes), les tests unitaires représentent environ 27 000 lignes
- Les tests s’exécutent en quelques minutes, ce qui permet de les lancer fréquemment pendant le développement
Tests au niveau système : validation du consensus
- Les tests au niveau système valident l’interaction entre le module de consensus Raft et SQLite
- Principaux éléments testés :
- Réplication des instructions SQLite entre les nœuds
- Opérations de lecture à différents niveaux de cohérence
- Validation de la reprise après panne du cluster et de l’élection du leader
- Environ 7 000 lignes de code de test couvrent de manière approfondie les interactions dans des configurations à nœud unique et à plusieurs nœuds
Tests end-to-end : une couche minimale
- Les tests end-to-end jouent le rôle de smoke tests en vérifiant le démarrage du système, la mise en cluster et le fonctionnement de base
- Ils sont écrits en Python et valident les fonctions principales en exécutant un véritable cluster rqlite
- Exemple : vérification des sauvegardes vers AWS S3
- Le code de test est limité à environ 5 000 lignes, selon une approche volontairement restreinte pour minimiser les coûts de débogage
Tests de performance : éprouver les limites
- Les tests de performance évaluent des métriques telles que :
- Vitesse maximale des INSERT
- Traitement des requêtes concurrentes
- Comparaison de l’utilisation de la mémoire, du CPU et du disque
- Ils incluent des tests de bases SQLite de plus de 2 Go afin d’analyser la gestion mémoire et les goulots d’étranglement liés aux écritures disque
- Ils permettent de détecter les problèmes de performance et de garantir la stabilité grâce aux optimisations
Leçons retenues
- Commencer les tests dès le départ
- Les tests unitaires sont le moyen le plus efficace d’instaurer la confiance dans le système
- Il ne faut pas remettre à plus tard l’écriture des tests unitaires pendant le développement, car ils permettent de détecter les bugs plus vite que les tests d’intégration ou les tests E2E
- Garder le code de test simple
- Une suite de tests n’est pas l’endroit où s’obstiner à faire de gros refactorings ou à appliquer à tout prix le principe DRY(Don't Repeat Yourself)
- Il est important d’écrire un code facile à comprendre, et d’accepter aussi du code boilerplate supplémentaire
- Valider les tests
- Lors de l’écriture d’un test, réexécuter temporairement le test en inversant le résultat attendu
- Un test correctement écrit doit alors échouer, ce qui permet d’éviter à l’avance les erreurs dans le code de test
- Ne pas ignorer les échecs de test
- Même les échecs de test rares ou difficiles à comprendre apportent des informations importantes sur le logiciel
- Les cas d’échec difficiles à déboguer peuvent souvent devenir une occasion de découvrir des défauts critiques dans le code
- Maximiser le déterminisme
- Mettre en place des mécanismes permettant d’exécuter manuellement les processus automatiques du système
- Exemple : la fonction de snapshot de Raft s’exécute généralement de manière semi-automatique, mais a été conçue pour pouvoir être déclenchée explicitement pendant les tests
- Agir de manière délibérée (Be Deliberate)
- N’ajouter des tests d’intégration de niveau supérieur ou des tests E2E que lorsque leur nécessité est clairement démontrée
- Des tests excessifs peuvent ralentir le développement et le débogage
- Appliquer et itérer
- Les appels à
fsync ont été identifiés comme un goulot d’étranglement majeur lors des tests de performance, et l’utilisation du disque a été optimisée en compressant les entrées du journal Raft avant leur écriture sur le disque
- Mettre l’accent sur l’efficacité
- Conserver une suite de tests capable de s’exécuter en quelques minutes permet un développement itératif rapide
- C’est un avantage essentiel pour maintenir et faire vivre un projet open source
La qualité avant tout
- La pyramide des tests est respectée, et chaque couche de test est conçue avec un objectif clair
- À mesure que la complexité des systèmes distribués augmente, préserver la simplicité des tests devient essentiel
- L’objectif est de construire une base de données fiable et facile à exploiter
1 commentaires
Commentaires sur Hacker News
Le premier test est le plus difficile, mais il vaut la peine d’être ajouté. Les tests suivants deviennent plus faciles.
L’engagement envers le projet est impressionnant.
La pyramide des tests est compréhensible, mais il arrive souvent qu’il manque certains niveaux, ce qui impose de corriger cela rapidement.
Il semble y avoir une erreur de copier-coller dans la FAQ.
Le rapport Jepsen est attendu avec impatience.
Le format vidéo a aussi été apprécié.
La configuration des tests de performance fait envie.
Quelqu’un a utilisé rqlite et trouve qu’il transmet bien sa simplicité.
Quelqu’un demande des avis sur les tests de simulation déterministe.
Quelqu’un se demande si rqlite est utilisé en production.
rqlite est un projet original et génial.