1 points par GN⁺ 2025-04-29 | 1 commentaires | Partager sur WhatsApp
  • Explique comment mettre en place une architecture utilisant une base de données distincte pour chaque locataire dans Rails, ainsi que les difficultés rencontrées
  • ActiveRecord est conçu à l’origine en partant du principe d’une connexion à une base unique, ce qui rend le basculement de connexion par locataire complexe et délicat
  • Propose une méthode pour basculer dynamiquement les connexions à l’exécution en exploitant la fonctionnalité connected_to de Rails 6 et versions ultérieures
  • SQLite3 convient bien à la gestion d’un grand nombre de petites bases indépendantes, ce qui facilite les opérations de sauvegarde, débogage et suppression
  • Souligne qu’à l’inverse de l’infrastructure Rails, surtout développée pour optimiser les très grands systèmes, une architecture centrée sur de petites bases de données indépendantes est tout à fait possible

Pourquoi utiliser une base de données distincte pour chaque locataire

  • En séparant les données par locataire (Site), qui fonctionne de manière autonome dans le modèle de données, l’isolation et l’administration deviennent plus simples
  • Stocker les données de chaque locataire dans une base distincte est aussi avantageux pour la montée en charge des grands sites et pour les enjeux de sécurité
  • Avec SQLite, une simple archive fichier suffit pour exploiter la base de données, sans configuration serveur, ce qui rend l’approche simple et flexible

Les difficultés dans Rails

  • Les opérations open/close de base de SQLite sont très simples, mais ActiveRecord repose en interne sur une gestion des connexions complexe
  • ActiveRecord est conçu pour attacher les connexions aux modèles, ce qui complique le changement de locataire à l’exécution
  • Le pool de connexions, le cache des requêtes et le cache de schéma dépendent tous de la connexion, ce qui rend chaque changement de connexion coûteux

Historique de la gestion multibase dans Rails

  • Rails 1 : possibilité de définir la base au niveau de ActiveRecord::Base
  • Rails 3 : introduction du pool de connexions
  • Rails 4 : ajout de connection_handling
  • Rails 6 : introduction de connected_to
  • Rails 7 : extension de connected_to et prise en charge du sharding
  • Mais des scénarios comme l’« ajout/suppression dynamique de locataires à l’exécution » ne sont toujours pas pris en charge nativement

Avantages d’une base de données par locataire

  • On peut sauvegarder ou restaurer uniquement les fichiers d’un locataire, ce qui simplifie l’exploitation et le débogage
  • Supprimer un locataire revient simplement à supprimer le fichier (unlink)
  • Les gros serveurs de bases de données optimisent des bases de plusieurs dizaines de téraoctets, alors que SQLite est optimisé pour des milliers de petites bases
  • En pratique, iCloud adopte aussi une architecture stockant des millions de petites bases SQLite au-dessus de Cassandra

Démarche de résolution du problème

  • L’approche existante (establish_connection manuel) provoquait des erreurs ConnectionNotEstablished dans des environnements à connexions multiples
  • En s’alignant sur l’approche de Rails 6 et versions suivantes, la structure a été modifiée pour laisser Rails gérer les connexions plutôt que de piloter manuellement le pool
  • Un pool de connexions est créé dynamiquement pour chaque locataire, puis les opérations sont encapsulées dans un bloc connected_to
  • L’utilisation d’un middleware a permis d’améliorer le système en préparant et en libérant dynamiquement la connexion DB nécessaire au moment de la requête

Motif de code essentiel

  • Vérifier le pool de connexions puis le créer s’il n’existe pas
MUX.synchronize do  
  if ActiveRecord::Base.connection_handler.connection_pool_list(role_name).none?  
    ActiveRecord::Base.connection_handler.establish_connection(database_config_hash, role: role_name)  
  end  
end  
  • Après la connexion, exécuter les requêtes en toute sécurité dans le bloc connected_to
ActiveRecord::Base.connected_to(role: role_name) do  
  pages = Page.order(created_at: :desc).limit(10)  
end  

Gestion du streaming Rack

  • Lorsque la réponse Rack est diffusée en streaming, la connexion est fermée proprement à l’aide de Rack::BodyProxy et de Fiber pour garantir une gestion sûre des connexions
connected_to_context_fiber = Fiber.new do  
  ActiveRecord::Base.connected_to(role: role_name) do  
    Fiber.yield  
  end  
end  
connected_to_context_fiber.resume  
  
status, headers, body = @app.call(env)  
body_with_close = Rack::BodyProxy.new(body) { connected_to_context_fiber.resume }  
  
[status, headers, body_with_close]  

Structure finale du middleware

  • Un middleware Shardine::Middleware a été écrit pour trouver la connexion DB appropriée à chaque requête, basculer via connected_to, puis nettoyer une fois la réponse terminée
  • Il peut être appliqué dans le fichier config.ru d’un projet Rails comme suit
use Shardine::Middleware do |env|  
  site_name = env["SERVER_NAME"]  
  {adapter: "sqlite3", database: "sites/#{site_name}.sqlite3"}  
end  

Travaux restant à faire

  • Dans ActiveRecord 6, la fonctionnalité shard n’a pas encore été exploitée, mais les versions suivantes permettent aussi la séparation lecture/écriture
  • La fonction de nettoyage du pool de connexions lors de la suppression d’un locataire n’a pas encore été implémentée, car elle n’est pas encore nécessaire
  • À l’avenir, les architectures gérant « un grand nombre de petites bases de données » pourraient attirer davantage l’attention

1 commentaires

 
GN⁺ 2025-04-29
Avis Hacker News
  • Utilise l’approche « database-per-tenant » avec environ 1 million d’utilisateurs

    • Cette approche convient bien aux applications centrées sur la lecture, et comme la plupart des tenants sont petits et que les tables contiennent peu d’enregistrements, même les jointures complexes sont très rapides
    • Le principal problème est qu’il faut migrer chaque base de données individuellement, ce qui peut allonger considérablement le temps de release
    • En cas de dérive de schéma ou de données, la release est bloquée et il faut chercher pourquoi certaines fonctionnalités ne marchent pas chez certains tenants
  • Aime SQLite, mais se demande si les bases OLTP existantes doivent décharger une partie des index de la mémoire

    • Avec une base de données par utilisateur, rien n’est conservé en mémoire pour les utilisateurs inactifs ou ceux actifs uniquement sur d’autres instances
    • Cela ressemble à la situation JSON de Mongo, et Postgres est deux fois plus rapide que Mongo
  • La plupart des gens n’ont pas besoin d’une base de données par tenant, et ce n’est pas l’approche habituelle

    • Il existe des cas spécifiques où cela compense les inconvénients comme les migrations et la dérive de schéma
    • Ce n’est pas parce qu’on peut l’utiliser qu’on doit forcément le faire
    • Il faut avancer avec prudence et savoir clairement qu’on a besoin d’une base par tenant
  • On peut envisager une approche intermédiaire

    • Identifier les N plus gros tenants
    • Séparer la base de données pour ces tenants
    • Les N principaux sont déterminés selon les IOPS, l’importance critique (en termes de revenus), etc.
    • Le modèle de données doit être conçu pour pouvoir extraire les lignes correspondant à chaque tenant
  • Travaille par hasard sur FeebDB pour Elixir

    • On peut le voir comme une alternative à Ecto, qui ne fonctionne pas bien quand il y a des milliers de bases de données
    • Cela a surtout commencé comme une expérimentation amusante, mais cette architecture aurait été d’une grande aide partout où il a travaillé auparavant
    • L’objectif est d’éliminer ou de réduire les problèmes habituels de l’approche base-de-données-par-tenant
    • Garantie d’un seul writer par base de données
    • Gestion améliorée des connexions pour tous les tenants
    • Support des migrations et des sauvegardes si nécessaire
    • Support des opérations map/reduce/filter sur plusieurs bases
    • Support du déploiement en cluster
  • Forward Email fait quelque chose de similaire en utilisant une base sqlite chiffrée pour chaque boîte mail/utilisateur

    • C’est un excellent moyen de différencier la protection par utilisateur
  • Le nom est vraiment excellent. Cela fait penser à Sean Connery

  • Le workflow « database per tenant » n’en est qu’à ses débuts

    • James Edward Gray en a parlé à RailsConf en 2012
  • A déjà utilisé quelque chose de similaire par le passé et en était très satisfait

    • Si un utilisateur veut ses données, on peut lui fournir la base complète
    • Si un utilisateur supprime son compte, cela se gère simplement avec rm username.sql
    • La conformité devient beaucoup plus simple
  • Il est difficile de faire un mauvais design quand les données sont isolées les unes des autres et qu’il n’y a pas de problème de montée en charge à l’intérieur d’un seul tenant

    • Presque tout fonctionnera