web-dev-qa-db-fra.com

Utilisation du référentiel git comme backend de base de données

Je fais un projet qui traite de la base de données de documents structurés. J'ai un arbre de catégories (~ 1000 catégories, jusqu'à ~ 50 catégories à chaque niveau), chaque catégorie contient plusieurs milliers (jusqu'à, disons, ~ 10000) de documents structurés. Chaque document contient plusieurs kilo-octets de données sous une forme structurée (je préfère YAML, mais il peut tout aussi bien être JSON ou XML).

Les utilisateurs de ces systèmes effectuent plusieurs types d'opérations:

  • récupération de ces documents par ID
  • recherche de documents par certains des attributs structurés qu'ils contiennent
  • éditer des documents (c'est-à-dire ajouter/supprimer/renommer/fusionner); chaque opération d'édition doit être enregistrée comme une transaction avec un commentaire
  • visualiser un historique des modifications enregistrées pour un document particulier (y compris voir qui, quand et pourquoi a changé le document, obtenir une version antérieure - et probablement revenir à celui-ci si demandé)

Bien sûr, la solution traditionnelle serait d'utiliser une sorte de base de données de documents (comme CouchDB ou Mongo) pour ce problème - cependant, cette chose de contrôle de version (historique) m'a tenté de me faire une idée folle - pourquoi ne devrais-je pas utiliser git référentiel comme backend de base de données pour cette application?

À première vue, cela pourrait être résolu comme ceci:

  • catégorie = répertoire, document = fichier
  • obtenir le document par ID => changer de répertoire + lire un fichier dans une copie de travail
  • éditer des documents avec éditer des commentaires => faire des commits par divers utilisateurs + stocker des messages de commit
  • historique => journal git normal et récupération des transactions plus anciennes
  • search => c'est une partie un peu plus délicate, je suppose que cela nécessiterait l'exportation périodique d'une catégorie dans une base de données relationnelle avec indexation des colonnes que nous allons permettre de rechercher par

Y a-t-il d'autres pièges courants dans cette solution? Quelqu'un a-t-il déjà essayé d'implémenter un tel backend (c'est-à-dire pour tous les frameworks populaires - RoR, node.js, Django, CakePHP)? Cette solution a-t-elle des implications possibles sur les performances ou la fiabilité - c'est-à-dire qu'il est prouvé que git serait beaucoup plus lent que les solutions de base de données traditionnelles ou qu'il y aurait des pièges d'évolutivité/fiabilité? Je suppose qu'un cluster de tels serveurs qui se poussent/se déposent mutuellement devrait être assez robuste et fiable.

Fondamentalement, dites-moi si cette solution fonctionnera et pourquoi cela fonctionnera ou ne fonctionnera pas?

107
GreyCat

Répondre à ma propre question n'est pas la meilleure chose à faire, mais, comme j'ai finalement abandonné l'idée, je voudrais partager la justification qui a fonctionné dans mon cas. Je tiens à souligner que cette justification pourrait ne pas s'appliquer à tous les cas, c'est donc à l'architecte de décider.

Généralement, le premier point principal que ma question manque est que je traite avec système multi-utilisateur qui fonctionne en parallèle, en même temps, en utilisant mon serveur avec un client léger (c'est-à-dire juste un navigateur Web) . De cette façon, je dois maintenir état pour chacun d'eux. Il existe plusieurs approches pour celle-ci, mais toutes sont soit trop exigeantes en ressources ou trop complexes à implémenter (et donc à tuer le but initial de décharger tous les éléments d'implémentation difficile à git en premier lieu):

  • Approche "franche": 1 utilisateur = 1 état = 1 copie de travail complète d'un référentiel que le serveur gère pour l'utilisateur. Même si nous parlons d'une base de données de documents assez petite (par exemple, 100 Mio) avec environ 100 Ko d'utilisateurs, le maintien d'un clone de référentiel complet pour tous fait que l'utilisation du disque passe par le toit (c.-à-d. 100 Ko d'utilisateurs multipliés par 100 Mo ~ 10 TiB) . Pire encore, le clonage de 100 Mo de référentiel à chaque fois prend plusieurs secondes, même s'il est fait de manière assez efficace (c'est-à-dire sans utilisation par git et déballage-remballage), ce qui n'est pas acceptable, OMI. Et pire encore - chaque modification que nous appliquons à une arborescence principale doit être tirée vers le référentiel de chaque utilisateur, ce qui est (1) le porc de ressources, (2) peut conduire à des conflits d'édition non résolus dans le cas général.

    Fondamentalement, il peut être aussi mauvais que O (nombre de modifications × données × nombre d'utilisateurs) en termes d'utilisation du disque, et une telle utilisation du disque signifie automatiquement une utilisation assez élevée du processeur.

  • Approche "Uniquement les utilisateurs actifs": ne conservez la copie de travail que pour les utilisateurs actifs. De cette façon, vous ne stockez généralement pas un clone de repo complet par utilisateur, mais:

    • Lorsque l'utilisateur se connecte, vous clonez le référentiel. Cela prend plusieurs secondes et ~ 100 Mo d'espace disque par utilisateur actif.
    • Comme l'utilisateur continue de travailler sur le site, il travaille avec la copie de travail donnée.
    • Lorsque l'utilisateur se déconnecte, son clone de référentiel est recopié dans le référentiel principal en tant que branche, stockant ainsi uniquement ses "modifications non appliquées", s'il y en a, ce qui est assez peu encombrant.

    Ainsi, l'utilisation du disque dans ce cas culmine à O (nombre de modifications × données × nombre d'utilisateurs actifs), qui est généralement ~ 100..1000 fois moins que le nombre total d'utilisateurs, mais cela rend la connexion/déconnexion plus compliquée et plus lente , car cela implique le clonage d'une branche par utilisateur à chaque connexion et le retrait de ces modifications à la déconnexion ou à l'expiration de la session (ce qui devrait être fait de manière transactionnelle => ajoute une autre couche de complexité). En chiffres absolus, il réduit de 10 TiB l'utilisation du disque à 10..100 Gio dans mon cas, ce qui pourrait être acceptable, mais, encore une fois, nous parlons maintenant de manière équitable petit base de données de 100 Mio.

  • Approche "caisse clairsemée": faire "caisse clairsemée" au lieu d'un clone de repo complet par utilisateur actif n'aide pas beaucoup. Cela pourrait économiser environ 10 fois l'utilisation de l'espace disque, mais au prix d'une charge beaucoup plus élevée du processeur/disque sur les opérations impliquant l'historique, ce qui tue le but.

  • Approche "pool de travailleurs": au lieu de faire à chaque fois des clones à part entière pour une personne active, nous pourrions garder un pool de clones "de travailleurs", prêts à être utilisés. De cette façon, chaque fois qu'un utilisateur se connecte, il occupe un "travailleur", y tirant sa branche du référentiel principal, et, en se déconnectant, il libère le "travailleur", qui fait une réinitialisation matérielle intelligente pour redevenir juste un clone de dépôt principal, prêt à être utilisé par un autre utilisateur qui se connecte. N'aide pas beaucoup avec l'utilisation du disque (il est encore assez élevé - seulement un clone complet par utilisateur actif), mais au moins il rend la connexion/déconnexion plus rapide encore plus de complexité.

Cela dit, notez que j'ai intentionnellement calculé le nombre de bases de données et de bases d'utilisateurs assez petites: 100 000 utilisateurs, 1 000 utilisateurs actifs, 100 Mio de base de données au total + historique des modifications, 10 Mio de copie de travail. Si vous regardez des projets de crowdsourcing plus importants, il y a des chiffres beaucoup plus élevés:

│              │ Users │ Active users │ DB+edits │ DB only │
├──────────────┼───────┼──────────────┼──────────┼─────────┤
│ MusicBrainz  │  1.2M │     1K/week  │   30 GiB │  20 GiB │
│ en.wikipedia │ 21.5M │   133K/month │    3 TiB │  44 GiB │
│ OSM          │  1.7M │    21K/month │  726 GiB │ 480 GiB │

De toute évidence, pour ces quantités de données/activité, cette approche serait tout à fait inacceptable.

En règle générale, cela aurait fonctionné si l'on pouvait utiliser le navigateur Web comme un client "épais", c'est-à-dire émettre des opérations git et stocker à peu près la totalité du paiement du côté client, pas du côté serveur.

Il y a aussi d'autres points que j'ai ratés, mais ils ne sont pas si mauvais par rapport au premier:

  • Le modèle même d'avoir un état d'édition d'utilisateur "épais" est controversé en termes d'ORM normaux, comme ActiveRecord, Hibernate, DataMapper, Tower, etc.
  • Autant que j'ai cherché, il n'y a aucune base de code libre existante pour faire cette approche avec git à partir de frameworks populaires.
  • Il y a au moins un service qui parvient à le faire efficacement - c'est évidemment github - mais, hélas, leur base de code est une source fermée et je soupçonne fortement qu'ils n'utilisent pas les serveurs git normaux/stockage repo à l'intérieur, c'est-à-dire qu'ils ont fondamentalement implémenté un git alternatif "big data".

Donc, résultat : c'est c'est possible, mais pour la plupart des cas d'utilisation actuels, il ne sera pas du tout proche de l'optimal Solution. Rouler votre propre implémentation de l'historique des modifications de documents vers SQL ou essayer d'utiliser une base de données de documents existante serait probablement une meilleure alternative.

48
GreyCat

Une approche intéressante en effet. Je dirais que si vous avez besoin de stocker des données, utilisez une base de données, pas un référentiel de code source, qui est conçu pour une tâche très spécifique. Si vous pouvez utiliser Git prêt à l'emploi, alors ça va, mais vous devez probablement construire une couche de référentiel de documents dessus. Vous pouvez donc également le construire sur une base de données traditionnelle, non? Et si c'est le contrôle de version intégré qui vous intéresse, pourquoi ne pas simplement utiliser l'un des outils de référentiel de documents open source ? Il y a beaucoup de choix.

Eh bien, si vous décidez d'utiliser le backend Git de toute façon, alors cela fonctionnerait pour vos besoins si vous l'implémentiez comme décrit. Mais:

1) Vous avez mentionné "un groupe de serveurs qui se poussent/se tirent" - j'y pense depuis un moment et je ne suis toujours pas sûr. Vous ne pouvez pas pousser/tirer plusieurs dépôts comme une opération atomique. Je me demande s'il pourrait y avoir une possibilité de désordre de fusion pendant le travail simultané.

2) Peut-être que vous n'en avez pas besoin, mais une fonctionnalité évidente d'un référentiel de documents que vous n'avez pas répertorié est le contrôle d'accès. Vous pouvez éventuellement restreindre l'accès à certains chemins (= catégories) via des sous-modules, mais vous ne pourrez probablement pas accorder facilement l'accès au niveau du document.

12
Kombajn zbożowy

ma valeur de 2 pence. Un peu de nostalgie mais ...... J'avais une exigence similaire dans l'un de mes projets d'incubation. Semblable à la vôtre, mes principales exigences étaient une base de données de documents (xml dans mon cas), avec versionnage de documents. C'était pour un système multi-utilisateur avec beaucoup de cas d'utilisation de collaboration. Ma préférence était d'utiliser les solutions open source disponibles qui prennent en charge la plupart des exigences clés.

Pour aller droit au but, je n'ai trouvé aucun produit qui fournisse les deux, d'une manière suffisamment évolutive (nombre d'utilisateurs, volumes d'utilisation, ressources de stockage et de calcul) .J'étais partisan de git pour toutes les capacités prometteuses, et (probables) solutions que l'on pourrait en tirer. Comme je jouais plus avec l'option git, passer d'une perspective à utilisateur unique à une perspective multi (milli) utilisateur est devenu un défi évident. Malheureusement, je n'ai pas pu faire une analyse de performance substantielle comme vous l'avez fait. (.. paresseux/quittez tôt .... pour la version 2, mantra) Power to you !. Quoi qu'il en soit, mon idée biaisée s'est depuis transformée en l'alternative suivante (toujours biaisée): un maillage d'outils qui sont les meilleurs dans leurs sphères, bases de données et contrôle de version séparés.

Bien que toujours en cours (... et légèrement négligé), la version morphée est tout simplement la suivante.

  • sur le frontend: (userfacing) utilise une base de données pour le stockage de 1er niveau (interface avec les applications utilisateur)
  • sur le backend, utilisez un système de contrôle de version (VCS) (comme git) pour effectuer le versioning des objets de données dans la base de données

En substance, cela reviendrait à ajouter un plugin de contrôle de version à la base de données, avec une colle d'intégration, que vous devrez peut-être développer, mais cela peut être beaucoup plus facile.

Comment cela fonctionnerait (supposé), c'est que les principaux échanges de données de l'interface multi-utilisateurs se font via la base de données. Le SGBD gérera tous les problèmes amusants et complexes tels que le multi-utilisateur, la concurrence e, les opérations atomiques, etc. Sur le backend, le VCS effectuerait le contrôle de version sur un seul ensemble d'objets de données (pas de problèmes de concurrence ou multi-utilisateurs). Pour chaque transaction effective sur la base de données, le contrôle de version n'est effectué que sur les enregistrements de données qui auraient effectivement changé.

Quant à la colle d'interfaçage, elle se présentera sous la forme d'une simple fonction d'interfonctionnement entre la base de données et le VCS. En termes de conception, comme une approche simple serait une interface événementielle, avec des mises à jour des données de la base de données déclenchant les procédures de contrôle de version (indice: en supposant Mysql, utilisation de déclencheurs et sys_exec () bla bla .. .). En termes de complexité de mise en œuvre, il ira du simple et efficace (par exemple, l'écriture de scripts) au complexe et merveilleux (une interface de connecteur programmé). Tout dépend de la façon dont vous voulez devenir fou et du capital de sudation que vous êtes prêt à dépenser. Je pense que les scripts simples devraient faire la magie. Et pour accéder au résultat final, les différentes versions de données, une alternative simple consiste à remplir un clone de la base de données (plus un clone de la structure de la base de données) avec les données référencées par la balise de version/id/hash dans le VCS. encore une fois, ce bit sera un simple travail de requête/traduction/mappage d'une interface.

Il y a encore des défis et des inconnues à résoudre, mais je suppose que l'impact et la pertinence de la plupart d'entre eux dépendront largement des exigences de votre application et de vos cas d'utilisation. Certains peuvent finir par ne pas être des problèmes. Certains des problèmes incluent la correspondance des performances entre les 2 modules clés, la base de données et le VCS, pour une application avec une activité de mise à jour des données à haute fréquence, la mise à l'échelle des ressources (stockage et puissance de traitement) dans le temps du côté git en tant que données, et les utilisateurs croître: stable, exponentiel ou éventuellement plateau

Du cocktail ci-dessus, voici ce que je prépare actuellement

  • en utilisant Git pour le VCS (initialement considéré comme un bon vieux CVS pour la raison de l'utilisation uniquement de changesets ou de deltas entre 2 versions)
  • en utilisant mysql (en raison de la nature hautement structurée de mes données, xml avec des schémas xml stricts)
  • jouer avec MongoDB (pour essayer une base de données NoSQl, qui correspond étroitement à la structure de base de données native utilisée dans git)

Quelques faits amusants - git fait en fait des choses claires pour optimiser le stockage, comme la compression et le stockage des seuls deltas entre la révision des objets - OUI, git ne stocke que les changesets ou les deltas entre les révisions des objets de données, où est-il applicable (il sait quand et comment) . Référence: packfiles, au fond des tripes des internes de Git - L'examen du stockage d'objets de git (système de fichiers adressable par contenu), montre des similitudes frappantes (du point de vue conceptuel) avec les bases de données noSQL telles que mongoDB. Encore une fois, au détriment du capital de sudation, il peut offrir des possibilités plus intéressantes pour intégrer le 2 et le réglage des performances

Si vous êtes arrivé jusqu'ici, permettez-moi de savoir si ce qui précède peut être applicable à votre cas, et en supposant que ce serait le cas, comment cela correspondrait à certains des aspects de votre dernière analyse de performance complète

11
young chisango

J'ai implémenté une bibliothèque Ruby en plus de libgit2 ce qui rend cela assez facile à implémenter et à explorer. Il y a des limites évidentes, mais c'est aussi un système assez libérateur puisque vous obtenez la chaîne d'outils git complète.

La documentation comprend quelques idées sur les performances, les compromis, etc.

2
ioquatix

Comme vous l'avez mentionné, le cas multi-utilisateurs est un peu plus délicat à gérer. Une solution possible consisterait à utiliser des fichiers d'index Git spécifiques à l'utilisateur

  • pas besoin de copies de travail séparées (l'utilisation du disque est limitée aux fichiers modifiés)
  • pas de travail préparatoire long (par session utilisateur)

L'astuce consiste à combiner Git's GIT_INDEX_FILE variable d'environnement avec les outils pour créer des validations Git manuellement:

Un aperçu de la solution suit (hachages SHA1 réels omis des commandes):

# Initialize the index
# N.B. Use the commit hash since refs might changed during the session.
$ GIT_INDEX_FILE=user_index_file git reset --hard <starting_commit_hash>

#
# Change data and save it to `changed_file`
#

# Save changed data to the Git object database. Returns a SHA1 hash to the blob.
$ cat changed_file | git hash-object -t blob -w --stdin
da39a3ee5e6b4b0d3255bfef95601890afd80709

# Add the changed file (using the object hash) to the user-specific index
# N.B. When adding new files, --add is required
$ GIT_INDEX_FILE=user_index_file git update-index --cacheinfo 100644 <changed_data_hash> path/to/the/changed_file

# Write the index to the object db. Returns a SHA1 hash to the tree object
$ GIT_INDEX_FILE=user_index_file git write-tree
8ea32f8432d9d4fa9f9b2b602ec7ee6c90aa2d53

# Create a commit from the tree. Returns a SHA1 hash to the commit object
# N.B. Parent commit should the same commit as in the first phase.
$ echo "User X updated their data" | git commit-tree <new_tree_hash> -p <starting_commit_hash>
3f8c225835e64314f5da40e6a568ff894886b952

# Create a ref to the new commit
git update-ref refs/heads/users/user_x_change_y <new_commit_hash>

Selon vos données, vous pouvez utiliser un travail cron pour fusionner les nouvelles références vers master mais la résolution des conflits est sans doute la partie la plus difficile ici.

Les idées pour vous faciliter la tâche sont les bienvenues.

2
7mp