- SQLite peut être utilisé en production pour un petit site web, mais à mesure que le Django ORM prend en charge davantage de tâches, la complexité opérationnelle de la base de données devient tangible
- Une recherche FTS5 sur une table de 4 000 lignes prenait 5 secondes, mais est tombée à environ 0,05 seconde après exécution de
ANALYZE, confirmant que les statistiques du planificateur de requêtes peuvent avoir un fort impact sur les performances - Si un
DELETEmassif dépasse 5 secondes, les autres opérations d’écriture peuvent aussi expirer, et même les workers ainsi que la VM peuvent être arrêtés ; les tâches de nettoyage sont donc découpées en petits lots - Pour les sauvegardes,
VACUUM INTOet restic ont été utilisés, mais des arrêts OOM ont été rencontrés ; Litestream est aussi en cours de test pour une sauvegarde incrémentale plus efficace - Il est possible de répartir les tables dans plusieurs fichiers SQLite, et Mess with DNS fonctionne avec SQLite depuis 2022, pendant 4 ans, ce qui en fait un choix jugé adapté au projet
Exploiter SQLite sur un site Django
- SQLite a été adopté comme base de données du site Django en s’appuyant sur plusieurs articles expliquant qu’il peut être utilisé même pour de petits sites en production
- SQLite reste une base de données complexe qui demande aussi des connaissances d’exploitation, et dans ce projet, davantage de tâches qu’auparavant lui sont confiées via le Django ORM
- Conformément à plusieurs recommandations, le mode WAL a été activé dès le départ
- Il s’agit du quatrième site web utilisant SQLite
Les performances de recherche transformées par ANALYZE
- Une requête de recherche plein texte avec SQLite FTS5 sur une table de 4 000 lignes prenait 5 secondes
- Après exécution de
ANALYZE, la même requête est tombée à environ 0,05 seconde, au point qu’il n’a plus été nécessaire d’enquêter davantage - Il n’a pas été possible d’identifier précisément quel problème de plan de requête était en cause, mais il est supposé qu’il s’agissait d’une situation proche d’une complexité temporelle quadratique involontaire
ANALYZEgénère des informations statistiques, notamment le nombre de lignes des tables, afin d’aider le planificateur de requêtes à faire de meilleurs choix- La lecture des plans de requête n’est pas encore maîtrisée
Nettoyage des données et contrainte d’écriture unique
- Lors de la suppression massive de lignes inutiles, comme les tâches terminées de django-tasks-db, des problèmes en chaîne sont apparus
- Une commande de nettoyage traitant beaucoup de lignes s’exécutait pendant plus de 5 secondes
- Pendant ce temps, les écritures en base des autres workers dépassaient la limite configurée de 5 secondes et expiraient
- Les workers en échec sur l’écriture plantaient, et la VM était aussi arrêtée
- La raison exacte de la lenteur de
DELETEn’a pas été identifiée ; il est aussi envisagé que beaucoup de code Python s’exécute à l’intérieur de la transaction - Pour l’instant, les tâches de nettoyage sont découpées en petits lots afin qu’aucune requête en base ne dépasse 5 secondes
- Cette expérience a permis de mieux comprendre pourquoi une base comme Postgres, capable de gérer plusieurs écritures simultanées, peut être nécessaire
- À l’avenir, la possibilité de couper le site pendant ce type d’opérations pour effectuer une maintenance planifiée est aussi envisagée, mais aucun workflow n’est encore en place
Requêtes ORM et volume actuel des données
- Jusqu’ici, les requêtes souhaitées ont été construites avec le Django ORM sans vérification séparée des performances, mais en dehors du problème lié à
ANALYZE, tout a globalement bien fonctionné - La base de données reste petite, avec environ 10 000 lignes, et devrait le rester à l’avenir
Méthodes de sauvegarde SQLite
- Deux approches ont été essayées pour les sauvegardes SQLite : restic et Litestream
- Les tâches de sauvegarde sont généralement surveillées par un dead man’s switch, mais il semble qu’aucun vrai test de restauration n’ait encore été effectué
-
Sauvegarde avec restic
- Une copie de la base est créée avec
VACUUM INTO, compressée avec gzip, puis envoyée sur S3 - restic sert à créer la sauvegarde, vérifier les snapshots, supprimer les anciennes sauvegardes et faire le nettoyage
- Les sauvegardes se terminaient parfois par un OOM, laissant un verrou derrière elles, ce qui a conduit à exécuter aussi
restic unlock
sqlite3 /data/calendar.db "VACUUM INTO '/tmp/calendar.sqlite'" gzip /tmp/calendar.sqlite restic -r s3://s3.amazonaws.com/some_bucket/ unlock restic -r s3://s3.amazonaws.com/some_bucket/ backup /tmp/calendar.sqlite.gz restic -r s3://s3.amazonaws.com/some_bucket/ snapshots restic -r s3://s3.amazonaws.com/some_bucket/ forget -l 1 -H 6 -d 2 -w 2 -m 2 -y 2 restic -r s3://s3.amazonaws.com/some_bucket/ prune - Une copie de la base est créée avec
-
Sauvegarde incrémentale avec Litestream
- Litestream a commencé à être utilisé pour éviter les arrêts OOM des sauvegardes restic et tester l’efficacité d’une sauvegarde incrémentale
- Après avoir écrit le fichier de configuration, la réplication est lancée avec la commande suivante
litestream replicate -config litestream.ymlretention: 400ha été défini pour conserver l’historique de la base pendant un certain temps, mais il n’a pas encore été vérifié si cela fonctionne comme prévu- Les sauvegardes sont faites sur AWS, mais comme la création d’identifiants dans la console est fastidieuse, un passage futur vers un autre service compatible S3 est envisagé
Utiliser plusieurs fichiers de base de données SQLite
- Le projet actuel n’utilise qu’une seule base de données, mais dans Mess with DNS, les tables qui n’avaient pas besoin d’être regroupées ont été réparties dans 3 fichiers de base de données
- Cette séparation est considérée comme ayant aidé à l’exploitation
- Mess with DNS fonctionne avec SQLite depuis 2022, depuis 4 ans, et pour ce projet, le passage de Postgres à SQLite a été un bon choix
Des fonctionnalités de base découvertes tardivement en production
- SQLite a été utilisé pour la première fois dans un projet web en 2022, mais l’existence de
ANALYZEn’a été découverte que cette fois-ci - Même les fonctionnalités de base d’une technologie utilisée continuent à s’apprendre au fil des années en conditions réelles d’exploitation
1 commentaires
Commentaires sur Hacker News
Le mode
.expertde SQLite permet de repousser un peu le jour où il faut apprendre à lire les plans de requête : https://www.sqlite.org/cli.html#index_recommendations_sqlite...En analysant
SELECT * FROM x1 WHERE a=? AND b>?;, il recommande un index commeCREATE INDEX x1_idx_000123a7 ON x1(a, b);, puis, après sa création, une nouvelle analyse indique qu’aucun nouvel index n’est nécessaireMême avec de “vraies” bases de données comme Postgres, il est généralement recommandé de découper les opérations de nettoyage en petits lots. C’est simplement que, à petite échelle, le caractère inefficace de l’opération se voit moins, donc l’approche de l’article était plus juste qu’elle n’en avait l’air
Faire des commits fréquents aide, mais pour nettoyer régulièrement de très grosses bases de données, le partitionnement s’est révélé le plus efficace. Supprimer la partition la plus ancienne se termine presque instantanément
En revanche, dans « le worker n’arrive pas à écrire dans la base de données, plante et la VM s’arrête », on ne comprend pas bien pourquoi la VM s’arrête. Ici, VM semble désigner la machine virtuelle qui exécute le système d’exploitation
UPDATEouDELETEmodifiant des millions de lignes. Sinon, une seule requête oblige à envoyer d’un coup les millions de lignes mises à jour à tous les réplicasEXPLAIN QUERY PLAN: https://sqlite.org/eqp.htmlLe
EXPLAINclassique affiche généralement un bytecode bien trop verbeux, tandis queEXPLAIN QUERY PLANmontre un plan résuméEn tant que personne qui travaille avec des bases de données, cette lecture était frustrante, et j’avais envie d’identifier moi-même la cause pour la corriger. Si c’est une table de seulement 10 000 lignes, un scan complet de table devrait être très rapide, et encore plus avec SQLite exécuté dans le processus sur le même serveur physique
La solution évidente qui me venait à l’esprit était bien sûr « créez un index ». La lenteur de la suppression ressemble fortement au classique problème N+1 qu’on voit souvent chez les utilisateurs d’ORM avant qu’ils comprennent les interactions internes avec la base de données, et j’espère que Julia publiera un billet de suivi
Comme créer des identifiants dans la console AWS était trop pénible, j’ai créé il y a quelques années un outil qui ne résout que ce problème
uvx s3-credentials create my-existing-s3-bucketCette commande affiche des identifiants en lecture/écriture limités à ce seul bucket. On peut restreindre davantage avec
--read-onlyou--write-only, ou n’autoriser la lecture et l’écriture que sur les clés commençant par un préfixe donné avec--prefix foo/barJe l’ai aussi utilisé avec Restic et Cloudflare R2, et cela a très bien fonctionné
À l’ère des LLM, j’apprécie encore davantage le texte de Julia. Ce processus d’exploration sincère est un antidote aux textes générés automatiquement qui prétendent tout savoir avec assurance
J’exécute mes sauvegardes comme ceci :
OUT="${i}.sql.zst"PART="${OUT}.part"sqlite3 -readonly "${i}" .dump | zstd --fast --rsyncable -v -o "${PART}" -mv "${PART}" "${OUT}"Cela ne bloque pas les writers s’ils utilisent le WAL, et produit des dumps faciles à synchroniser avec un bon taux de compression. La base de données Home Assistant fait 1,8 Go, mais le dump compressé ne pèse que 286 Mo, et j’estime qu’environ 90 % du contenu généré chaque jour est identique
Je me demande aussi si la base de données est devenue aussi grosse à cause de l’historique de séries temporelles conservé sur une longue période
VACUUM INTO,.backup,sqlite3_rsyncet Litestream ne bloquent pas non plus les writers..backuputilise l’API de sauvegarde.dump, puis je suis passé à.backup. Cela dit, la méthode consistant à écrire dans.partpuis fairemvest éléganteJe trouve que ce texte manque de substance réelle, avec seulement des formulations comme « je n’ai pas creusé davantage », « c’est ma meilleure hypothèse », « peut-être d’autres choses aussi ? », « il se peut qu’il y ait beaucoup de code Python exécuté dans la transaction ». Il avance parfois même des suppositions erronées sans avoir vraiment vérifié ou enquêté
En tant qu’utilisateur de Debian, si un résultat de recherche Linux mène vers un forum Ubuntu, je ne l’ouvre généralement pas, justement parce qu’on y trouve trop de mauvaises suppositions. À l’inverse, même si Arch est assez différent de Debian, je consulte en général l’Arch Wiki, rédigé par des gens compétents
Cet article ne prétend pas être l’explication du plus grand expert mondial de SQLite, et le titre lui-même fixe clairement l’attente avec « quelques apprentissages sur l’exploitation de SQLite »
Dans l’ensemble des textes de Julia, on retrouve l’idée qu’il n’est pas nécessaire de tout savoir ni de faire semblant, et qu’on peut construire sa compréhension en identifiant les problèmes avec des méthodes simples. Partager aussi clairement que possible ce qu’on a découvert jusqu’ici est une bonne attitude
Hier encore, en développant une fonctionnalité, j’ai utilisé 2 langages de programmation, 2 systèmes de build, un fournisseur cloud, un outil de gestion des secrets, un framework complexe de communication client-serveur couvrant deux langages, un système de gestion de versions, un éditeur et un outil de CI. Et on pourrait encore creuser du côté du système d’exploitation, des versions de runtime, de la base de données, du proxy inverse, du cache ou de la logique métier
Si l’on approfondit tous les sujets rencontrés, on ne termine jamais rien ; il faut donc, comme l’autrice, choisir le problème sur lequel se concentrer
Le problème de
DELETEpeut être facilement atténué par des suppressions par lots, des pauses entre les lots, et une approche consistant à précharger lesrowidviaSELECT.SELECTne provoque pas de blocageSi les données ont été ajoutées séquentiellement dans la même table, il est probable qu’elles soient aussi stockées dans un ordre similaire dans le fichier ; les supprimer dans cet ordre, ou en ordre inverse, peut donc être plus rapide. Le résultat varie selon le support de stockage et d’autres conditions
SELECTvers un réplica, et la pression mémoire des index liée au filtrage des lignes pesait fortement sur le CPU et le buffer cache de la base de donnéesC’est une stratégie très puissante quand on ne peut pas facilement écarter les données inutiles du chemin principal de traitement, par exemple via l’élagage de partitions
Creuser un peu plus les bases de données que ne l’exigent votre niveau actuel ou les besoins du travail reste une bonne manière de progresser. Beaucoup de développeurs web se retrouvent bloqués face aux outils de base de données, et il m’arrive pareil avec certaines technologies d’exploitation comme K8s
Comprendre comment SQL se traduit en opérations de lecture et d’écriture de données sur disque aide énormément à juger intuitivement quelles approches sont raisonnables. Il faut aussi comprendre le mécanisme de verrouillage de la base
Avec ces connaissances, on est moins susceptible d’être déconcerté quand un simple
COUNTmet du temps à se terminer sur Postgressqlite_stat1etsqlite_stat4stockent diverses informations statistiques sur la distribution des valeurs d’index, et le planificateur de requêtes s’en sert pour estimer la sélectivité et l’utilité des indexsqlite_stat1ne fournit que le nombre d’enregistrements de l’index et le nombre moyen d’enregistrements par valeur ; si activé,sqlite_stat4stocke aussi des données d’histogrammeLitestream est très intéressant, et j’ai réussi à le faire fonctionner avec S3 comme backend. Cela permet à de très nombreuses applications utilisant SQLite de dépendre beaucoup moins de l’état du système de fichiers
L’état de S3 est bien plus facile à gérer, et le fournisseur prend aussi en charge les sauvegardes et la synchronisation