一年半之前,我有一个 ROGit 项目计划,旨在构建一个 cgit 同生态位的现代替代品
R(Read or Rust) O(Only or Online) Git
得益于技术栈发展,cgit 所使用的 CGI 协议已相当少见。ROGit 相比 cgit 有以下几个目标:
- 现代 CSS 界面及交互设计
- 避免 Git 源码依赖
- 灵活的 API 接口及扩展
当前正在做后端部分,遇到了严重的性能问题:解析 aports 仓库需要 3 个小时(3 分钟是可以接受的)。
PS:这里加上火焰图
Git Object #
理论上讲 Git 中有三个大类别的对象:Commit、Tree、Blob,具体到解析时还有 Commit 的同类 Tag 及 Note。这些对象以各自 object 的 hash 作为索引,经 zlib 压缩后保存在 .git/objects
下的文件中,纯正文件系统驱动,并在需要时被 Git 解析。这种 NO-SQL 设计保证了后续一定会有性能问题(
Git Commit #
提交 Commit 指向自己的历史来源,有 0 个、1 个、2 个或者更多个 Parent Commit。不同的数量表示了这次提交的不同种类,是 root 节点还是 fast-forward 这种单线链表,或者是合并分支的 Merge 节点,超过 2 的情况不多见,是特殊的合并节点,比如内核的提交中出现的:
https://github.com/torvalds/linux/commit/9b25d604182169a08b206306b312d2df26b5f502
当然像提交信息、提交人、提交时间、以及 Committer 与 Author 之间的区别这里不再赘述。Commit 还保存了提交时的文件(树)快照,指向当前 Commit 相随的 Tree。
这就是一个 Commit 对象信息的全部。
Git Log #
Git 并没有在 Commit 中描述这次提交修改了哪些文件,所以若要知晓给定文件的最后修改日期,过程较为艰难:
- 获取 Commit 与第一个 Parent Commit 的不同 Tree
- 在两个 Tree Object 中遍历,查找修改的文件
- 检查给定文件是否在本次 Diff 中修改
- 重复上述操作,直到两次提交的树之间存在期望差异
在没有对应数据结构的情况下进行类似 SQL 的结构化查询相当消耗性能,面对稍微大一点的仓库,时间来到秒级
# aports.git
$ time git diff --quiet 20752720e52 38d77a246d4
real 0m0.007s
user 0m0.005s
sys 0m0.003s
(为什么 Git 优化这么好,libgit2 版本就没这么快,后面再看看)
Git Blame #
Blame 完全是 Log 的细节版本,不仅找到相关的提交,而且按照行定位每行的最后修改提交,类似糖豆人的蜂巢关卡,直到完成对目标文件当前所有行的修改提交定位,终止遍历。
所以这里有个 ddos 的小技巧,请求一个不存在的文件,Git 不得不遍历所有提交定位最终修改(但是似乎大家普遍缓存了每个文件的最终修改提交,所以可能没用)
Git 生态 #
尽管如 GitHub、GitLab、Gitea、Forgejo、Sourcehut等近现代托管平台欣欣向荣,但 Git 现存底层生态相当贫瘠
Libgit2 #
这是经典老牌实现库,由 C 语言实现。虽然官网写了几十种语言绑定,但真正积极维护的屈指可数,目前只推荐 C(本家)、Rust(Cargo 这边需要,不确定会不会搬到 Gitoxide)、Python(Fedora 和 Sourcehut 在用,不确定未来是否搬走)、Ruby(GitHub 和 GitLab 从前的技术栈,GitLab 已经搬走)这些语言。
尽管功能丰富,但诸如前文所提到的 git-diff
问题,以及与之相关的 git-log
、git-blame
等操作并无很好解决办法,许多功能存在能跑这一阶段,性能上与原版 Git 存在 2~10 倍的差距。
Gitaly #
GitLab 公司不思进取,没有优化相关算法,反而用 GO 把原版 Git 命令完完全全包装成了 RPC 远程调用,舍弃了 libgit2 相关的所有实现。目前 Gitee(中国代码托管平台)也在使用这一开源组件。
Gitoxide #
Rust 从头开始的实现,RIIR
Gogit #
Pure Go 从头开始的实现,关注度、使用人数、实现完成度、以及最重要的性能还不如 libgit2。
Git #
很多情况下性能最好的一种方法,但点名批评 GitLab、Gitea、Gogs,把命令行调用包装起来,除了结果缓存,没有任何优化。
ROGit #
近半年来切换到 Rust 语言生态,在除 Git 缓存速度较慢外,不存在技术问题(就是要解决这个问题),目前决定对以下功能不做支持:
- git-note
- git-blame
针对 git-refs --contains <commit>
也没有考虑到较优美的解决办法,准备同样调用 Git 命令并解析输出。
已完成对配置文件的解析、对 sqlite 数据库的重新设计、对 commit 对象的遍历与更新、以及 tag 对象与 commit 对象的抽象等工作。下一步继续构思 git-log 相关加速:
- 使用 sqlite 作为 libgit2 的自定义后端(已排除:经过火焰图排查,完全缓存 ODB 的情况下,仅仅会带来约 40% 的加速,无法进一步提升)
- 实现独立解析并缓存 Tree 对象及 TreeEntry 至数据库,实现基于数据库的
git_diff_tree_to_tree()
并在请求时动态查询缓存提交变动文件 - 偷学代码:Git、Sourcehut、Gitaly、git-sizer、fossil(sqlite 家的 CVS 系统)
相关链接: