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

119

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érerais YAML, mais cela 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 quelques commentaires
  • afficher l'historique des modifications enregistrées pour un document particulier (y compris afficher qui, quand et pourquoi a modifié le document, obtenir une version antérieure - et probablement revenir à celle-ci si demandé)

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

Au premier coup d'œil, cela pourrait être résolu comme ceci:

  • category = répertoire, document = fichier
  • obtention du document par ID => changement de répertoire + lecture d'un fichier dans une copie de travail
  • éditer des documents avec des commentaires d'édition => faire des commits par divers utilisateurs + stocker les messages de commit
  • history => 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 autoriserons à rechercher par

Y a-t-il d'autres écueils 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 écueils d'évolutivité / fiabilité? Je suppose qu'un cluster de tels serveurs qui se poussent / tirent le référentiel de l'autre devrait être assez robuste et fiable.

En gros, dites-moi si cette solution fonctionnera et pourquoi elle fonctionnera ou ne fonctionnera pas?

GreyCat
la source
s'il vous plaît voir youtube.com/watch?v=nPPlyjMlQ34
Assaf S.

Réponses:

58

Répondre à ma propre question n'est pas la meilleure chose à faire, mais, comme j'ai finalement abandonné l'idée, j'aimerais partager sur la justification qui a fonctionné dans mon cas. Je tiens à souligner que cette justification peut 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 travaille avec un système multi-utilisateur qui fonctionne en parallèle, simultanément, en utilisant mon serveur avec un client léger (c'est-à-dire juste un navigateur Web). De cette façon, je dois maintenir l' état pour tous. Il existe plusieurs approches pour celui-ci, mais toutes sont soit trop lourdes pour les ressources, soit trop complexes à implémenter (et donc en quelque sorte tuer le but initial de décharger toutes les implémentations matérielles sur git en premier lieu):

  • Approche "émoussée": 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 ~ 100 K d'utilisateurs, le maintien d'un clonage complet du référentiel pour tous fait courir l'utilisation du disque à travers le toit (c'est-à-dire 100 K d'utilisateurs multiplié par 100 Mo ~ 10 Tio) . Ce qui est encore pire, le clonage d'un référentiel de 100 Mio à chaque fois prend plusieurs secondes de temps, même s'il est fait de manière assez efficace (c'est-à-dire ne pas utiliser git et décompresser-reconditionner des trucs), ce qui n'est pas acceptable, IMO. Et pire encore, chaque modification que nous appliquons à une arborescence principale doit être extraite dans le référentiel de chaque utilisateur, ce qui est (1) une corvée de ressources, (2) peut conduire à des conflits de modification 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 «Seuls les utilisateurs actifs»: conserver la copie de travail uniquement pour les utilisateurs actifs. De cette façon, vous ne stockez généralement pas un clone de référentiel complet par utilisateur, mais:

    • Lorsque l'utilisateur se connecte, vous clonez le référentiel. Cela prend plusieurs secondes et ~ 100 Mio d'espace disque par utilisateur actif.
    • Au fur et à mesure que 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, ne stockant ainsi que 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 lors de la déconnexion ou de l'expiration de la session (ce qui devrait être fait de manière transactionnelle => ajoute une autre couche de complexité). En chiffres absolus, cela fait tomber 10 Tio d'utilisation du disque à 10..100 Gio dans mon cas, cela pourrait être acceptable, mais, encore une fois, nous parlons maintenant d'une base de données assez petite de 100 Mio.

  • Approche "Sparse checkout": faire un "checkout clairsemé" au lieu d'un clone de dépôt 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 CPU / disque beaucoup plus élevée sur les opérations impliquant l'historique, ce qui tue le but.

  • Approche "Workers pool": au lieu de faire des clones à part entière à chaque fois pour une personne active, nous pourrions garder un pool de clones "worker", prêts à être utilisés. De cette façon, chaque fois qu'un utilisateur se connecte, il occupe un "worker", y tirant sa branche du repo principal, et, au fur et à mesure qu'il se déconnecte, il libère le "worker", qui fait une réinitialisation intelligente git hard pour redevenir juste un clone de référentiel principal, prêt à être utilisé par un autre utilisateur se connectant. N'aide pas beaucoup avec l'utilisation du disque (il est toujours assez élevé - seulement un clone complet par utilisateur actif), mais au moins, cela accélère la connexion / déconnexion, au coût de 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: 100K utilisateurs, 1K utilisateurs actifs, 100 Mio de base de données totale + historique des modifications, 10 Mio de copie de travail. Si vous regardez des projets de crowdsourcing plus importants, les chiffres sont 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 / d'activité, cette approche serait tout à fait inacceptable.

En général, cela aurait fonctionné si l'on pouvait utiliser le navigateur Web comme un client "épais", c'est-à-dire en émettant des opérations git et en stockant à peu près l'intégralité de la caisse du côté du client, pas du côté du serveur.

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

  • Le modèle même d'avoir l'état d'édition de l'utilisateur "épais" est controversé en termes d'ORM normaux, tels que ActiveRecord, Hibernate, DataMapper, Tower, etc.
  • Autant que j'ai recherché, il n'y a aucune base de code gratuite existante pour faire cette approche de git à partir de frameworks populaires.
  • Il y a au moins un service qui parvient d'une manière ou d'une autre à 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 de serveurs git / techniques de stockage de référentiel normaux à l'intérieur, c'est-à-dire qu'ils ont essentiellement implémenté alternative "big data" git.

Ainsi, la ligne de fond : il est possible, mais pour la plupart usecases actuelles , il ne sera pas partout près de la solution optimale. Rouler votre propre implémentation de l'historique d'édition de document vers SQL ou essayer d'utiliser n'importe quelle base de données de documents existante serait probablement une meilleure alternative.

GreyCat
la source
16
Probablement un peu en retard à la fête, mais j'avais une exigence similaire à celle-ci et j'ai en fait emprunté la route git. Après quelques recherches sur les composants internes de git, j'ai trouvé un moyen de le faire fonctionner. L'idée est de travailler avec un référentiel nu. Il y a quelques inconvénients, mais je trouve que cela fonctionne. J'ai tout écrit dans un article que vous voudrez peut-être consulter (le cas échéant, par intérêt): kenneth-truyers.net/2016/10/13/git-nosql-database
Kenneth
Une autre raison pour laquelle je ne fais pas cela est les capacités de requête. Les magasins de documents indexent souvent les documents, ce qui facilite leur recherche. Ce ne sera pas simple avec git.
FrankyHollywood
12

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 c'est bien, mais vous devez probablement créer 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 quand même d'opter pour le backend Git, cela fonctionnerait essentiellement pour vos besoins si vous l'implémentiez comme décrit. Mais:

1) Vous avez mentionné "un cluster de serveurs qui se poussent / se tirent" - j'y ai réfléchi pendant un moment et je ne suis toujours pas sûr. Vous ne pouvez pas pousser / tirer plusieurs dépôts en tant qu'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.

Kombajn zbożowy
la source
11

mes 2 pence valent. Un peu envie 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 gestion des versions de documents. C'était pour un système multi-utilisateurs avec de nombreux cas d'utilisation de collaboration. Ma préférence était d'utiliser des 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 offrant les deux, d'une manière suffisamment évolutive (nombre d'utilisateurs, volumes d'utilisation, ressources de stockage et de calcul) .J'étais biaisé vers git pour toutes les capacités prometteuses, et des solutions (probables) que l'on pourrait en tirer. Au fur et à mesure que je jouais avec l'option git, passer d'une perspective mono-utilisateur à 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 / quitter 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 une alternative (toujours biaisée): un maillage d'outils qui sont les meilleurs dans leurs sphères distinctes, bases de données et contrôle de version.

Alors que le travail est encore en cours (... et légèrement négligé), la version transformée est 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 contrôle de version 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 un peu de colle d'intégration, que vous devrez peut-être développer, mais qui pourrait être beaucoup plus facile.

Comment cela fonctionnerait (censé), c'est que les échanges de données de l'interface multi-utilisateur primaire se font via la base de données. Le SGBD gérera tous les problèmes amusants et complexes tels que les opérations multi-utilisateurs, simultanées, atomiques, etc. Sur le backend, le VCS effectuerait le contrôle de version sur un seul ensemble d'objets de données (pas de concurrence ou des problèmes 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, 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: supposer Mysql, utilisation de déclencheurs et sys_exec () bla bla ...). En termes de complexité d'implémentation, cela va du simple et efficace (scripting par exemple) au complexe et merveilleux (certaines interfaces de connecteurs programmées). Tout dépend de la façon dont vous voulez y aller et du capital de sueur que vous êtes prêt à dépenser. Je pense qu'un script simple devrait faire la magie. Et pour accéder au résultat final, aux différentes versions de données, une alternative simple est de peupler 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 / hachage dans le VCS. encore une fois, ce bit sera un simple travail de requête / traduction / carte d'une interface.

Il y a encore des défis et des inconnus à résoudre, mais je suppose que l'impact et la pertinence de la plupart d'entre eux dépendront en grande partie des exigences de votre application et de vos cas d'utilisation. Certains peuvent simplement 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 de données à haute fréquence, la mise à l'échelle des ressources (stockage et puissance de traitement) au fil du temps du côté git comme les données et les utilisateurs grandir: 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 le en raison de l'utilisation uniquement de changesets ou de deltas entre 2 versions)
  • en utilisant mysql (en raison de la nature très 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 les révisions d'objets - OUI, git ne stocke que les ensembles de modifications ou les deltas entre les révisions d'objets de données, où est-il applicable (il sait quand et comment) . Référence: packfiles, au plus profond des entrailles de Git - L'examen du stockage d'objets de git (système de fichiers adressable par le contenu) montre des similitudes frappantes (du point de vue du concept) avec des bases de données noSQL telles que mongoDB. Encore une fois, au détriment du capital de sueur, cela peut offrir des possibilités plus intéressantes pour l'intégration du 2 et le réglage des performances.

Si vous êtes arrivé aussi loin, permettez-moi si ce qui précède peut s'appliquer à votre cas, et en supposant que ce soit le cas, comment cela correspondrait à certains des aspects de votre dernière analyse complète des performances.

jeune chisango
la source
4

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

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

ioquatix
la source
2

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

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

L'astuce consiste à combiner la GIT_INDEX_FILEvariable d'environnement de Git avec les outils pour créer manuellement des commits Git:

Un aperçu de la solution suit (les hachages SHA1 réels sont 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>

En fonction de vos données, vous pouvez utiliser un travail cron pour fusionner les nouvelles références, mastermais la résolution des conflits est sans doute la partie la plus difficile ici.

Les idées pour faciliter les choses sont les bienvenues.

7 mégapixels
la source
C'est généralement une approche qui ne mène nulle part, à moins que vous ne souhaitiez avoir un concept complet de transaction et d'interface utilisateur pour la résolution manuelle des conflits. L'idée générale des conflits est de faire en sorte que l'utilisateur le résolve dès la validation (c'est-à-dire "désolé, quelqu'un d'autre a édité ce document que vous étiez en train de modifier -> veuillez voir ses modifications et vos modifications et les fusionner"). Lorsque vous autorisez deux utilisateurs à s'engager avec succès et que vous découvrez dans un cronjob asynchrone que les choses ont mal tourné, il n'y a généralement personne de disponible pour résoudre les problèmes.
GreyCat