使用 git 存储库作为数据库后端

我在做一个关于结构性文件数据库的项目。我有一个类别树(大约1000个类别,每个级别大约50个类别) ,每个类别包含几千个(大约10000个)结构化文档。每个文档都是一些结构化形式的几千字节的数据(我更喜欢 YAML,但它也可以是 JSON 或 XML)。

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

  • 通过身份证检索这些文件
  • 通过文档中的一些结构化属性搜索文档
  • 编辑文档(即添加/删除/重命名/合并) ; 每个编辑操作应该记录为一个带有注释的事务
  • 查看特定文档的记录更改历史(包括查看谁、何时和为什么更改了文档,获取更早的版本——如果需要,还可能恢复到这个版本)

当然,传统的解决方案是使用某种类型的文档数据库(比如 CouchDB 或 Mongo)来解决这个问题——然而,这个版本控制(历史)的东西让我产生了一个疯狂的想法——为什么我不应该使用 git存储库作为这个应用程序的数据库后端呢?

乍一看,问题可以这样解决:

  • 种类 = 目录,文档 = 文件
  • 通过 ID 获取文档 = > 更改目录 + 读取工作副本中的文件
  • 使用编辑注释编辑文档 = > 由各种用户提交 + 存储提交消息
  • History = > 普通 git 日志和检索旧事务
  • Search = > 这是一个稍微有点棘手的部分,我想它需要定期导出一个类别到关系数据库中,并对我们允许搜索的列进行索引

这个解决方案中还有其他常见的缺陷吗?是否已经有人尝试过实现这样的后端(例如,对于任何流行的框架—— RoR、 node.js、 Django、 CakePHP) ?这个解决方案是否对性能或可靠性有任何可能的影响——也就是说,是否证明了 git 比传统的数据库解决方案慢得多,或者存在任何可伸缩性/可靠性缺陷?我假设一组这样的服务器,它们推送/拉动彼此的存储库,应该是相当健壮和可靠的。

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

32207 次浏览

确实是个有趣的方法。我会说,如果你需要存储数据,使用数据库,而不是原始码储存库,这是为非常具体的任务而设计的。如果您可以使用开箱即用的 Git,那么没问题,但是您可能需要在它上面构建一个文档存储库层。所以你也可以在传统的数据库上构建它,对吗?如果您感兴趣的是内置的版本控制,为什么不使用 开源文档存储库工具呢?有很多选择。

那么,如果您决定无论如何都要使用 Git 后端,那么基本上,如果您按照所述实现它,它就可以满足您的需求。但是:

1)你提到了“相互推拉的服务器集群”——我已经考虑了一段时间,但还是不确定。作为一个原子操作,你不能推动/拉动几个回购协议。我想知道在并发工作期间是否有可能出现一些合并混乱。

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

回答我自己的问题并不是最好的做法,但是,由于我最终放弃了这个想法,我想分享一下在我的案例中起作用的基本原理。我想强调的是,这个基本原理可能并不适用于所有的情况,所以这取决于架构师的决定。

一般来说,我的问题忽略的第一个要点是,我正在处理并行工作的 多用户系统多用户系统,并发地使用我的服务器和瘦客户机(即只是一个 Web 浏览器)。这样,我必须为他们所有人维护 国家。有几种方法可以解决这个问题,但是它们要么对资源来说太难,要么太复杂而无法实现(因此有点扼杀了最初将所有难以实现的东西卸载到 git 上的初衷) :

  • “钝化”方法: 1 user = 1 state = 1个服务器为用户维护的存储库的完整工作副本。即使我们讨论的是相当小的文档数据库(例如,100s MiB) ,用户数大约为100K,维护所有用户的完整存储库克隆也会使磁盘使用量飙升(即100K 用户乘以100MiB ~ 10TiB)。更糟糕的是,每次克隆100个 MiB 存储库需要几秒钟的时间,即使是以相当有效的方式完成(即不使用 git 和解包-重新包装的东西) ,这是不可接受的,IMO。更糟糕的是ーー我们应用到主树上的每个编辑都应该被拉到每个用户的存储库中,也就是说(1)占用资源,(2)在一般情况下可能导致未解决的编辑冲突。

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

  • “只有活动用户”方法: 只为活动用户维护工作副本。这种方法通常不存储每个用户完整的回收克隆,而是:

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

    因此,在这种情况下,光盘的使用高峰在 O (编辑次数 & 时间; 数据 & 时间; 活动用户的数量) ,这通常是约100。比总用户数少1000倍,但它使登录/退出更复杂和更慢,因为它涉及到在每次登录时克隆每个用户的分支,并在注销或会话过期时拉回这些更改(这应该通过事务处理 = > 添加另一层复杂性)。从绝对数字来看,它将磁盘使用量的10个 TiB 降低到了10个。在我的例子中是100 GB,这可能是可以接受的,但是,再一次,我们现在讨论的是相当 很小的100 MB 数据库。

  • “稀疏结帐”的方法: 使“稀疏结帐”而不是完整的回购克隆每个活跃用户并没有很大的帮助。它可以节省大约10倍的磁盘空间使用,但代价是在涉及历史的操作上更高的 CPU/磁盘负载,这有点扼杀了这个目的。

  • “工人池”方法: 我们可以保留一个“工人”克隆池,以备使用,而不是每次都为活跃用户做全面的克隆。这样,每次用户登录时,他占用一个“ worker”,从主回购中拉出他的分支,当他退出时,他释放“ worker”,聪明的 git 硬重置再次成为一个主回购克隆,准备供另一个登录的用户使用。虽然对光盘的使用没有多大帮助(使用率仍然相当高,每个活动用户只能完全复制) ,但至少它使得登录/退出更快,代价是更加复杂。

也就是说,请注意我有意计算了相当小的数据库和用户基数: 100K 用户,1K 活动用户,100MiB 总数据库 + 编辑历史,10MiB 工作副本。如果你看看更多著名的众包项目,你会发现数字要高得多:

│              │ 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 等。
  • 尽管我已经搜索了很多内容,但仍然没有任何现有的免费代码库可以用来从流行的框架中获取这种方法。
  • 至少有一个服务以某种方式有效地做到了这一点ーー那就是 Githubーー但遗憾的是,它们的代码库是封闭源代码的,我强烈怀疑它们在内部没有使用普通的 git 服务器/repo 存储技术,也就是说,它们基本上实现了替代的“大数据”git。

因此,底线: 是可能的,但是对于大多数当前用例来说,它不会接近最佳解决方案。将您自己的 document-edit-history-to-SQL 实现或尝试使用任何现有的文档数据库可能是更好的选择。

我的两便士。有点渴望,但是..。在我的一个孵化项目中也有类似的要求。与您的类似,我的关键需求在于一个文档数据库(在我的例子中是 xml) ,具有文档版本控制。它适用于具有大量协作用例的多用户系统。我倾向于使用支持大多数关键需求的可用开源解决方案。

直截了当地说,我找不到任何一个同时提供这两种功能的产品,其可伸缩性足够(用户数量、使用量、存储和计算资源)。我偏向于 git 的所有有前途的能力,以及(可能的)解决方案,一个人可以制定出来。随着我越来越多地使用 git 选项,从单个用户视角转移到多个(毫米)用户视角成为一个明显的挑战。不幸的是,我没有像你一样做实质性的性能分析。( ..懒惰/提前退出... ... 对于版本2,咒语)权力给你!.无论如何,我的偏见的想法已经转变为下一个(仍然偏见)的替代方案: 一个网状的工具,是最好的在他们各自的领域,数据库和版本控制。

虽然仍然在进行中(... 和稍微忽略)的变形版本是简单的。

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

从本质上说,这相当于向数据库添加一个版本控制插件,并使用一些集成粘合剂,这可能需要开发,但可能要容易得多。

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

至于接口粘合剂,它将采用数据库和 VCS 之间简单的互操作函数的形式。在设计方面,简单的方法是事件驱动接口,数据库中的数据更新触发版本控制过程(提示: 假设 Mysql,使用触发器和 sys _ exec ()等等)。就实现的复杂性而言,它的范围从简单而有效(如脚本)到复杂而精彩(一些编程的连接器接口)。这一切都取决于你有多疯狂,你愿意花多少汗水资本。我认为简单的脚本应该有奇效。要访问不同数据版本的最终结果,一个简单的替代方法是使用 VCS 中的 version 标记/id/hash 引用的数据填充数据库的克隆(更像是数据库结构的克隆)。同样,这个位将是一个接口的简单查询/翻译/映射作业。

仍然有一些挑战和未知因素需要处理,但我认为其中大部分的影响和相关性将在很大程度上取决于您的应用程序需求和用例。有些可能只是最终没有问题。一些问题包括两个关键模块(数据库和 VCS)之间的性能匹配,用于高频数据更新活动的应用程序; 随着时间的推移,随着数据的增长,Git 方面的资源(存储和处理能力)的扩展,以及用户的增长: 稳定的、指数级的,或者最终是平台级的

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

  • 在 VCS 中使用 Git (由于在两个版本之间只使用变更集或增量,因此最初认为旧版本的 CVS 很好)
  • 使用 mysql (由于我的数据具有高度结构化的特性,xml 具有严格的 xml 模式)
  • 玩玩 MongoDB (尝试使用 NoSQL 数据库,它与 git 中使用的本机数据库结构非常匹配)

一些有趣的事实 Git 实际上在优化存储方面做了很多工作,比如压缩,以及在对象修订之间只存储增量 - 是的,git 只存储数据对象修订之间的变更集或增量,它适用于哪里(它知道何时和如何)。参考文件: packfiles,在 Git 内部的内核的深处 回顾 git 的对象存储(内容寻址的文件系统) ,显示了与 noSQL 数据库(如 mongoDB)惊人的相似性(从概念的角度来看)。同样,以牺牲汗水资本为代价,它可能为集成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的新引用,但是冲突解决可以说是这里最困难的部分。

让事情变得简单的想法是受欢迎的。

我在 libgit2之上实现了一个 Ruby 库,这使得它非常容易实现和探索。虽然存在一些明显的局限性,但是由于您获得了完整的 git 工具链,所以它也是一个相当自由的系统。

文档包括一些关于性能、权衡等的想法。