147

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

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

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

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

乍一看,可以这样解决:

  • 类别 = 目录,文档 = 文件
  • 通过 ID 获取文档 => 更改目录 + 读取工作副本中的文件
  • 使用编辑评论编辑文档 => 由各种用户提交 + 存储提交消息
  • 历史 => 正常的 git 日志和旧事务的检索
  • search => 这是一个稍微棘手的部分,我想它需要定期将类别导出到关系数据库中,并为我们允许搜索的列建立索引

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

基本上,告诉我这个解决方案是否有效以及为什么它会或不会有效?

4

5 回答 5

66

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

一般来说,我的问题遗漏的第一个要点是我正在处理并行、并发工作的多用户系统,使用我的服务器和一个瘦客户端(即只是一个 Web 浏览器)。这样,我必须维护所有这些状态。有几种方法可以解决这个问题,但所有这些方法要么在资源上太难,要么太复杂而无法实现(因此,首先将所有硬实现的东西卸载到 git 的初衷就被扼杀了):

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

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

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

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

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

  • “稀疏结帐”方法:为每个活跃用户进行“稀疏结帐”而不是完整的 repo 克隆并没有多大帮助。它可能会节省大约 10 倍的磁盘空间使用量,但代价是在涉及历史记录的操作上会产生更高的 CPU/磁盘负载,这会破坏目的。

  • “工人池”方法:我们可能会保留一个“工人”克隆池,以备使用,而不是每次都为活跃的人进行完整的克隆。这样,每次用户登录时,他都会占用一个“工人”,将他的分支从主仓库拉到那里,当他注销时,他释放了“工人”,这使得聪明的 git hard reset 再次成为一个主要的 repo 克隆,可供其他登录用户使用。对磁盘使用没有多大帮助(它仍然很高——每个活动用户只能完全克隆),但至少它使登录/注销更快,​​因为更复杂。

也就是说,请注意我故意计算了相当小的数据库和用户群的数量: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。

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

于 2014-06-12T04:43:07.787 回答
13

我的 2 便士值。有点渴望,但是……我的一个孵化项目也有类似的要求。与您的类似,我的关键要求是文档数据库(在我的情况下为 xml),带有文档版本控制。它适用于具有大量协作用例的多用户系统。我的偏好是使用支持大多数关键要求的可用开源解决方案。

切入正题,我找不到任何一款产品能够同时提供这两种功能,并且具有足够的可扩展性(用户数量、使用量、存储和计算资源)。我偏向于 git,因为它具有所有有前途的功能,并且(可能的)解决方案可以从中制作出来。当我更多地玩弄 git 选项时,从单一用户视角转变为多(毫)用户视角成为一个明显的挑战。不幸的是,我没有像您那样进行实质性的性能分析。(.. 懒惰/早点退出....对于第 2 版,口头禅)给你力量!无论如何,我有偏见的想法已经演变为下一个(仍然有偏见的)替代方案:在各自的领域、数据库和版本控制中最好的工具的网格组合。

虽然仍在进行中(......并且略微被忽视),但变形版本就是这样。

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

从本质上讲,这相当于向数据库添加一个版本控制插件,使用一些集成胶水,您可能需要开发它,但可能要容易得多。

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

至于接口胶水,它将是数据库和VCS之间的简单互通函数的形式。在设计方面,简单的方法是事件驱动接口,来自数据库的数据更新触发版本控制程序(提示:假设 Mysql,使用触发器和 sys_exec() blah blah ...)。就实现的复杂性而言,它将从简单有效的(例如脚本)到复杂而精彩的(一些编程的连接器接口)。一切都取决于你想用它有多疯狂,以及你愿意花多少汗水资本。我认为简单的脚本应该可以发挥作用。为了访问最终结果,各种数据版本,一个简单的替代方法是使用 VCS 中版本标记/id/hash 引用的数据填充数据库的克隆(更多是数据库结构的克隆)。同样,这一位将是一个简单的接口查询/翻译/映射作业。

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

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

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

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

如果您做到了这一点,请告诉我上述是否适用于您的情况,并假设适用,它将如何与您上次综合性能分析中的某些方面相匹配

于 2014-06-28T10:12:30.370 回答
12

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

好吧,如果您还是决定使用 Git 后端,那么如果您按照描述实现它,基本上它会满足您的要求。但:

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

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

于 2013-11-23T22:43:46.040 回答
5

我在上面实现了一个Ruby 库libgit2,这使得它非常容易实现和探索。有一些明显的限制,但它也是一个非常自由的系统,因为您获得了完整的 git 工具链。

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

于 2017-03-10T09:15:14.003 回答
3

正如您所提到的,多用户案例处理起来有点棘手。一种可能的解决方案是使用特定于用户的 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 作业将新的 ref 合并到其中,master但冲突解决可以说是这里最难的部分。

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

于 2016-10-20T13:26:43.890 回答