使用git仓库作为数据库后端


119

我正在做一个处理结构化文档数据库的项目。我有一棵类别树(〜1000个类别,每个级别上多达〜50个类别),每个类别包含数千个(最多,例如〜10000个)结构化文档。每个文档都是某种结构化形式的几千字节的数据(我更喜欢YAML,但也可能是JSON或XML)。

该系统的用户执行几种类型的操作:

  • 通过ID检索这些文档
  • 通过文档中的某些结构化属性搜索文档
  • 编辑文件(即添加/删除/重新命名/合并);每个编辑操作都应记录为带有一些注释的事务
  • 查看特定文档记录的更改的历史记录(包括查看更改文档的人员,时间和原因,获取较早的版本-如果需要,可以还原为该版本)

当然,传统解决方案将使用某种文档数据库(例如CouchDB或Mongo)来解决此问题-但是,此版本控制(历史记录)使我产生了一个疯狂的主意-为什么我不应该将git存储库用作此应用程序的数据库后端?

乍一看,可以这样解决:

  • 类别=目录,文档=文件
  • 通过ID获取文档=>更改目录+读取工作副本中的文件
  • 使用编辑注释编辑文档=>由不同的用户进行提交+存储提交消息
  • 历史=>正常的git日志和旧事务的检索
  • search =>这是一个比较棘手的部分,我想这需要定期将类别导出到关系数据库中,并为列提供索引,以便我们通过

此解决方案还有其他常见陷阱吗?有没有人尝试过实现这样的后端(例如,对于任何流行的框架-RoR,node.js,Django,CakePHP)?该解决方案是否会对性能或可靠性产生任何潜在影响?即,是否证明git会比传统数据库解决方案慢得多,或者存在任何可伸缩性/可靠性陷阱?我认为,这种推/拉彼此的存储库的服务器集群应该相当健壮和可靠。

基本上,告诉我,如果这个解决方案将工作和为什么它会或不会做?


Answers:


58

回答我自己的问题并不是最好的办法,但是当我最终放弃这个想法时,我想分享适用于我的案例的基本原理。我想强调一下,这种基本原理可能并不适用于所有情况,因此,由架构师决定。

通常,我的问题遗漏的第一个要点是,我要处理的是使用瘦客户机(即Web浏览器)的服务器并行,并行工作的多用户系统。这样,我必须保持所有状态。有几种方法,但是所有这些方法要么太耗费资源,要么太复杂以致于无法实现(因此一开始就杀死了将所有困难的实现工作卸载到git的最初目的):

  • “直截了当”的方法:1个用户= 1个状态= 1个服务器为用户维护的存储库的完整工作副本。即使我们谈论的是约100K用户的相当小的文档数据库(例如100s MiB),维护所有用户的完整存储库克隆也会使磁盘使用率上升(即100K用户乘以100MiB〜10 TiB) 。更糟糕的是,即使以相当有效的方式(即不使用git和拆包-重新包装的东西使用)进行操作,每次克隆100个MiB存储库也要花费几秒钟的时间,这是不可接受的,IMO。更糟糕的是,我们应用于主树的每个编辑都应拉到每个用户的存储库中,这是(1)资源消耗,(2)在一般情况下可能导致未解决的编辑冲突。

    基本上,就磁盘使用而言,它可能与O(编辑数×数据×用户数)一样糟糕,并且这种磁盘使用情况自动意味着相当高的CPU使用率。

  • “仅活动用户”方法:仅为活动用户维护工作副本。这样,您通常不会存储每个用户的完整存储库,而是:

    • 当用户登录时,您将克隆存储库。每个活动用户需要几秒钟和大约100 MiB的磁盘空间。
    • 当用户继续在站点上工作时,他将使用给定的工作副本。
    • 当用户注销时,他的存储库克隆将作为一个分支复制回主存储库,因此仅存储他的“未应用的更改”(如果有的话),这是相当节省空间的。

    因此,在这种情况下,光盘使用量达到O(编辑数×数据×活动用户数)的峰值,通常比总用户数少100..1000倍,但它使登录/注销更加复杂且速度较慢,因为它涉及到在每次登录时克隆每个用户分支,并在注销或会话到期时撤回这些更改(这应该通过事务完成=>增加了另一层复杂性)。从绝对数量来看,在我的情况下,它会将10 TiB的磁盘使用量降低到10..100 GiB,这是可以接受的,但是,再次,我们现在谈论的是100 MiB的相当小的数据库。

  • “稀疏签出”方法:对每个活动用户进行“稀疏签出”而不是全面的repo克隆并没有太大帮助。它可能节省约10倍的磁盘空间使用量,但会以较高的CPU /磁盘负载为代价来进行涉及历史的操作,而这会破坏目的。

  • “工人池”方法:我们可能会保留一个可供使用的“工人”池池,而不是每次都为活跃的人进行全面的克隆。这样,每次用户登录时,他都会占用一个“工作人员”,从主存储库中拉出他的分支,并且在注销时,他释放了“工作人员”,这确实使git硬重置为聪明一个主要的回购克隆,准备供其他用户登录使用。对磁盘使用率没有多大帮助(仍然很高-每个活动用户只有完整的克隆),但至少可以使登录/注销速度更快,这是因为更加复杂。

就是说,请注意,我有意计算了相当小的数据库和用户群的数量:100K用户,1K活跃用户,100 MiB的总数据库+编辑历史记录,10 MiB的工作副本。如果您查看更杰出的众包项目,那么数量会更多:

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

显然,对于如此大量的数据/活动,这种方法将是完全不能接受的。

通常,如果可以将Web浏览器用作“加厚型”客户端,那将是可行的,即发出git操作并在客户端(而不是服务器)上存储几乎所有的完整签出内容。

我还错过了其他一些要点,但与第一个要点相比还不错:

  • 就普通的ORM(例如ActiveRecord,Hibernate,DataMapper,Tower等)而言,具有“较厚”用户的编辑状态的这种模式是有争议的。
  • 正如我一直在寻找的那样,从流行的框架中进行git的这种方法的现有免费代码库为零。
  • 至少有一种服务可以通过某种方式设法有效地做到这一点-显然是github-但可惜的是,它们的代码库是封闭源代码,我强烈怀疑它们内部没有使用普通的git服务器/ repo存储技术,即它们基本上已实现替代“大数据” git。

因此,底线:它可能的,但对于目前大多数usecases它不会是最优解附近的任何地方。汇总自己的文档编辑历史记录到SQL的实现或尝试使用任何现有的文档数据库可能是一个更好的选择。


16
可能要晚一些参加聚会,但是我对此有类似的要求,实际上是沿着git-route进行的。在深入研究git内部之后,我找到了一种使其工作的方法。这个想法是使用裸仓库。有一些缺点,但是我发现它是可行的。我已将所有内容写在您可能想查看的文章中(如有需要,请关注):kenneth-truyers.net/2016/10/13/git-nosql-database
Kenneth

我不这样做的另一个原因是查询功能。文档存储区经常对文档建立索引,从而使它们易于搜索。使用git不会很简单。
FrankyHollywood

12

确实是一个有趣的方法。我想说的是,如果您需要存储数据,请使用数据库,而不是为特定任务设计的源代码存储库。如果您可以直接使用Git,那很好,但是您可能需要在其上构建文档存储库层。因此,您也可以在传统数据库上构建它,对吗?如果您感兴趣的是内置版本控制,为什么不只使用一种开源文档存储库工具呢?有很多选择。

好吧,如果您决定无论如何都要使用Git后端,那么按照说明实现它基本上可以满足您的要求。但:

1)您提到“相互推/拉的服务器集群”-我已经考虑了一段时间,但仍不确定。您不能将多个存储库作为原子操作推/拉。我想知道在并发工作期间是否可能发生合并混乱。

2)也许您不需要它,但是未列出的文档存储库的一个明显功能就是访问控制。您可能可以通过子模块限制对某些路径(=类别)的访问,但是可能无法轻松授予文档级别的访问权限。


11

我的2便士值。有点渴望,但是……我的一个孵化项目也有类似的要求。与您相似,我的主要要求是文档数据库(本例中为xml)以及文档版本控制。它用于具有许多协作用例的多用户系统。我更喜欢使用支持大多数关键需求的可用开源解决方案。

顺带一提,我找不到任何一种产品可以提供足够的可扩展性(用户数量,使用量,存储和计算资源)同时提供这两种产品。 (可能)可以从中得出解决方案。随着我更多地使用git选项,从单用户角度转到多(milli)用户角度成为一个明显的挑战。不幸的是,我没有像您那样进行大量的性能分析。(..懒惰/早点退出..对于版本2,咒语)强大的力量!无论如何,我有偏见的想法此后演变为下一个(仍然有偏见的)替代方案:在各自领域,数据库和版本控制方面最好的工具组合而成。

虽然仍在进行中(...并略有忽略),但变形版本就是这个。

  • 在前端:(用户界面)将数据库用于第一级存储(与用户应用程序接口)
  • 在后端,使用版本控制系统(VCS)(例如git)对数据库中的数据对象执行版本控制

从本质上讲,这相当于在数据库中添加一个版本控制插件,并带有一些集成胶,您可能需要开发这些胶,但是要容易得多。

(应该)如何工作是主要的多用户界面数据交换是通过数据库进行的。DBMS将处理所有有趣且复杂的问题,例如多用户,并发e,原子操作等。在后端,VCS将对单个数据对象集执行版本控制(无并发或多用户问题)。对于数据库上的每个有效事务,仅对将被有效更改的数据记录执行版本控制。

至于接口胶水,它将采用数据库和VCS之间的简单接口功能。在设计方面,一种简单的方法是事件驱动的接口,其中来自数据库的数据更新将触发版本控制过程(提示:假定为 Mysql,使用触发器和sys_exec() 就实现的复杂性而言,它的范围从简单有效(例如脚本)到复杂而奇妙(某些已编程的连接器接口)。一切都取决于您要花多少钱,以及愿意花多少汗水。我认为简单的脚本应该可以发挥作用。为了访问最终结果(各种数据版本),一种简单的替代方法是使用VCS中的tag / id / hash版本引用的数据填充数据库的克隆(更多是数据库结构的克隆)。同样,该位将是接口的简单查询/翻译/映射作业。

仍然存在一些挑战和未知数,但是我想其中的影响和相关性将在很大程度上取决于您的应用程序需求和用例。有些可能最终成为非问题。其中的一些问题包括数据库和VCS这两个关键模块之间的性能匹配(针对具有高频数据更新活动的应用程序),在git端随时间推移作为数据的资源规模(存储和处理能力)以及用户增长:稳定,指数或最终达到平稳

在上面的鸡尾酒中,这是我目前正在酿造的

  • 将Git用于VCS(由于仅使用2版本之间的变更集或增量,因此最初被认为是良好的旧CVS)
  • 使用mysql(由于我的数据具有高度结构化的特性,因此具有严格的xml模式的xml)
  • 与MongoDB一起玩耍(尝试使用NoSQl数据库,该数据库与git中使用的本机数据库结构紧密匹配)

一些有趣的事实-git实际上确实做了一些事情来优化存储,例如压缩和仅存储对象修订版之间的增量-是,git确实仅存储数据对象修订版之间的变更集或增量,它在哪里适用(它知道何时以及如何)。参考资料:packfiles,深入Git内部 -回顾git的对象存储(可寻址内容的文件系统),显示了与noSQL数据库(例如mongoDB)的惊人相似之处(从概念角度来看)。同样,以牺牲大量资金为代价,它可能会提供更多有趣的可能性来整合2和性能调整

如果到此为止,让我看看以上是否适用于您的情况,并假设适用于您的情况,那么它将如何与您上一次全面性能分析中的某些方面保持一致


4

我在的顶部实现了一个Ruby库libgit2,这使得这很容易实现和探索。有一些明显的局限性,但由于您拥有完整的git工具链,因此它也是一个相当自由的系统。

该文档包含有关性能,折衷等方面的一些想法。


2

如您所提到的,多用户案例有些棘手。一种可能的解决方案是使用特定于用户的Git索引文件,从而导致

  • 无需单独的工作副本(磁盘使用仅限于更改的文件)
  • 无需费时的准备工作(每个用户会话)

诀窍是将Git的GIT_INDEX_FILE环境变量与工具结合起来以手动创建Git提交:

解决方案概述如下(命令中省略了实际的SHA1哈希):

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

根据您的数据,您可以使用cron作业将新引用合并到 master但是冲突解决在这里可能是最难的部分。

欢迎提出使之更容易的想法。


通常,这是一事无成的方法,除非您希望拥有用于手动冲突解决的完整的事务和UI概念。冲突的一般想法是让用户在提交时立即解决它(即“抱歉,其他人编辑了您正在编辑的文档->请查看他的编辑和您的编辑并合并它们”)。当您允许两个用户成功提交内容,然后在异步cronjob中发现问题已经解决时,通常没有人可以解决问题。
GreyCat
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.