Usando o repositório git como backend de banco de dados


119

Estou fazendo um projeto que lida com banco de dados de documentos estruturados. Eu tenho uma árvore de categorias (~ 1000 categorias, até ~ 50 categorias em cada nível), cada categoria contém vários milhares (até, digamos, ~ 10.000) de documentos estruturados. Cada documento tem vários kilobytes de dados em alguma forma estruturada (eu preferiria YAML, mas também pode ser JSON ou XML).

Os usuários deste sistema realizam vários tipos de operações:

  • recuperação desses documentos por ID
  • pesquisar documentos por alguns dos atributos estruturados dentro deles
  • editar documentos (ou seja, adicionar / remover / renomear / mesclar); cada operação de edição deve ser registrada como uma transação com algum comentário
  • visualizar um histórico de alterações registradas para um documento específico (incluindo a visualização de quem, quando e por que alterou o documento, obtendo uma versão anterior - e provavelmente revertendo para esta se solicitado)

Claro, a solução tradicional seria usar algum tipo de banco de dados de documentos (como CouchDB ou Mongo) para este problema - no entanto, essa coisa de controle de versão (histórico) me levou a uma ideia selvagem - por que eu não deveria usar o gitrepositório como um backend de banco de dados para este aplicativo?

À primeira vista, poderia ser resolvido assim:

  • categoria = diretório, documento = arquivo
  • obtendo documento por ID => alterando diretórios + lendo um arquivo em uma cópia de trabalho
  • editar documentos com comentários de edição => fazer commits por vários usuários + armazenar mensagens de commit
  • histórico => log git normal e recuperação de transações mais antigas
  • search => essa é uma parte um pouco mais complicada, acho que exigiria a exportação periódica de uma categoria para um banco de dados relacional com indexação de colunas que permitiremos pesquisar por

Existem outras armadilhas comuns nesta solução? Alguém já tentou implementar esse back-end (ou seja, para qualquer framework popular - RoR, node.js, Django, CakePHP)? Esta solução tem alguma implicação possível no desempenho ou confiabilidade - ou seja, está provado que o git seria muito mais lento do que as soluções de banco de dados tradicionais ou haveria alguma armadilha de escalabilidade / confiabilidade? Eu presumo que um cluster de tais servidores que fazem push / pull do repositório uns dos outros deve ser bastante robusto e confiável.

Basicamente, diga-me se essa solução funcionará e por que funcionará ou não?


Respostas:


58

Responder à minha própria pergunta não é a melhor coisa a fazer, mas, como acabei desistindo da ideia, gostaria de compartilhar o raciocínio que funcionou no meu caso. Gostaria de enfatizar que esse raciocínio pode não se aplicar a todos os casos, então cabe ao arquiteto decidir.

Geralmente, o primeiro ponto principal que minha pergunta deixa passar é que estou lidando com um sistema multiusuário que funciona em paralelo, simultaneamente, usando meu servidor com um cliente fino (ou seja, apenas um navegador da web). Dessa forma, tenho que manter o estado para todos eles. Existem várias abordagens para este, mas todas elas são muito difíceis em recursos ou muito complexas para implementar (e, assim, meio que matar o propósito original de descarregar todo o material de implementação difícil para o git em primeiro lugar):

  • Abordagem "bruta": 1 usuário = 1 estado = 1 cópia de trabalho completa de um repositório que o servidor mantém para o usuário. Mesmo que estejamos falando de um banco de dados de documentos razoavelmente pequeno (por exemplo, 100s MiBs) com ~ 100K de usuários, manter um clone de repositório completo para todos eles faz com que o uso do disco ultrapasse o limite (ou seja, 100K de usuários vezes 100MiB ~ 10 TiB) . E o que é ainda pior, clonar um repositório de 100 MiB a cada vez leva vários segundos, mesmo se feito de maneira bastante eficaz (ou seja, não usando por git e descompactando-reempacotando coisas), o que não é aceitável, IMO. E ainda pior - cada edição que aplicamos a uma árvore principal deve ser puxada para o repositório de cada usuário, o que é (1) consumo de recursos, (2) pode levar a conflitos de edição não resolvidos em casos gerais.

    Basicamente, pode ser tão ruim quanto O (número de edições × dados × número de usuários) em termos de uso do disco, e esse uso do disco significa automaticamente um alto uso da CPU.

  • Abordagem "Apenas usuários ativos": mantenha uma cópia de trabalho apenas para usuários ativos. Dessa forma, você geralmente não armazena um repo-clone completo por usuário, mas:

    • Conforme o usuário efetua login, você clona o repositório. Demora vários segundos e aproximadamente 100 MiB de espaço em disco por usuário ativo.
    • Conforme o usuário continua a trabalhar no site, ele trabalha com a cópia de trabalho fornecida.
    • Conforme o usuário efetua logout, seu clone de repositório é copiado de volta para o repositório principal como um branch, armazenando assim apenas suas "alterações não aplicadas", se houver alguma, que é bastante eficiente em termos de espaço.

    Assim, o uso do disco, neste caso, atinge o pico em O (número de edições × dados × número de usuários ativos), que geralmente é ~ 100..1000 vezes menor que o número total de usuários, mas torna o login / logout mais complicado e mais lento , já que envolve a clonagem de um branch por usuário em cada login e puxando essas alterações de volta no logout ou expiração da sessão (o que deve ser feito de forma transacional => adiciona outra camada de complexidade). Em números absolutos, cai 10 TiBs de uso de disco para 10..100 GiBs no meu caso, isso pode ser aceitável, mas, novamente, agora estamos falando de um banco de dados bastante pequeno de 100 MiBs.

  • Abordagem de "checkout esparso": fazer "checkout esparso" em vez de clone repo completo por usuário ativo não ajuda muito. Isso pode economizar cerca de 10 vezes o uso de espaço em disco, mas às custas de uma carga muito maior de CPU / disco em operações que envolvem histórico, o que acaba com o propósito.

  • Abordagem de "pool de trabalhadores": em vez de fazer clones completos todas as vezes para a pessoa ativa, podemos manter um pool de clones "trabalhadores", prontos para serem usados. Dessa forma, toda vez que um usuário se loga, ele ocupa um "trabalhador", puxando lá seu branch do repo principal, e, ao se desconectar, ele libera o "trabalhador", que faz git hard reset inteligente para se tornar mais uma vez apenas um clone repo principal, pronto para ser usado por outro usuário que fizer login. Não ajuda muito com o uso do disco (ainda é muito alto - apenas clone completo por usuário ativo), mas pelo menos torna o login / logout mais rápido, à custa de ainda mais complexidade.

Dito isso, observe que calculei intencionalmente números de banco de dados e base de usuários relativamente pequenos: 100 mil usuários, mil usuários ativos, banco de dados total de 100 MiBs + histórico de edições, 10 MiBs de cópia de trabalho. Se você olhar para projetos de crowdsourcing mais proeminentes, há números muito mais altos lá:

│              │ 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 │

Obviamente, para essa quantidade de dados / atividade, essa abordagem seria totalmente inaceitável.

Geralmente, teria funcionado, se alguém pudesse usar o navegador da web como um cliente "grosso", ou seja, emitindo operações git e armazenando praticamente o checkout completo no lado do cliente, não no lado do servidor.

Também há outros pontos que perdi, mas não são tão ruins em comparação com o primeiro:

  • O próprio padrão de ter o estado de edição "espesso" do usuário é controverso em termos de ORMs normais, como ActiveRecord, Hibernate, DataMapper, Tower, etc.
  • Por mais que eu tenha pesquisado, não há nenhuma base de código livre existente para fazer essa abordagem para git a partir de estruturas populares.
  • Há pelo menos um serviço que de alguma forma consegue fazer isso com eficiência - que é obviamente o github - mas, infelizmente, sua base de código é de código fechado e eu suspeito fortemente que eles não usam servidores git normais / técnicas de armazenamento repo dentro, ou seja, eles basicamente implementaram git "big data" alternativo.

Portanto, linha de fundo : é possível, mas para a maioria dos casos de uso atuais, não estará nem perto da solução ideal. Acumular sua própria implementação de histórico de edição de documento para SQL ou tentar usar qualquer banco de dados de documento existente seria provavelmente uma alternativa melhor.


16
Provavelmente um pouco tarde para a festa, mas eu tinha um requisito semelhante a este e realmente peguei a rota do git. Depois de pesquisar um pouco sobre os componentes internos do git, descobri uma maneira de fazê-lo funcionar. A ideia é trabalhar com um repositório vazio. Existem algumas desvantagens, mas acho que é viável. Escrevi tudo em uma postagem que você pode querer conferir (se houver alguma, por interesse): kenneth-truyers.net/2016/10/13/git-nosql-database
Kenneth

Outro motivo para não fazer isso são os recursos de consulta. Os armazenamentos de documentos geralmente indexam documentos, facilitando a pesquisa dentro deles. Isso não será direto com o git.
FrankyHollywood

12

De fato, uma abordagem interessante. Eu diria que se você precisa armazenar dados, use um banco de dados, não um repositório de código-fonte, que é projetado para uma tarefa muito específica. Se você pudesse usar o Git pronto para uso, tudo bem, mas provavelmente você precisará construir uma camada de repositório de documentos sobre ele. Portanto, você também pode construí-lo sobre um banco de dados tradicional, certo? E se você está interessado no controle de versão integrado, por que não usar uma das ferramentas de repositório de documentos de código aberto ? Há muito por onde escolher.

Bem, se você decidir ir para o back-end Git de qualquer maneira, então basicamente ele funcionaria para seus requisitos se você o implementasse conforme descrito. Mas:

1) Você mencionou "cluster de servidores que empurram / puxam uns aos outros" - pensei nisso por um tempo e ainda não tenho certeza. Você não pode empurrar / puxar vários repos como uma operação atômica. Eu me pergunto se poderia haver a possibilidade de alguma confusão de mesclagem durante o trabalho simultâneo.

2) Talvez você não precise, mas uma funcionalidade óbvia de um repositório de documentos que você não listou é o controle de acesso. Você pode restringir o acesso a alguns caminhos (= categorias) por meio de submódulos, mas provavelmente não será capaz de conceder acesso no nível do documento facilmente.


11

meu valor de 2 pence. Um pouco de saudade, mas ... tive um requisito semelhante em um de meus projetos de incubação. Semelhante ao seu, meus principais requisitos eram um banco de dados de documentos (xml no meu caso), com versionamento de documentos. Era para um sistema multiusuário com muitos casos de uso de colaboração. Minha preferência era usar soluções de código aberto disponíveis que atendessem à maioria dos principais requisitos.

Para ir direto ao ponto, não consegui encontrar nenhum produto que fornecesse ambos, de uma forma que fosse escalonável o suficiente (número de usuários, volumes de uso, armazenamento e recursos de computação). Eu estava inclinado para o git por toda a capacidade promissora, e soluções (prováveis) que alguém poderia criar a partir dele. Conforme eu brincava mais com a opção git, mudar de uma perspectiva de usuário único para uma perspectiva de vários (milli) usuários tornou-se um desafio óbvio. Infelizmente, não consegui fazer uma análise de desempenho substancial como você fez. (.. preguiçoso / saia cedo .... para a versão 2, mantra) Poder para você !. De qualquer forma, minha ideia tendenciosa se transformou na próxima alternativa (ainda tendenciosa): uma malha de ferramentas que são as melhores em suas esferas separadas, bancos de dados e controle de versão.

Enquanto ainda está em andamento (... e ligeiramente negligenciado), a versão transformada é simplesmente esta.

  • no frontend: (userfacing) use um banco de dados para o armazenamento de primeiro nível (interface com os aplicativos do usuário)
  • no back-end, use um sistema de controle de versão (VCS) (como git) para realizar o controle de versão dos objetos de dados no banco de dados

Em essência, isso equivaleria a adicionar um plugin de controle de versão ao banco de dados, com alguma cola de integração, que você pode ter que desenvolver, mas pode ser muito mais fácil.

Como isso (deveria) funcionar é que as trocas de dados da interface multiusuário primária são feitas por meio do banco de dados. O SGBD tratará de todas as questões divertidas e complexas, como multiusuário, simultaneidade e, operações atômicas, etc. No backend, o VCS executaria o controle de versão em um único conjunto de objetos de dados (sem simultaneidade ou problemas com vários usuários). Para cada transação efetiva no banco de dados, o controle de versão é executado apenas nos registros de dados que teriam sido alterados efetivamente.

Quanto à cola de interface, ela terá a forma de uma função de interoperação simples entre o banco de dados e o VCS. Em termos de design, uma abordagem simples seria uma interface orientada a eventos, com atualizações de dados do banco de dados acionando os procedimentos de controle de versão (dica: assumindo Mysql, uso de triggers e sys_exec () blá blá ...). Em termos de complexidade de implementação, vai desde o simples e eficaz (por exemplo, scripts) até o complexo e maravilhoso (alguma interface de conector programada). Tudo depende de quão louco você quer ir com isso e quanto capital de suor você está disposto a gastar. Acho que um script simples deve fazer a mágica. E para acessar o resultado final, as várias versões de dados, uma alternativa simples é preencher um clone do banco de dados (mais um clone da estrutura do banco de dados) com os dados referenciados pela versão tag / id / hash no VCS. novamente, este bit será um simples trabalho de consulta / tradução / mapa de uma interface.

Ainda há alguns desafios e incógnitas a serem enfrentados, mas suponho que o impacto e a relevância da maioria deles dependerão em grande parte dos requisitos do aplicativo e dos casos de uso. Alguns podem acabar não sendo problemas. Alguns dos problemas incluem a correspondência de desempenho entre os 2 módulos principais, o banco de dados e o VCS, para um aplicativo com atividade de atualização de dados de alta frequência, escalonamento de recursos (armazenamento e poder de processamento) ao longo do tempo no lado git como os dados e usuários crescer: estável, exponencial ou eventualmente platô

Do coquetel acima, aqui está o que estou preparando no momento

  • usando Git para o VCS (inicialmente considerado o bom e velho CVS devido ao uso de apenas changesets ou deltas entre as 2 versões)
  • usando mysql (devido à natureza altamente estruturada de meus dados, xml com esquemas xml estritos)
  • brincar com o MongoDB (para tentar um banco de dados NoSQl, que se aproxima muito da estrutura de banco de dados nativa usada no git)

Alguns fatos divertidos - git realmente faz coisas claras para otimizar o armazenamento, como compressão e armazenamento de apenas deltas entre a revisão de objetos - SIM, git armazena apenas changesets ou deltas entre revisões de objetos de dados, onde é aplicável (ele sabe quando e como) . Referência: packfiles, no fundo do interior do Git - Revisão do armazenamento de objeto do git (sistema de arquivos endereçável por conteúdo), mostra semelhanças impressionantes (da perspectiva do conceito) com bancos de dados noSQL, como mongoDB. Novamente, às custas de suor de capital, pode fornecer possibilidades mais interessantes para integrar o 2 e ajustes de desempenho

Se você chegou até aqui, deixe-me se o acima pode ser aplicável ao seu caso, e supondo que seria, como se enquadraria em alguns dos aspectos em sua última análise de desempenho abrangente


4

Implementei uma biblioteca Ruby em cima da libgit2qual torna muito fácil de implementar e explorar. Existem algumas limitações óbvias, mas também é um sistema bastante libertador, pois você obtém o conjunto de ferramentas git completo.

A documentação inclui algumas idéias sobre desempenho, compensações, etc.


2

Como você mencionou, o caso de vários usuários é um pouco mais complicado de lidar. Uma solução possível seria usar arquivos de índice Git específicos do usuário, resultando em

  • sem necessidade de cópias de trabalho separadas (o uso do disco é restrito a arquivos alterados)
  • sem necessidade de trabalho preparatório demorado (por sessão de usuário)

O truque é combinar a GIT_INDEX_FILEvariável de ambiente do Git com as ferramentas para criar commits do Git manualmente:

Segue um esboço da solução (hashes SHA1 reais omitidos dos comandos):

# 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>

Dependendo dos seus dados, você pode usar um cron job para mesclar os novos refs, mastermas a resolução do conflito é sem dúvida a parte mais difícil aqui.

Ideias para tornar mais fácil são bem-vindas.


Geralmente, essa é uma abordagem que não leva a lugar nenhum, a menos que você queira ter um conceito completo de transação e interface do usuário para resolução manual de conflitos. A ideia geral para conflitos é fazer com que o usuário resolva diretamente no commit (ou seja, "desculpe, outra pessoa editou aquele documento que você estava editando -> por favor, veja as edições dele e suas edições e junte-as"). Quando você permite que dois usuários façam commit com sucesso e então descobre em um cronjob assíncrono que as coisas deram errado, geralmente não há ninguém disponível para resolver as coisas.
GreyCat de
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.