Des articles approfondis sur les technologies qui façonnent l'avenir.

SQLite distribué : 5 mythes testés après 12 ans

Après douze ans de SQLite distribué (Raft, Litestream, Turso, D1), on teste cinq idées reçues sur la cohérence, la réplication et le choix de Postgres.

Un anneau de petits classeurs reliés par des fils de lumière, l'un d'eux brillant comme leader
Douze ans de SQLite distribué : de nombreuses conceptions, une même idée centrale sans cesse réinventée.

Le SQLite distribué est passé du projet du week-end d'une seule personne à une catégorie de produits financée par du capital-risque, sans que la plupart des ingénieurs ne remarquent la décennie intermédiaire. En septembre 2014, Philip O'Toole a placé un journal Raft devant SQLite, répliqué chaque écriture sur un cluster et publié le code sur GitHub sous le nom de rqlite. Douze ans plus tard, un écosystème complet existe (dqlite, Litestream, LiteFS, cr-sqlite, libSQL/Turso, Cloudflare D1), et la plupart des croyances à son sujet sont fausses, et ce de manière intéressante.

J'ai fait tourner des bases de données en cluster en production et j'ai nettoyé les dégâts de leurs modes de défaillance, alors laissez-moi vous épargner quelques douleurs : les débats sur le SQLite distribué ne portent presque jamais sur SQLite. Ils portent sur la question qu'on se pose vraiment : que se passe-t-il si la machine tombe, que se passe-t-il si la machine tombe et que je ne peux pas attendre, ou que se passe-t-il s'il n'y a pas de machine. Voici cinq mythes, confrontés à douze ans d'histoire.

Mythe 1 : le SQLite distribué est une idée récente

La vague actuelle d'articles sur le SQLite répliqué commence vers 2021, quand Litestream et le mouvement « SQLite is enough » ont pris de l'ampleur, et que Turso et Cloudflare D1 sont arrivés avec des financements et des annonces de lancement. La croyance s'est donc installée selon laquelle quelqu'un de malin venait récemment de remarquer qu'on pouvait répliquer SQLite. En réalité, le premier système Raft-sur-SQLite est sorti en 2014, et ce n'était même pas la première réplication de SQLite : Expensify le faisait en interne depuis des années avec un protocole de type Paxos avant de publier Bedrock en 2016, et ActorDB donnait à chaque acteur Erlang sa propre base SQLite répliquée via Raft à peu près à la même époque.

Pourquoi cela compte-t-il au-delà du mérite ? Parce qu'une conception qui survit douze ans de réinvention indépendante vous dit quelque chose. Au moins huit équipes différentes, avec des besoins et des protocoles différents, ont examiné le problème et sont arrivées à la même architecture : SQLite comme moteur de stockage, la réplication ajoutée à l'extérieur. Personne n'a trouvé de raison d'arrêter. Quand on observe ce schéma, une réimplémentation convergente pendant une décennie, on a affaire à une bonne idée, pas à un cycle de hype. Comparez avec les catégories qui s'enflamment puis disparaissent : celle-ci n'a cessé de s'accumuler.

Cela compte aussi parce que les anciens choix de conception portent toujours la charge. rqlite a choisi d'être un système CP en 2014 (les écritures passent par le leader, une majorité doit acquitter, une minorité isolée refuse les écritures) et n'a jamais changé d'avis. Ce seul choix a dicté l'API HTTP, les niveaux de cohérence en lecture et le comportement des snapshots. Les équipes qui traitent la cohérence comme un paramètre à trancher plus tard le regrettent généralement ; l'écart entre la spécification implicite et celle effectivement implémentée est exactement là où les bugs distribués prospèrent.

Mythe 2 : Raft sur SQLite donne une Postgres distribuée bon marché

C'est le mythe le plus coûteux de la liste. L'argumentaire est séduisant : prendre la base de données la plus déployée au monde, y ajouter du consensus, et s'épargner le poids opérationnel d'un système client-serveur. Ce qu'on obtient en réalité, c'est une base à écrivain unique où chaque écriture paie un aller-retour réseau vers une majorité de nœuds, et où la surface SQL de SQLite (pas de procédures stockées, un système de types réduit, des primitives de concurrence limitées) reste la surface SQL de SQLite.

Le chemin d'écriture est le coût honnête. Un commit SQLite local, ce sont quelques fsync. Un commit répliqué par Raft, c'est : le leader ajoute l'entrée à son journal, l'envoie aux followers, attend qu'une majorité la persiste, l'applique à SQLite, puis répond. Sur un LAN sain, cela représente une ou deux millisecondes au lieu de quelques dizaines de microsecondes. Entre régions, c'est la vitesse de la lumière plus le disque. Pour des charges à forte lecture et des taux d'écriture modestes (magasins de configuration, métadonnées de découverte de services, petits plans de contrôle), c'est acceptable, et c'est exactement la niche pour laquelle rqlite a été conçu (son histoire d'origine : un système en cluster qui avait besoin d'un peu de stockage relationnel, sans bonne réponse quant au nœud qui devait l'héberger). Pour une charge OLTP intensive, c'est le mauvais outil.

Et puis il y a la réalité opérationnelle que personne ne met dans le README : l'élection du leader est un mode de défaillance dont vous êtes désormais responsable. Le split-brain est bien empêché par le vote majoritaire, oui, mais cela signifie qu'un cluster à trois nœuds qui perd deux nœuds refuse toutes les écritures. Vous avez troqué « la base est down parce que le serveur est mort » contre « la base est down parce que le quorum est mort », et la seconde situation est plus difficile à expliquer à votre astreinte à 3 h du matin. Si vous n'avez pas réfléchi à des tests de type Jepsen sur le système choisi, vous devriez : un commentateur de Hacker News a posé la question pointue de savoir si rqlite avait été testé avec Jepsen, car les systèmes qui ne l'ont pas été réservent généralement des surprises. Par ailleurs, un ingénieur expérimenté de la même discussion faisait remarquer qu'avec les outils actuels, faire tourner un vrai Postgres est devenu assez simple pour que « utilisez simplement un serveur de base de données » redevienne une valeur par défaut légitime. Il n'a pas tort.

Une balance qui pèse un simple bloc contre un enchevêtrement de chaînes et d'engrenages
Le consensus vous offre le basculement ; le prix se paie à chaque écriture.

Mythe 3 : répliquer, c'est forcément du consensus

Si l'histoire de douze ans apporte une seule idée, c'est celle-ci : le « SQLite répliqué » n'est pas une conception unique, c'est trois coins d'un espace de conception, et ils sont apparus dans un ordre révélateur. Les confondre, c'est ce qui pousse les équipes à faire tourner un cluster Raft pour résoudre un problème de sauvegarde.

  • Consensus (CP). rqlite, dqlite. Un leader, une cohérence forte, un vrai cluster. Répond à : « que se passe-t-il si la machine tombe et que je ne peux pas attendre la restauration ? » Coût : latence d'écriture et opérations de quorum.
  • Streaming (écrivain unique). Litestream, puis LiteFS. Aucun consensus : le WAL de SQLite est streamé vers un stockage objet, ce qui offre une reprise après sinistre quasi continue pour un seul nœud. Répond à : « que se passe-t-il si la machine tombe ? » Coût : une petite fenêtre de perte de données possible et un temps de restauration de quelques secondes à quelques minutes.
  • Fusion (AP). cr-sqlite et le mouvement local-first. Des CRDT sous forme d'extension SQLite, plusieurs écrivains qui divergent et fusionnent sans leader. Répond à : « que se passe-t-il s'il n'y a aucune machine centrale ? » Coût : une cohérence éventuelle et une sémantique de fusion à concevoir soi-même.

L'argumentaire de Ben Johnson pour Litestream mérite le respect, car il avait raison pour la plupart des applications : elles tournent sur une seule machine et ont besoin d'un moyen de récupérer quand elle tombe, pas d'un cluster. La première question, « que se passe-t-il si la machine tombe », est bien plus fréquente que la seconde, « et je ne peux pas attendre la restauration ». J'ai vu des équipes déployer un cluster Raft à trois nœuds pour protéger une base dont l'exigence de RTO était « avant l'heure du déjeuner ». Ce n'est pas de l'ingénierie de la fiabilité, c'est du cargo cult. Et pour boucler la boucle : l'approche de streaming du WAL de Litestream a fini par inspirer la façon dont rqlite gère lui-même le WAL en v8. Le coin streaming a nourri le coin consensus.

Alors, avant de choisir un outil, écrivez la question. Littéralement. « Quand le nœud X tombe, nous devons reprendre les écritures en N secondes, et nous acceptons de perdre les M dernières secondes de données validées. » Si M est de quelques secondes et N d'une minute, Litestream vers S3 est votre réponse, et il ne vous réveillera jamais pour un problème de quorum. Si M vaut zéro et N quelques secondes, vous êtes en territoire de consensus, et vous le paierez à chaque écriture. C'est la même discipline que pour tout modèle de menaces : nommer la défaillance avant d'acheter la défense, comme on aborde les systèmes qui résistent aux catastrophes en décidant d'emblée quelle catastrophe on veut résister.

Mythe 4 : le « SQLite en edge » managé supprime les compromis

L'argent de cette catégorie est allé aux plateformes : Turso a levé des fonds en capital-risque puis a été racheté par Supabase, et Cloudflare a livré D1 au sein de son réseau edge. L'argumentaire : quelqu'un d'autre gère désormais les parties distribuées difficiles. Parfois c'est vrai. Parfois c'est du marketing, et le secret qui gêne finit par fuiter : un développeur de Cloudflare a reconnu publiquement que D1 est essentiellement un wrapper autour d'un Durable Object singleton qui expose sa base SQLite, et qu'on a souvent intérêt à utiliser directement le stockage des Durable Objects. Ce n'est pas une base distribuée au sens du consensus ; c'est un fichier SQLite unique entouré d'une plateforme. Les utilisateurs de D1 en production rapportent une fiabilité à l'avenant.

Rien de tout cela ne signifie que les options managées sont mauvaises. Cela signifie que les compromis n'ont pas disparu : ils ont été déplacés derrière une API qu'on ne peut pas inspecter. Quand vous faites tourner rqlite vous-même, vous savez exactement ce que signifie CP : trois nœuds, un leader, des acquittements majoritaires, une minorité isolée qui refuse les écritures. Quand vous achetez du « SQLite en edge », il faut poser les mêmes questions à une boîte noire : où se trouve l'écrivain, que se passe-t-il en cas de partition, quelle est ma fenêtre de durabilité réelle ? Si la réponse du fournisseur est floue, votre revue d'incident ne le sera pas.

La réplication en tant que service ne supprime pas la question de la cohérence. Elle transfère simplement la réponse dans l'infrastructure de quelqu'un d'autre, où vous ne pouvez pas l'auditer.

Le schéma historique mérite d'être intégré : les systèmes open source (rqlite, dqlite, Litestream) ont porté l'idée pendant huit ans avant que quiconque ne la vende, et les capitaux ont afflué vers ceux qui en ont fait un service facturé. C'est le sort habituel de l'infrastructure. Acheter est souvent le bon choix, mais achetez en connaissance de cause sur lequel des trois coins vous obtenez réellement.

Une boule à neige avec une cabane chaleureuse à l'intérieur, posée dans un champ brumeux de baies de serveurs
SQLite en edge managé : une base douillette enveloppée dans une plateforme que vous ne voyez pas.

Mythe 5 : la cohérence forte est une mise à niveau gratuite par rapport aux sauvegardes

On présente souvent le choix comme sauvegardes (faibles, bon marché) contre consensus (fort, coûteux mais strictement supérieur). C'est faux. La cohérence forte via Raft est un produit différent, pas une meilleure version du même produit, et elle impose des taxes permanentes que les sauvegardes n'imposent jamais.

Première taxe : le débit. Un praticien qui a utilisé les deux a fait remarquer que rqlite offre des garanties du type « stocké à plus d'un endroit », mais qu'il est bien plus lent qu'une base à un seul nœud : c'est l'aller-retour du consensus, qui ne disparaît jamais. Deuxième taxe : une sémantique subtile. Si vous répliquez en envoyant des instructions SQL, vous héritez des problèmes de déterminisme : des fonctions comme random() ou datetime('now') peuvent diverger d'un nœud à l'autre, à moins que la couche ne les réécrive, ce qui explique que certaines implémentations ne supportent qu'un sous-ensemble du SQL de SQLite. Si vous répliquez au niveau du WAL, vous évitez cela, mais vous vous couplez au format sur disque et à la disposition des pages de SQLite. Il n'existe pas de placement de couche sans compromis. Troisième taxe : les transactions distribuées sont réellement difficiles. Un ingénieur a décrit l'évolution d'une conception où chaque instruction d'une transaction était une commande Raft distincte, à peu près correcte mais lente, vers une conception où le nœud initiateur exécute toute la transaction localement, la valide, puis envoie une seule commande de commit avec le lot entier. C'est le genre de leçon chèrement acquise qu'on n'apprend qu'en exploitant ces systèmes.

Voici une illustration minimale de la différence du chemin d'écriture, avec l'API HTTP de rqlite :

# Local SQLite: one process, one fsync, microseconds.
sqlite3 app.db "INSERT INTO kv VALUES ('flag', 'on')"
# rqlite: this HTTP call hides a Raft round trip —
# leader log append, replication to a majority, then apply.
curl -XPOST 'localhost:4001/db/execute?pretty' \
-H 'Content-Type: application/json' \
-d '["INSERT INTO kv VALUES (?, ?)", "flag", "on"]'
# The guarantee changed: the write now survives the loss
# of any single node. So did the latency budget.

Et remarquez ce que vous n'avez pas obtenu : la montée en charge en lecture. Raft réplique le journal pour que les followers puissent prendre le relais ; il ne fait pas en sorte que les followers servent par défaut des lectures fortement cohérentes (rqlite propose des niveaux de cohérence en lecture précisément parce que c'est un compromis). Si vous êtes venu pour le « distribué » en espérant plus de capacité de lecture, vous vouliez des réplicas en lecture, pas du consensus.

Ce que douze ans de Raft sur SQLite nous ont réellement appris

L'histoire de cette catégorie est un témoignage inhabituellement clair de la façon dont les idées d'infrastructure mûrissent. D'abord, quelqu'un construit la solution de principe pour un besoin concret (rqlite, né d'un vrai problème de clustering dans une vraie entreprise). Ensuite, d'autres la reconstruisent pour leurs contraintes de packaging : dqlite de Canonical est une bibliothèque qu'on embarque, parce que Canonical avait besoin d'une base de données à l'intérieur de ses produits, tandis que rqlite est un serveur qu'on fait tourner à côté de n'importe quel produit. Puis quelqu'un soutient que le cas courant est plus petit que ce que suppose la solution de principe (Litestream), se révèle avoir raison, et tout le domaine se recalibre. Enfin, les plateformes l'enveloppent et le vendent. Si vous travaillez assez longtemps dans l'infrastructure, vous reverrez ce film : les agents LLM réapprennent les leçons des systèmes distribués exactement de la même façon en ce moment.

Ma recommandation sans détour, après tout cela : commencez par du SQLite sur un seul nœud avec streaming du WAL vers un stockage objet. C'est étonnant de voir jusqu'où cela mène, et votre scénario de panne se résume à une restauration, pas à un post-mortem de consensus. Ne passez à un système basé sur Raft que lorsque vous pouvez énoncer en une phrase l'exigence de disponibilité que le streaming ne peut pas satisfaire, et acceptez la facture de latence d'écriture et d'opérations de quorum qui vient avec. Tournez-vous vers Postgres dès que vous avez besoin de sa surface SQL ou d'un débit multi-écrivains. Et si rien de tout cela ne convient, le monde plus large des bases de données embarquées offre aussi des options, des bases de données graphes embarquées jusqu'au coin CRDT local-first.

Le bilan des mythes : le SQLite distribué n'est pas nouveau, n'est pas une Postgres bon marché, n'est pas une conception unique, n'est pas rendu sûr par un wrapper managé et n'est pas une mise à niveau gratuite par rapport aux sauvegardes. C'est un espace de conception de douze ans, bien cartographié, avec trois coins, chacun juste pour une question différente. Les ingénieurs qui se brûlent sont ceux qui n'ont jamais écrit la question.