- OpenRun est une plateforme de déploiement d’applications web pour outils internes qui stocke les fichiers statiques, le code applicatif et les fichiers de configuration dans SQLite plutôt que dans le système de fichiers, afin de gérer l’état des déploiements de manière centrée sur la base de données
- L’objectif principal est de traiter en une seule transaction les mises à jour d’applications où plusieurs fichiers changent ensemble, afin d’éviter de servir des pages web cassées pendant un changement de version
- En utilisant le hachage SHA256 avant compression comme clé primaire, OpenRun réduit le stockage de fichiers dupliqués entre les versions d’une application, ainsi qu’entre les applications de staging, de preview et de production
- Le stockage via SQLite simplifie le rollback, les sauvegardes, le stockage des hachages pour les ETag et le stockage compressé en Brotli ; si nécessaire, des données GZip ou non compressées peuvent aussi être gérées en ajoutant des colonnes
- Aujourd’hui, OpenRun fonctionne sur un nœud unique et prévoit, pour le support multi-nœuds, d’utiliser à la fois un Postgres partagé et un cache de fichiers SQLite local afin de réduire la latence
Méthode de stockage des fichiers d’OpenRun
- OpenRun est une plateforme open source de déploiement pour outils internes code-first qui déploie des applications web en mode GitOps sur un nœud unique ou dans un cluster Kubernetes
- Au lieu de placer le contenu statique dans le système de fichiers comme le font les serveurs web classiques, OpenRun stocke dans SQLite les données applicatives telles que les fichiers statiques, le code applicatif et les fichiers de configuration
- Comme les métadonnées de l’application sont générées dynamiquement, le stockage en base de données est naturel, et traiter aussi les fichiers dans la même couche de stockage facilite la gestion de l’état du déploiement
- Lors de la création et de la mise à jour d’une application, les fichiers sont téléversés depuis GitHub ou depuis le disque local vers la base de données SQLite
- Seul le mode développement utilise le système de fichiers local
Pourquoi avoir choisi SQLite
- Les mises à jour transactionnelles constituent le principal avantage
- Plusieurs modifications de fichiers peuvent être regroupées et traitées dans une seule transaction
- Grâce à l’isolation, une application web cassée n’est pas servie pendant la mise à jour
- En cas d’erreur de déploiement, il est possible d’effectuer un rollback au niveau de la transaction de base de données
- Même lorsque plusieurs applications sont mises à jour en même temps, il est possible de tout annuler d’un seul coup
- C’est plus simple que de rechercher et nettoyer les fichiers modifiés dans le système de fichiers
- OpenRun gère automatiquement toutes les mises à jour par versioning, et les données de fichiers sont stockées dans une table du schéma suivant
CREATE TABLE files (sha text, compression_type text, content blob, create_time datetime, PRIMARY KEY(sha));
- Le hachage SHA256 du contenu avant compression est utilisé comme clé primaire, ce qui permet de ne stocker qu’une seule fois un même contenu de fichier à travers plusieurs versions
- Chaque application de production possède une application de staging et peut avoir plusieurs applications de preview, ce qui peut créer des doublons de fichiers
- Un stockage basé sur SQLite évite aussi de stocker en double les fichiers au contenu identique entre plusieurs applications
Sauvegarde, cache et compression
- L’état complet du système, les métadonnées et les fichiers peuvent être sauvegardés avec des outils de sauvegarde SQLite comme Litestream
- Si le SHA du contenu nécessaire à l’en-tête ETag pour le cache navigateur est stocké une fois lors du téléversement du fichier, il n’est plus nécessaire de le recalculer ensuite
- Le contenu des fichiers est stocké dans la table SQLite sous forme compressée en Brotli
- Avec une approche base de données, il suffit d’ajouter des colonnes à la table
files pour stocker aussi des données compressées en GZip ou non compressées
Performances et projet multi-nœuds
- Dans OpenRun, l’approche basée sur une base de données SQLite offre de bonnes performances
- Aucun benchmark comparatif direct n’a été réalisé, faute d’implémentation équivalente basée sur le système de fichiers
- D’après les benchmarks de l’équipe SQLite, SQLite peut offrir de meilleures performances que l’accès direct au système de fichiers sur certaines charges de travail
- OpenRun s’exécute actuellement sur un nœud unique
- Lorsqu’un support multi-nœuds sera ajouté, OpenRun prévoit d’utiliser une base de données Postgres partagée pour stocker les métadonnées et les données de fichiers à la place du SQLite local
- Cette approche peut introduire des problèmes de latence
- Pour éviter la latence d’accès à Postgres, OpenRun prévoit d’utiliser une base de données SQLite locale comme cache de fichiers
Pourquoi l’approche système de fichiers reste la plus courante
- L’une des raisons pour lesquelles la plupart des serveurs web utilisent le système de fichiers est la praticité
- Les fichiers peuvent être copiés et mis à jour avec des outils existants du système de fichiers comme rsync ou tar
- Une autre raison est historique
- Les systèmes de fichiers étaient déjà utilisés bien avant l’existence de bonnes bases de données relationnelles in-process
- Utiliser une base de données comme stockage de fichiers nécessite une interface API pour le téléversement des fichiers, ce qui n’est pas toujours réalisable
1 commentaires
Avis sur Hacker News
J’ai expérimenté cette idée il y a quelques années, en partie inspiré par l’article « 35% Faster Than The Filesystem » : https://www.sqlite.org/fasterthanfs.html
Mes notes de l’époque sont ici : https://simonwillison.net/2020/Jul/30/fun-binary-data-and-sq...
J’ai créé https://datasette.io/plugins/datasette-media, un plugin pour servir des fichiers statiques depuis SQLite dans Datasette, et il fonctionne bien, mais honnêtement je ne l’ai pas beaucoup utilisé depuis que je l’ai fait.
Dans le même ordre d’idées, il existe aussi une façon de servir des tuiles cartographiques depuis SQLite, et https://datasette.io/plugins/datasette-tiles s’en charge. Il s’avère que le format MBTiles est en fait une base de données SQLite remplie de PNG.
Si vous voulez expérimenter SQLite pour servir des fichiers, l’outil en ligne de commande « sqlite-utils insert-files » peut être utile pour la configuration initiale de la base de données : https://sqlite-utils.datasette.io/en/stable/cli.html#inserti...
Le hash du contenu n’a besoin d’être créé qu’une seule fois, lors de l’upload du fichier, sans étape de build qui le recrée à chaque redémarrage du serveur web ou qui renomme les fichiers réels. On peut aussi l’appliquer dynamiquement à des fichiers du système de fichiers (voir l’implémentation embedFS de https://github.com/benbjohnson/hashfs), mais la base de données rend cela un peu plus simple.
requests-cache, si je me souviens bien, met en cache les requêtes dans SQLite avec
(date, URI): https://github.com/requests-cache/requests-cache/blob/main/r...Recherche pyfilesystem SQLite : https://www.google.com/search?q=pyfilesystem+sqlite
Recherche sendfile mmap SQLite : https://www.google.com/search?q=sendfile+mmap+sqlite
https://github.com/adamobeng/wddbfs est un « fournisseur webdavfs permettant de lire le contenu d’une base de données sqlite ».
Il doit aussi y avoir une bonne façon d’implémenter un système de fichiers au-dessus de SQLite en y ajoutant les permissions Unix et les permissions d’attributs étendus xattrs.
SQLite serait-il, par exemple, plus rapide ou plus pratique que ngx_http_memcached_module.c ? Je me demande aussi si SQLite dispose d’ACL au niveau des cellules.
Quand on lit des fichiers statiques, il faut ouvrir, lire et fermer le fichier à chaque requête, ce qui entraîne davantage de changements de contexte même si la couche du système de fichiers a mis le contenu du fichier en cache. Si l’on veut accélérer cela, la bonne approche est d’ajouter un frontend de cache, pas de tout transformer en base de données. Ce sera plus rapide que SQLite, et plus facile à maintenir et à dépanner.
Cela inclut les systèmes de fichiers qui tournent entièrement en espace utilisateur. FUSE est exclu, puisque ses appels passent par le noyau.
Dire que les « mises à jour transactionnelles » sont le principal avantage a ses limites. Que le serveur utilise SQLite ou le système de fichiers, cela ne suffit pas en soi à empêcher qu’une webapp se retrouve cassée pendant une mise à jour.
Chaque page dans le navigateur est un arbre de ressources récupérées par des requêtes HTTP distinctes ; elle n’est donc pas la cible du système de transactions/mises à jour atomiques côté serveur. Même si le serveur remplace toutes les ressources dans une transaction, le navigateur peut voir une combinaison mêlant anciennes et nouvelles ressources.
La solution habituelle consiste à donner à toutes les sous-ressources de la page (bundles JavaScript, feuilles de style, médias, etc.) un nom (URL) contenant un hash du contenu ou une version. Si le document HTML racine charge la version X, toutes les sous-ressources doivent aussi charger la version X correspondante.
De plus, lors d’une mise à jour de X vers Y, il faut continuer à fournir les sous-ressources de X pendant un certain temps après avoir commencé à servir les pages Y. Si on ne les conserve pas jusqu’à être raisonnablement certain qu’aucun navigateur n’est encore en train de charger une page X, cette page X peut casser.
Vouloir mettre le HTML racine et les sous-ressources dans un seul bundle remplacé atomiquement est donc, au contraire, une mauvaise idée : cela supprime d’anciennes sous-ressources qui peuvent encore être référencées.
Selon les cas, on peut aussi vouloir versionner séparément certaines sous-ressources, comme les fichiers média, indépendamment du document HTML. Si l’on veut les mettre à jour sans invalider tout le cache des éléments de structure de l’app, comme les blocs JavaScript ou les feuilles de style, le système de build des pages doit parfois en tenir compte.
Quand une grande entreprise a expérimenté cela (à une époque où elle observait une part importante du Web), la majorité des utilisateurs (plus de 80 %) restaient dans la webapp environ 2 à 3 jours. Le chiffre était probablement biaisé par les personnes qui laissaient des onglets ouverts pendant le week-end.
Le 95e percentile était autour de 2 semaines, et le 100 % autour de 600 jours. En gros, certains utilisateurs avaient gardé un onglet ouvert pendant presque deux ans.
Si l’on vise 100 %, il faut donc attendre assez longtemps. Tous ces chiffres reposent sur ma mémoire, et je ne travaille plus dans cette entreprise.
Le scénario où un utilisateur reste longtemps sur une page puis reçoit un lien cassé relève davantage d’un problème côté SPA.
Je suis globalement d’accord, mais les mises à jour transactionnelles ne protègent que contre une catégorie de problèmes liés aux mises à jour. D’autres problèmes au niveau de l’application peuvent aussi produire une expérience cassée.
Continuer à servir d’anciennes versions de contenu statique référencé par hash de contenu est possible, mais ce n’est pas actuellement implémenté dans Clace.
L’astuce essentielle consiste à téléverser les changements non HTML avant les changements HTML, afin qu’un fichier ne soit pas référencé avant d’exister. Si vous voulez rendre l’application aussi complexe que possible, vous pouvez appliquer un parcours en profondeur au téléversement. Mais si vous tenez à votre santé mentale, mieux vaut atténuer le problème et choisir, dans l’application, un téléversement des assets en premier.
En 2011/2012, quand je travaillais dans un petit studio de jeux vidéo, j’ai recommandé de déplacer tous les assets de moins de 100 Ko dans une base sqlite3, puis de créer des « fichiers pak » et de stocker les offsets de ces fichiers dans la base sqlite3.
Ce choix a été influencé par une présentation post-mortem de Richard Hipp, où il disait qu’avec le recul il aurait aimé traiter les BLOB comme des inodes, en les plaçant à des offsets plus loin dans la base, et en ajoutant les BLOB à la fin du fichier.
Le chargement des assets était extrêmement rapide. Comme c’était un jeu mobile, il n’y avait que très peu d’assets absents de la base. C’est aussi intéressant de voir d’autres personnes adopter davantage cette approche depuis.
Un autre avantage facile à manquer est qu’on peut attacher une quantité presque illimitée de métadonnées à côté du contenu, ce qui permet de trouver des fichiers « similaires » via des requêtes en base de données.
Nous avons mis énormément de métadonnées dans la base ; le fichier pak final faisait, je crois, environ 200 Mo, et la base autour de 20 Mo. Encore une fois, c’était un jeu mobile.
Le pire côté client a été une double jointure interne que nous n’avons pas pu supprimer à cause de la complexité côté serveur. C’était frustrant de ne pas pouvoir implémenter nous-mêmes le serveur : l’équipe avec laquelle nous travaillions était très mauvaise en développement logiciel et modifiait les spécifications backend sans tout nous communiquer, ce qui cassait soudainement les builds.
Nous utilisions aussi une base sqlite3 séparée pour les replays de parties, et après la fin d’un match on pouvait rejouer toute la partie et voir ce que chaque adversaire avait fait. C’était aussi très utile pour les tests automatisés.
Dans le système de contrôle de changements lix aussi, nous avons fini par mettre les fichiers dans SQLite au lieu de manipuler le système de fichiers et git. Cet article traite le problème que nous avons rencontré : https://opral.substack.com/i/150054233/breaking-git-compatib...
SQLite résout des problèmes comme le verrouillage de fichiers et la concurrence.
Avec SQLite, on peut interroger les fichiers en SQL au lieu d’utiliser des API de système de fichiers propres à chaque plateforme.
Les requêtes SQL peuvent être écrites de manière type-safe avec Kysely https://kysely.dev/, sans ORM.
Il faut toutefois faire attention au fait qu’une base SQLite ne rétrécit pas si on ne fait pas de vacuum. En gros, cela revient à copier les données dans un fichier séparé puis à supprimer l’original.
C’est une opération à lancer manuellement à un moment pertinent dans l’application ; si l’on écrit et supprime des données binaires de cette façon, il faut donc surveiller l’usage disque.
Fait intéressant, le CMS de génération de site statique que j’ai créé fonctionne exactement à l’inverse de l’approche décrite ici.
Pendant le développement/la mise à jour du site web, toutes les pages et tous les articles sont des entrées dans une base SQLite, manipulées via une interface web qui affiche une version éditable du site.
Ensuite, le site est exporté sous forme de pages statiques dans le système de fichiers pour être déployé directement, ou téléchargé en zip puis envoyé ailleurs, y compris sur des services d’hébergement entièrement statiques.
Selon « Appropriate Uses For SQLite » de SQLite https://www.sqlite.org/whentouse.html, le trafic web que SQLite peut gérer dépend de l’intensité avec laquelle le site utilise la base de données
En général, un site à moins de 100K hits par jour devrait bien fonctionner avec SQLite. 100K/jour est une estimation prudente, pas une limite stricte. SQLite a déjà géré des trafics dix fois supérieurs
Le site de SQLite (https://www.sqlite.org/) utilise bien sûr SQLite lui aussi et, en 2015, traitait environ 400K à 500K requêtes HTTP par jour, dont 15 à 20 % étaient des pages dynamiques touchant à la base de données. Le contenu dynamique utilise environ 200 instructions SQL par page web
Cette configuration tourne sur une seule VM partageant un serveur physique avec 23 autres VM, tout en maintenant la plupart du temps une charge moyenne sous 0,1. Référence : https://news.ycombinator.com/item?id=33975635
Pour une charge surtout en lecture, comme le service de fichiers statiques, SQLite peut en gérer beaucoup plus. Si des en-têtes de cache de contenu sont configurés, le navigateur met le contenu en cache, donc les requêtes serveur ne deviennent nécessaires que pour les nouveaux clients
Dans la plupart des cas d’usage, SQLite ne risque pas vraiment d’être le goulot d’étranglement
L’idée de servir du contenu statique avec SQLite en s’appuyant seulement sur la page « 35% Faster Than The Filesystem » de 2017 paraît, pour le dire gentiment, assez peu mûre
Les serveurs web modernes comme Nginx utilisent des stratégies optimisées pour traiter les fichiers statiques. Cela commence avec sendfile, puis va jusqu’à io_uring et aux opérations splice, le tout au sein de pools de threads bien conçus, bâtis selon les besoins sur epoll, kqueue ou eventport
À l’inverse, le mieux que SQLite puisse offrir par défaut se limite à peu près à la prise en charge des I/O mappées en mémoire (https://www.sqlite.org/mmap.html)
Cette approche peut bien convenir à un service à client unique, comme une application web hébergée localement (voir aussi https://github.com/electron/asar). Mais pour un gros site web, comme le disent d’autres commentaires, cela revient à essayer de résoudre un problème qui n’existe pas
Je fais beaucoup de calcul scientifique haute performance, et notamment quand il faut accéder aux données en parallèle, la méthode la plus flexible et rapide a souvent été une base SQLite en lecture seule sur un ramdisk
Cela donne une impression très bricolée, mais c’est plus simple à configurer et plus rapide que tout ce que j’ai trouvé jusqu’ici
Un ami dans l’astronomie disait que beaucoup de gens dans les sciences devraient se familiariser avec les bases de données. Sinon, ils finissent sans s’en rendre compte par consacrer des efforts énormes à créer leur propre très mauvaise base de données
Si cette approche n’est pas plus répandue, c’est parce que les systèmes de fichiers sont excellents pour gérer des fichiers
Si l’on a besoin de mises à jour atomiques, il suffit de faire un checkout dans un nouveau répertoire et de changer un lien symbolique
J’ai vu plusieurs versions où l’on utilise une base de données comme un système de fichiers ; il y a de bons côtés, mais quand les choses tournent mal, cela peut devenir cauchemardesque
Ensuite, on peut utiliser quelque chose comme btrfs pour dédupliquer au niveau du système de fichiers
L’argument selon lequel, comme beaucoup de fichiers peuvent changer lors d’une mise à jour d’app, utiliser une base de données permettrait de traiter atomiquement toutes les modifications via une transaction et d’éviter de servir des pages web cassées pendant un changement de version pose problème
La raison est que les fichiers SQLite se verrouillent vis-à-vis des lectures pendant les écritures afin d’atteindre une isolation sérialisable. On en conclut donc plutôt qu’il vaut mieux effectuer les opérations de base de données sur un fichier hors ligne, puis remplacer l’ancien fichier en production par le nouveau
Cela revient en fin de compte à utiliser un fichier tar, ou un répertoire séparé à remplacer par le nouveau contenu
Les fichiers statiques sont beaucoup plus simples à servir statiquement. Il n’est pas nécessaire de les servir depuis un programme qui gère des connexions SQLite en temps réel et tente d’obtenir une étrange magie de « concurrence des mises à jour ». Ce problème peut se résoudre sans aucune difficulté
Gérer un CMS dans une base SQLite est très bien, mais si le contenu est statique et servi en temps réel, mieux vaut utiliser des fichiers statiques