2 points par GN⁺ 2023-11-08 | 1 commentaires | Partager sur WhatsApp
  • La PR #1705 de refactorisation du PDS de Bluesky atproto modifie le PDS pour qu’il utilise un datastore SQLite en locataire unique, et stocke le repo de chaque utilisateur ainsi que l’état privé de son compte dans son propre fichier SQLite
  • Les bases utilisateur sont stockées selon la structure de chemin /${dbDirectory}/${sha256Hex(did).slice(0,2)}/${did}, et la clé de signature de chaque repo est conservée à côté du fichier SQLite correspondant
  • L’abstraction d’accès aux données utilisateur existante est remplacée par ActorStore, et comme SQLite ne prend pas en charge les transactions concurrentes, les opérations d’écriture doivent explicitement établir une transaction avec le store
  • Les handles de fichiers DB ouverts et les clés de signature sont gérés via LRUCache, avec jusqu’à 30k handles de fichiers ouverts et 30k clés conservés en mémoire, et le handle de fichier est fermé lorsqu’une base est évincée du cache
  • Trois bases SQLite distinctes sont introduites pour la gestion de l’état du service, exécutées en mode WAL afin de permettre les lectures concurrentes et la réplication en streaming, et la distribution PDS devrait inclure Litestream ou un outil similaire

Changements clés de la PR

  • La PR #1705 refactorise le PDS autour d’un datastore SQLite en locataire unique
  • Chaque utilisateur dispose de son propre fichier SQLite dédié, qui stocke le repo de cet utilisateur et l’état privé de son compte
  • Les bases utilisateur sont stockées dans un chemin hiérarchique utilisant le hash du DID
    • Format du chemin : /${dbDirectory}/${sha256Hex(did).slice(0,2)}/${did}
  • La repo signing key de chaque repo est stockée au même emplacement que le fichier SQLite

ActorStore et modèle de transaction

  • L’abstraction d’accès aux données utilisateur passe des anciens « services » à ActorStore
  • La principale différence d’ActorStore est la séparation des classes pour la lecture et l’écriture
  • Comme SQLite ne prend pas en charge les transactions concurrentes, toute opération d’écriture doit explicitement établir une transaction avec le store
  • Le journal des commits inclut notamment une refonte des reader et transactor, le traitement des races de transaction de l’actor store, et le nettoyage de l’interface du store

Gestion du cache et des handles de fichiers

  • Un LRUCache est maintenu pour les clés de signature et les bases de données
  • Les limites configurées sont les suivantes
    • maximum de 30k handles de fichiers ouverts
    • maximum de 30k clés conservées en mémoire
  • Lorsqu’une base est évincée du cache, son handle de fichier est fermé
  • Les commits associés incluent actor store in lru cache, fix open handles

Trois bases SQLite pour l’état du service

  • En plus des bases par utilisateur, trois bases SQLite distinctes sont introduites pour gérer l’état du service
    • service DB : gère les informations de compte, les codes d’invitation, les refresh tokens, etc.
    • did cache DB : contient uniquement une table unique pour le cache de résolution DID
    • sequencer DB : contient uniquement une table unique pour gérer l’ordre des mises à jour de tous les repos d’un service
  • Chaque fichier SQLite fonctionne en WAL mode
  • Le but du WAL mode est de permettre les lectures concurrentes et la réplication en streaming
  • La distribution PDS devrait inclure Litestream ou un outil similaire

État de la revue et de la fusion

  • Cette PR comprend au total 143 commits et a été fusionnée de la branche pds-sqlite-refactor vers la branche pds-v2
  • La date de fusion est le 1er novembre 2023, et le commit de fusion est 8449ceb
  • Le reviewer devinivy a laissé plusieurs notes et commentaires avant d’approuver les changements
  • devinivy a estimé que la refactorisation comportait « beaucoup d’excellentes simplifications » et donnait globalement une impression de meilleure organisation
  • Après la fusion, la branche pds-sqlite-refactor a été supprimée

Question ultérieure

  • Le 28 février 2025, npetrangelo a examiné l’ampleur des changements de cette PR et a demandé un résumé des compromis entre l’ancienne architecture Postgres et l’architecture SQLite introduite par cette PR
  • Le contenu fourni ne comprend pas la réponse de Bluesky à cette question

1 commentaires

 
GN⁺ 2023-11-08
Commentaires sur Hacker News
  • J’aime bien SQLite, mais l’approche qui consiste à séparer le schéma ou la base de données pour chaque tenant est généralement pleine de difficultés
    Sur une instance partagée, avec la sécurité au niveau des lignes (RLS), si une migration échoue on peut tout rollback, mais avec un schéma par tenant, si une migration de données échoue à cause de données inattendues, les utilisateurs restent sur des versions de schéma différentes jusqu’à ce qu’on trouve la cause
    Une fois qu’on atteint l’échelle du sharding, on peut de toute façon se retrouver avec des problèmes similaires, mais avant cela, une base de données unique est ce qu’il y a de plus simple, et il peut aussi falloir plus tard fusionner des données ou transférer atomiquement la propriété de ressources
    Je ne suis pas opposé à cette configuration, elle a des cas d’usage, mais dans mon entreprise on est en train de fuir à toute vitesse les schémas par tenant. Sans un véritable investissement, il y a trop de problèmes, et rarement cette préparation existe au moment où l’idée apparaît au départ
    Ce qui est amusant, c’est qu’il y a une dizaine d’années, l’application a commencé avec un SQLite par tenant, puis est passée à des schémas PostgreSQL par tenant, et maintenant elle migre vers un schéma unique avec RLS, donc l’évolution a pris exactement la direction inverse

    • Pour avoir géré d’énormes bases de données en production, je n’ai aucune envie de recommencer
      Quand la charge devient suffisante, chaque changement devient risqué, parce qu’il est impossible de tester exhaustivement tous les cas extrêmes de performance
      Voir un utilisateur du free tier trouver un chemin de code sans index et mettre la production à genoux, c’est aussi un schéma fréquent

    • Le fait que certains utilisateurs restent sur une autre version de schéma après l’échec d’une migration de données n’est pas forcément un gros problème
      Pour un service de cette taille et de cette complexité, on fait généralement les mises à niveau de schéma par étapes : 1. rendre le code compatible avec le futur schéma, 2. migrer les données, 3. supprimer le support de l’ancien schéma
      Donc en général il faut pouvoir fonctionner longtemps en toute sécurité dans l’état entre les étapes 1 et 2. Bien sûr, les nouveaux bugs restent une exception, mais d’un point de vue opérationnel, tant qu’on suit cette procédure, un système revenu à un état intermédiaire de migration peut rester acceptable

    • Si le produit a moins de 100 clients, il peut être au contraire préférable que chacun soit sur une version de schéma différente
      Chaque client peut avoir son propre calendrier de mise à niveau et ses propres exigences, et je connais aussi des entreprises qui font des personnalisations pour certains clients au point de ne plus vraiment exécuter le même code
      Au final, tout dépend de la structure du business

    • Pour être juste, il y a 10 ans RLS n’existait pas encore. C’est arrivé avec PostgreSQL 9.5 en 2016

    • https://blog.turso.tech/introducing-embedded-replicas-deploy...

      https://electric-sql.com/

  • Je ne comprends pas ce qu’on veut dire par « SQLite ne prend pas en charge les transactions concurrentes »
    À moins d’accéder au fichier .db via un partage de fichiers comme UNC ou NFS, je pensais que c’était bien pris en charge : https://www.sqlite.org/wal.html
    Je l’ai utilisé pour lire et mettre à jour une base depuis plusieurs threads/processus sur la même machine, et si on a besoin d’une vue cohérente ou qu’on ne veut pas garder une transaction ouverte trop longtemps, on peut aussi faire un snapshot avec l’API de backup de SQLite
    Il se peut que je rate quelque chose, et comme je n’ai pas touché à SQLite depuis quelques années, je ne suis pas totalement sûr

    • Non, en fait. Je me trompais. En pratique, c’est plutôt plusieurs lectures, une seule écriture
      C’est ce que j’avais supposé depuis longtemps sans le vérifier assez soigneusement. Cela dit, la plupart des bases que j’ai construites avec SQLite étaient bien plus orientées lecture qu’écriture
      Je corrige

    • Avec un peu de patience, hctree [1] finira peut-être par se stabiliser, et on pourra alors choisir entre le mécanisme backend traditionnel et un backend avec prise en charge de la concurrence nouvellement implémenté

      [1] https://sqlite.org/hctree/doc/hctree/doc/hctree/index.html

    • D’après la documentation, le writer ne fait qu’ajouter du nouveau contenu à la fin du fichier WAL, ce qui permet lecture et écriture en parallèle, mais comme il n’y a qu’un seul fichier WAL, un seul writer peut écrire à la fois
      Le billet d’origine voulait sans doute dire que les opérations de mise à jour doivent s’exécuter de façon séquentielle

    • Avec peu de trafic, ça fonctionne, mais quand les transactions grossissent ou que le nombre d’écritures concurrentes augmente, même avec WAL activé on finit à un moment donné par tomber sur des problèmes de database locked
      On peut les contourner dans une certaine mesure au niveau applicatif, mais en général, si on en est arrivé là, il faut sérieusement envisager un autre backend de base de données

    • Au moins la dernière fois que j’ai vérifié, cela voulait probablement dire qu’il n’y a pas de verrouillage au niveau des lignes, et que le verrouillage au niveau des tables est lui aussi très limité
      D’après la documentation, le writer prend toujours un verrou sur l’ensemble de la base de données

  • C’est intéressant, et j’aime bien la stratégie qui consiste à avoir un ratio 1:1 entre utilisateur et base de données
    Cela dit, je me demande comment ils gèrent les données qui nécessitent des agrégations entre utilisateurs. Si je suis abonné à un autre utilisateur et qu’il publie un post, comment ma base est-elle mise à jour avec ce nouveau contenu ? Ou bien est-ce que cela ne concerne que les données persistantes comme les profils ou les relations de suivi, tandis que les données interactives comme le feed sont traitées séparément ?
    J’aime aussi le fait que le « connection pooling » ne soit finalement rien de plus qu’une limitation du nombre de handles ouverts via un cache LRU. C’est aussi intéressant que chaque connexion DB soit mono-thread, donc que la concurrence soit gérée au niveau de la tenancy plutôt qu’au niveau de la connexion
    Il semble aussi facile d’ajouter par-dessus une limitation de débit par base, ce qui pourrait empêcher les abus d’un utilisateur donné
    Je me demande également s’il existe un moyen simple de configurer Litestream pour un nombre arbitraire de bases de données

  • C’est toujours agréable de voir l’adoption de SQLite/Litestream progresser côté serveur. Nous aussi, on l’utilise quand on crée de nouvelles applications
    SQLite + Litestream est un meilleur choix pour des bases de données de tenants, et le coût de la réplication et des sauvegardes sur S3/R2 est bien inférieur à celui des bases de données cloud managées coûteuses [1]
    Jusqu’à 3900 % moins cher que SQLServer sur Azure

[1] https://docs.servicestack.net/ormlite/litestream

  • Je ne comprends pas ce que signifie 3900 % moins cher

  • Dans une ancienne boîte fintech où j’ai travaillé, l’entreprise stockait les comptes clients sous forme de fichiers sqlite3 chiffrés dans un blob storage, et cela correspondait plutôt bien au modèle d’accès

    • Je me demande comment ils géraient le verrouillage des fichiers lors du réupload après modification
  • À première vue, ça ressemble à une combinaison du pire et de l’horrible
    J’aimerais que quelqu’un écrive un bon article avec de vrais chiffres pour expliquer les avantages et analyser les défauts attendus. Ça pourrait être un sujet vraiment intéressant si on l’étudie correctement

    • Tu peux expliquer pourquoi ça te semble être une « combinaison du pire et de l’horrible » ?
      Vu de l’extérieur, cela paraît assez raisonnable, surtout si on part du principe qu’on construit un système distribué que de nombreux utilisateurs, et pas seulement des administrateurs système professionnels, devront exécuter et déployer
      Cela semble d’ailleurs être l’objectif ici, et on peut s’attendre à ce que l’un des buts de conception soit d’éviter d’avoir à installer, configurer et administrer une base de données supplémentaire ou d’autres serveurs
  • J’aimerais que quelqu’un qui connaît mieux Bluesky explique quelles données sont stockées dans SQLite et lesquelles ne le sont pas
    Je pars du principe que cela n’inclut pas les messages entre utilisateurs

    • Je pense que les messages entre utilisateurs sont aussi stockés dans ces bases de données SQLite
      Pense à l’email. Si tu envoies un email et mets cinq personnes en copie, sept personnes stockent chacune une copie du même message sur leur propre serveur mail
      Ce n’est donc pas une architecture avec une base de données centrale contenant un seul email auquel les autres feraient référence
      Le sharding de bases de données relationnelles fonctionne fondamentalement de cette manière aussi
      Ce type de dénormalisation des données devient presque indispensable à mesure qu’une application passe à l’échelle, en particulier pour les applications many-to-many où le ratio lecture/écriture est élevé
      Si le ratio lecture/écriture est faible, une architecture relationnelle avec un maître unique et plusieurs esclaves peut gérer un volume étonnamment élevé de requêtes et de données
    • En tant qu’utilisateur, cela contient tous les posts et réponses que tu publies
      Pour l’instant, Bluesky héberge pratiquement le seul PDS, mais l’objectif final est que chaque utilisateur final ait son propre PDS
      Inrupt/SOLID appelle ce concept un « pod »
      En fait, ils ont intégré hier un deuxième PDS en production, donc il y a du progrès
    • Si par messages tu entends des messages directs, donc des messages privés entre deux parties, Bluesky n’a pas actuellement cette fonctionnalité
      Il n’y a que des messages publics diffusés au monde entier
      Je n’ai pas vérifié séparément s’il existe un projet pour les messages directs
  • Pourquoi hacher les utilisateurs en sha256 pour les répartir dans des répertoires cibles de deux caractères ?
    md5 n’est-il pas bien plus rapide tout en résolvant le même problème ?

    • J’imagine que ce hachage est effectué relativement rarement, donc la différence de performance se perd dans le bruit
      Et cela évite d’avoir à répondre à la question « pourquoi avoir utilisé un hachage non sûr ? », tout en éliminant ou en réduisant toute une catégorie potentielle de problèmes de sécurité
    • À cette échelle, ils se soucient peut-être des collisions
      Ou alors, comme moi, ils sont tellement noyés sous les outils de sécurité de l’entreprise qu’ils n’ont pas envie de créer une exception spécifique à chaque usage de md5
    • Ce n’est pas sain de laisser traîner des hachages cryptographiques cassés
      Si tu n’as pas besoin d’un hachage de sécurité, il existe plein de hachages non cryptographiques rapides
    • C’est probablement moins une question de collisions que de limites du système de fichiers, c’est-à-dire du nombre maximal de fichiers dans un répertoire
  • Bluesky est encore sur invitation ?

    • Oui, mais pas pour des raisons de « growth hacking »
      C’est un moyen de limiter la croissance pendant qu’ils font monter le système en charge côté backend et prévention des abus
      Il existe une file d’attente dédiée aux développeurs, et on peut obtenir l’accès assez rapidement : https://atproto.com/blog/call-for-developers