Je suis à fond pour SQLite côté serveur

Je m'appelle Ben Johnson. J'ai écrit BoltDB, une base de données embarquée qui est le moteur de systèmes comme etcd. Je travaille maintenant chez Fly.io, sur Litestream. Litestream est un projet open-source qui rend SQLite soutenable pour les applications full-stack grâce au pouvoir de la ✨réplication✨. Si vous savez configurer une base de données SQLite, vous pouvez faire fonctionner Litestream en moins de 10 minutes.

La sagesse conventionnelle des applications full-stack est l'architecture n-tier, qui est maintenant si commune qu'il est facile d'oublier qu'elle a même un nom. C'est ce que vous faites lorsque vous exécutez un "serveur d'application" comme Rails, Django ou Remix à côté d'un "serveur de base de données" comme Postgres. Selon les idées reçues, SQLite a une place dans cette architecture : celle d'exécuter des tests unitaires.

Les idées reçues auraient besoin d'une mise à jour. Je pense que pour de nombreuses applications - les applications en production, avec un grand nombre d'utilisateurs et des exigences de haute disponibilité - SQLite a une meilleure place, au centre de la stack, comme le cœur de votre couche de données et de persistance.

C'est beaucoup dire. Ce n'est peut-être pas le cas pour votre application. Mais vous devriez l'envisager, et je vais vous expliquer pourquoi.

Une brève histoire des bases de données applicatives

50 ans, ce n'est pas si long. Au cours de cette période, nous avons assisté à une quantité stupéfiante de changements dans la façon dont nos logiciels gèrent les données.

Au début de notre histoire, dans les années 70, il y avait les règles de Codd, définissant ce que nous appelons aujourd'hui les "bases de données relationnelles", également connues sous le nom de "bases de données". Vous les connaissez, même si vous ne les connaissez pas : toutes les données vivent dans des tables ; les tables ont des colonnes, et les lignes sont adressables avec des clés ; C.R.U.D. ; schémas ; un langage textuel pour transmettre ces concepts. Le langage, bien sûr, est SQL, ce qui a provoqué une explosion cambrienne de bases de données SQL, d'Oracle à DB2 en passant par Postgres et MySQL, tout au long des années 80 et 90.

Tout n'a pas été rose. Les années 2000 nous ont apporté les bases de données XML. Mais notre industrie s'est rachetée en construisant d'excellentes bases de données en colonnes à la même époque. Dans les années 2010, nous avons vu des dizaines de projets de bases de données distribuées à grande échelle et open-source arriver sur le marché. Aujourd'hui, n'importe qui peut créer un cluster et interroger des téraoctets de données.

Au fur et à mesure de l'évolution des bases de données, les stratégies que nous utilisons pour les intégrer à nos applications ont elles aussi évolué. Presque depuis Codd, nous avons divisé ces applications en niveaux. Tout d'abord, il y a eu le niveau de la base de données. Plus tard, avec memcached et Redis, nous avons eu le niveau de mise en cache. Nous avons des niveaux de travail en arrière plan, des niveaux de routage et des niveaux de distribution. Les tutoriels prétendent qu'il y a 3 niveaux, mais nous savons tous que c'est appelé "n-tier" parce que personne ne peut prédire avec combien de niveaux nous allons nous retrouver.

Vous voyez où je veux en venir. Nos scientifiques étaient tellement préoccupés par le fait de savoir s'ils pouvaient le faire, et ainsi de suite.

Au cours de ces cinq décennies, nous avons également vu les processeurs, la mémoire et les disques devenir des centaines de fois plus rapides et moins chers. Un terme qui définit pratiquement l'innovation en matière de bases de données dans les années 2010 est "big data". Mais les améliorations matérielles ont rendu ce concept glissant dans les années 2020. Gérer une base de données de 1 Go en 1996 ? Un gros problème. En 2022 ? Exécutez-la sur votre ordinateur portable, ou sur un t3.micro.

Lorsque nous réfléchissons à de nouvelles architectures de base de données, nous sommes hypnotisés par les limites d'échelle. Si une base de données ne peut pas gérer des pétaoctets, ou au moins des téraoctets, elle est disqualifiée d'office. Mais la plupart des applications ne verront jamais un téraoctet de données, même si elles ont du succès. Nous utilisons des marteaux-piqueurs pour enfoncer des clous.

La douce libération de SQLite

Il existe une base de données qui va à l'encontre de beaucoup de ces tendances. C'est l'une des bases SQL les plus populaires au monde, si standardisée qu'elle est un format d'archivage officiel de la Bibliothèque du Congrès américain, elle est réputée pour sa fiabilité et sa suite de tests d’une profondeur insondableet ses performances sont si bonnes que citer ses indicateurs sur un forum de discussion déclenche invariablement une discussion pour savoir si elle doit être écartée. Je n'ai probablement pas besoin de la nommer pour vous, mais, pour la personne du fond qui lève la main, je parle de SQLite.

SQLite est une base de données embarquée. Elle ne se trouve pas dans un niveau architectural classique ; c'est juste une bibliothèque, liée au processus de votre application serveur. C'est le symbole de l'"application à processus unique" : le serveur qui fonctionne en autonomie, sans dépendre de neuf autres serveurs secondaires pour fonctionner.

Je me suis intéressé à ce type d'applications parce que je crée des moteurs de base de données. J'ai écrit BoltDB, qui est un système de stockage clé/valeur embarqué populaire dans l'écosystème Go. BoltDB est fiable et, comme on peut s'y attendre de la part d'une base de données in-process, ses performances sont celles d'une voiture de sport à la nitro. Mais BoltDB a des limites : son schéma est défini en code Go, et c'est donc difficile de migrer les bases de données. Vous devez créer vos propres outils pour ça ; il n'y a même pas de REPL.

Si vous y faites attention, l'utilisation de ce type de base de données peut vous faire gagner beaucoup en performance. Mais pour une utilisation générale, vous ne voulez pas faire tourner votre base de données à vide comme une voiture de course. J'ai réfléchi au genre de boulot que j'aurais à faire pour rendre BoltDB viable pour plus d'applications, et la conclusion à laquelle j'ai rapidement abouti est la suivante : c'est à ça que sert SQLite.

SQLite, comme vous êtes sans doute déjà en train de le taper dans un commentaire, n'est pas sans limites. La plus importante d'entre elles est qu'une application à processus unique a un seul point de défaillance : si vous perdez le serveur, vous avez perdu la base de données. Il ne s'agit pas d'un défaut de SQLite, mais d'une caractéristique inhérente à sa conception.

Voici Litestream

Il y a deux grandes raisons pour lesquelles tout le monde n'utilise pas SQLite par défaut. La première est la résilience aux pannes de stockage, et la seconde est la concurrence des accès à l'échelle. Litestream a quelque chose à proposer pour ces deux problèmes.

Le fonctionnement de Litestream est le suivant : il prend le contrôle de la journalisation en mode WAL de SQLite. En mode WAL, les opérations d'écriture s'ajoutent à un fichier journal stocké à côté du fichier de base de données principal de SQLite. Les lecteurs vérifient à la fois le fichier WAL et la base de données principale pour satisfaire les requêtes. Normalement, SQLite reporte automatiquement les pages du WAL vers la base de données principale. Litestream intervient au milieu de tout cela : nous ouvrons une transaction de lecture infinie qui empêche les points de contrôle automatiques. Nous capturons ensuite nous-mêmes les mises à jour du WAL, les répliquons et déclenchons nous-mêmes le checkpointage.

La chose la plus importante que vous devez comprendre au sujet de Litestream est que c'est juste SQLite. Votre application utilise du SQLite classique, avec les bibliothèques SQLite classiques. Nous n'analysons pas vos requêtes, nous ne nous substituons pas à vos transactions et nous n'ajoutons pas de nouvelles dépendances aux bibliothèques. Nous tirons simplement parti des fonctionnalités de journalisation et de concurrence dont dispose déjà SQLite, dans un outil qui fonctionne en parallèle de votre application. Dans la plupart des cas, votre code peut ignorer l'existence de Litestream.

Ou, pensez-y comme ça : vous pouvez construire une application Remix basée sur SQLite répliqué par Litestream, et, pendant qu'elle tourne, ouvrir la base de données en utilisant la ligne de commande standard sqlite3 et faire quelques changements. Cela fonctionnera naturellement.

Vous pouvez en lire plus sur comment ça marche ici.

Cela semble compliqué, mais c'est incroyablement simple en pratique, et si vous jouez avec, vous verrez que ça "marche tout seul". Vous exécutez le binaire Litestream en mode "réplication" sur le serveur où se trouve votre base de données :

litestream replicate fruits.db s3://my-bukkit:9000/fruits.db

Et ensuite vous pouvez le "restaurer" à un autre endroit :

litestream restore -o fruits-replica.db s3://my-bukkit:9000/fruits.db

Commitez maintenant un changement dans votre base de données ; si vous la restaurez à nouveau, vous verrez le changement sur votre nouvelle copie.

La façon habituelle dont les gens utilisent Litestream aujourd'hui est de répliquer leur base SQLite sur S3 (c'est remarquablement peu coûteux pour la plupart des bases SQLite de se répliquer en direct sur S3). Ça, en soi, représente un gain opérationnel énorme : votre base de données est aussi résiliente que vous le demandez, et facilement déplacée, migrée ou manipulée.

Mais vous pouvez faire plus que ça avec Litestream. La prochaine version de Litestream vous permettra de répliquer SQLite en direct entre bases, ce qui signifie que vous pouvez configurer une base de données leader en écriture avec des répliques en lecture distribuées. Les répliques en lecture peuvent capturer les écritures et les rediriger vers le leader ; la plupart des applications sont orientées vers la lecture, et cette configuration donne à ces applications un système de base de données évolutif mondialement.

Litestream SQLite, Postgres, CockroachDB, ou toute autre base de données

Tous fonctionnent sur Fly.io ; nous intégrons un stockage persistant et un réseau privé pour un clustering facile, ce qui permet d'essayer facilement de nouveaux trucs.

Essayer Fly  

Vous devriez prendre cette option plus au sérieux

L'un de mes premiers emplois dans la technologie, au début des années 2000, était celui d'administrateur de base de données Oracle (DBA) pour une base de données Oracle9i. Je me souviens avoir passé des heures à éplucher des livres et de la documentation pour apprendre les tenants et aboutissants de la base de données Oracle. Et il y en avait beaucoup. Le guide d'administration comptait près de mille pages, et ce n'était que l'un des plus de cent guides de documentation.

À l'époque, apprendre à tourner les boutons pour optimiser les requêtes ou améliorer les écritures pouvait faire une grande différence. Les lecteurs de disques ne pouvaient lire que quelques dizaines de mégaoctets par seconde. L'utilisation d'un meilleur index pouvait donc transformer une requête de 5 minutes en une requête de 30 secondes.

Mais l'optimisation des bases de données est devenue moins importante pour les applications typiques. Si vous disposez d'une base de données de 1 Go, un disque NVMe peut faire entrer la totalité de la base en mémoire en moins d'une seconde. Même si j'adore optimiser les requêtes SQL, c'est un art en voie de disparition pour la plupart des développeurs d'applications. Même les requêtes mal réglées peuvent s'exécuter en moins d'une seconde pour les bases de données ordinaires.

Les versions modernes de Postgres sont un miracle. J'ai appris une tonne de choses en lisant leur code au fil des ans. Ce moteur comprend un grand nombre de fonctionnalités comme un optimiseur génétique de requêtes, des politiques de sécurité spécifiques au niveau de chaque ligne d’une table, sans compter une demi-douzaine de types d'index différents. Si vous avez besoin de ces fonctionnalités, il vous les faut. Mais vraisemblablement, la plupart d'entre vous n'en ont pas besoin.

Et si vous n'avez pas besoin des fonctionnalités de Postgres, elles constituent un handicap. Par exemple, même si vous n'utilisez pas de comptes utilisateurs multiples, vous devrez configurer et déboguer le système d'authentification basé sur l'hôte. Vous devez protéger votre serveur Postgres par un pare-feu. Et qui dit plus de fonctionnalités dit plus de documentation, ce qui rend difficile la compréhension du logiciel que vous exécutez. La documentation de Postgres 14 compte près de 3 000 pages.

SQLite possède un sous-ensemble de l'ensemble des fonctionnalités de Postgres. Mais ce sous-ensemble correspond à 99,9% de ce dont j'ai typiquement besoin. Excellent support du SQL, fonctions de fenêtrage (window functions)CTEs, recherche plein texte, JSON. Et lorsqu'il manque une fonctionnalité, les données sont déjà à proximité de mon application. Il y a donc peu de surcoût pour les extraire et les traiter dans mon code.

Par ailleurs, les problèmes complexes que je dois vraiment résoudre ne sont pas réellement traités par le cœur des fonctions de base de données. À la place, je veux optimiser deux choses : la latence et l'expérience de développement.

L'une des raisons de prendre SQLite au sérieux est donc qu'il est beaucoup plus simple d'un point de vue opérationnel. Vous passez votre temps à écrire du code d'application, et non à concevoir des niveaux de base de données complexes. Mais il y a l'autre problème.

La lumière est bien trop lente

Nous commençons à atteindre les limites théoriques. Dans le vide, la lumière parcourt environ 300 km en une milliseconde. C'est équivaut à peu près à la distance de Paris au Havre, aller-retour. Si l'on ajoute des couches de switchs réseau, de pare-feu et de protocoles d'application, la latence augmente encore.

Le surcoût de latence par requête pour une requête Postgres dans une seule région AWS peut atteindre une milliseconde. Ce n'est pas Postgres qui est lent, c'est vous qui vous heurtez aux limites de la vitesse de transmission des données. Maintenant, traitez une requête HTTP dans une application moderne. Une douzaine de requêtes de base de données et vous avez brûlé plus de 10 ms avant même que la logique métier ou que le rendu n'aie commencé.

Il existe un chiffre magique pour la latence : les réponses jusqu'à 100 ms semblent instantanées. Des applications rapides font des utilisateurs heureux. 100 ms, cela semble beaucoup, mais il est facile de le dépasser sans réfléchir. Le seuil de 100 ms est si important que les gens pré-rendent leurs pages et les postent sur des CDNs juste pour réduire la latence.

On devrait plutôt déplacer nos données à proximité de notre application. Proche comment ? Vraiment proche.

SQLite n'est pas simplement sur la même machine que votre application, mais est intégré à son processus. Lorsque vous placez vos données juste à côté de votre application, vous pouvez voir la latence par requête tomber à 10-20 microsecondes. C'est micro, avec un μ. Une amélioration de 50 à 100 fois par rapport à une requête Postgres intra-régionale.

Mais attendez, ce n'est pas tout. Nous avons effectivement éliminé la latence par requête. Notre application est rapide, mais elle est aussi plus simple. Nous pouvons décomposer les requêtes les plus importantes en de nombreuses requêtes plus petites et plus faciles à gérer, et consacrer le temps que nous utilisions jusqu'à présent à la chasse, sur un coin de table, aux requêtes N+1 à l'élaboration de nouvelles fonctionnalités.

La réduction de la latence ne concerne pas seulement la production. L'exécution de tests d'intégration avec une base de données client/serveur traditionnelle peut facilement prendre quelques minutes localement et la douleur persiste une fois que vous passez à l'intégration continue. La réduction de la boucle de rétroaction entre la modification du code et l'achèvement des tests ne permet pas seulement de gagner du temps, mais aussi de rester concentré sur le développement. Une modification d'une ligne de SQLite vous permettra de l'exécuter en mémoire et de réaliser des tests d'intégration en quelques secondes, voire moins.

Petit, rapide, fiable, mondialement distribué : prenez tout

Litestream est distribué et répliqué et, surtout, il est toujours facile à comprendre. Sérieusement, assez l'essayer. Il n'y a simplement pas grand-chose à savoir.

Ma thèse est la suivante : en construisant une réplication fiable et facile à utiliser pour SQLite, nous rendons attrayante l'exécution de toutes sortes d'applications complètes entièrement sur SQLite. Il était raisonnable de négliger cette option il y a 170 ans, lorsque le tutoriel du blog Rails a été écrit pour la première fois. Mais aujourd'hui, SQLite peut suivre la charge d'écriture de la plupart des applications, et les répliques peuvent faire évoluer les lectures vers autant d'instances que vous le souhaitez pour équilibrer la charge.

Litestream a des limites. Je l'ai construit pour des applications à un seul nœud, il ne fonctionnera donc pas bien sur des plateformes éphémères, "serverless" ou lors de déploiements continus. Il doit restaurer toutes les modifications de manière séquentielle, ce qui peut faire que les restaurations de bases de données prennent plusieurs minutes. Nous sommes en train de déployer la réplication en direct, mais le modèle de processus séparé nous limite à un contrôle au fil de l'eau des garanties de réplication.

On peut faire mieux. Au cours de l'année écoulée, je me suis attaché à mettre au point le cœur de Litestream et à mettre l'accent sur l'exactitude (correctness). Je suis satisfait de l'endroit où nous avons atterri. Au départ, c'était un simple outil de sauvegarde en continu, mais il évolue lentement vers une base de données fiable et distribuée. Il est maintenant temps de le rendre plus rapide et plus transparent, ce qui est tout mon travail chez Fly.io. Il y a des améliorations à venir pour Litestream — des améliorations qui ne sont pas du tout liées à Fly.io ! — que je suis impatient de partager.

Litestream a un nouveau foyer chez Fly.io, mais c'est et ce sera toujours un projet open-source. Mon plan pour les prochaines années est de continuer à le rendre plus utile, peu importe où fonctionne votre application, et de voir jusqu'où nous pouvons aller avec le modèle SQLite sur la façon dont les bases de données peuvent tourner.