@cursor_ai: 我们正在让Git托管变得更可靠、高性能和可扩展。这篇文章追溯了20年的Git基础设施,并……

X AI KOLs Timeline 产品

摘要

Cursor AI 介绍了如何通过重新设计其Git存储系统 Origin,使其像数据库一样运作,来提升Git托管的可靠性、性能和可扩展性。

我们正在让Git托管变得更可靠、高性能和可扩展。 这篇文章追溯了20年的Git基础设施,并解释了这段历史如何促使我们设计并运作Git存储系统Origin,使其如同数据库一般。 https://t.co/UW7jHuItSX
查看原文
查看缓存全文

缓存时间: 2026/08/18 22:35

我们正致力于让 Git 托管服务变得更可靠、更高效、更具扩展性。

本文回溯了 20 年来的 Git 基础设施演进历程,并阐述这段历史如何引导我们将 Git 存储服务 Origin 设计和运维得如同数据库一般。

https://t.co/UW7jHuItSX


规模化Git

来源:https://cursor.com/blog/git-at-any-scale 大规模托管 Git 仓库堪称一场噩梦。当 Linus Torvalds 设计地狱级信息管理器的首个版本时(没错,这确实是 Git 的原始宣传语,请查证 https://github.com/git/git/commit/e83c5163316),他心中有非常明确的用例:满足自身需求。他希望替代用于开发 Linux 内核的分布式版本控制系统 BitKeeper。当然,替代方案也必须是分布式的。内核是一个独特的软件项目;它具有极高的去中心化特性,其众多子系统由不同的维护者管理。分布式版本控制系统天然契合这种工作模式。

二十年后的今天,Git 已成为行业标准,但事实证明,其分布式特性在某种程度上弊大于利。普通开源软件项目并不采用去中心化工作流。普通企业当然更不会如此。他们利用分布式模型的诸多优势(例如离线工作、延迟推送等),但非常依赖中心化的托管服务。而事实证明,托管 Git 仓库是一项极其艰巨的任务。

https://cursor.com/blog/git-at-any-scale#whats-hard-about-gitGit的挑战何在?

大规模托管 Git 仓库的挑战源于 Git 自身的设计:作为分布式版本控制系统,意味着仓库的所有副本都是相同的。Git 服务器上的仓库与开发者笔记本电脑上的仓库并无本质区别。虽然乍看之下这似乎使 Git 仓库托管变得简单(只需在磁盘上的仓库副本前部署一个 HTTP 守护进程就能搭建 Git 服务器),但实际上存在诸多扩展性与可靠性挑战,使得情况截然相反。

在常规 Git 仓库中,你的代码和元数据(文件、提交、树)被压缩存储在打包文件中——这是一种简洁的二进制序列化格式,在本地机器上处理很方便,但在服务器端大规模管理时却并不理想。打包文件是 Git 存储和Git 网络传输的基础构建模块。当你向仓库推送或获取数据时,数据都是以打包文件的形式传输的。

这虽是 Git 的设计初衷,但认为必须如此未免武断。毕竟,你无法控制 Git 客户端(除非不惜惹恼用户并增添诸多操作摩擦),但在服务器内部,你可以为所欲为。没有任何规定强制你必须使用打包文件——Linus 不会来检查。唯一的限制是:所有 Git 操作通过网络传输时,确实需要发送和接收打包文件。

多年来,尝试大规模托管 Git 仓库的企业发现,这种基于打包文件的设计对可用性和扩展性构成了重大限制。打包文件是必须存在于文件系统上的大型二进制文件,Git 才能访问它们。仅在磁盘仓库前部署 HTTP 服务器这种简单方案存在明显瓶颈。理想情况下,你希望仓库存在于多块磁盘和多台服务器上(这能并行执行多项 Git 操作,并在服务器宕机时保持仓库可用)。但如何实现呢?

实现这一目标大致有三种方法,复杂度依次递增:分布式文件系统、分布式打包文件,或分布式 Git 本身。

https://cursor.com/blog/git-at-any-scale#git-without-packfiles无打包文件的Git

Git 是一个内容寻址的数据存储。Git 仓库中的所有对象(二进制大对象、树、提交等)均以其内容的 SHA-1 哈希值作为键。这直观上非常契合分布式键值存储(键是 SHA-1,值是实际对象),可为仓库存储扩展提供清晰方案。但这实际上行不通。

问题在于:Git 仓库的实际布局是有向无环图(简称 DAG)。你可以通过 SHA 值查找任何对象,但要执行仓库中最基础的操作,必须逐步遍历 DAG。

COMMIT DAGTREE /main → c8f3?commit·c8f3NETWORK↓ OLDER COMMITSobjects 0/54 · round‑trips 0key/value storeaa42a112c8f3e816f0214b70e8c487ab19b4d5c277b22dc83f7dc43040c22d6e21aa729a0db50f627cf191fe3e81b19092d080a5f31134e06c811f6e7a196e42e147b9086a70ce192ee45d83f7a9b02ec2a70c49b8e25ac074b1a93d47dd9b519d2a8c14d431dc31ef09e205

例如要列出仓库最近的变更,必须处理其提交记录。处理提交时,会获得指向其树根的指针。通过这个树,可获取指向每个文件和子树的指针。从原始提交中,还能获得指向其父提交(历史中前一个提交)的指针。关键在于,在这种遍历的每个步骤中,你都必须在获取前一个指针后,才能知道下一个指针的值。如果每次获取都需要分布式存储的一次往返传输,性能消耗会急剧增加。

这种在对象层分布式 Git 的方法已被多次尝试,往往在扩展性上失败。最有前景的尝试来自我以前的导师 Shawn Pearce 在 Google 版本控制系统团队的工作。他的方法是将对象存储在分布式哈希表中 (https://www.eclipse.org/lists/jgit-dev/msg01189.html)。这得益于 JGit——一个 Java 实现的定制 Git 库。如同所有优秀的 Java 库,JGit 提供了足够的接口和工厂来抽象普通 Git 仓库的所有细节,包括用分布式哈希表替换磁盘上的打包文件。虽然该系统运行正常,对常规 Git 操作的性能尚可,但 Git 协议的限制(无论服务器端如何存储数据,网络传输仍要求打包文件)导致 git clone 性能极差,最终该方案被弃用。

https://cursor.com/blog/git-at-any-scale#github-and-filesystemsGitHub与文件系统

Git 开始突破 Linux 内核圈几年后,一家初创公司在旧金山诞生。GitHub 成立于 2008 年,作为社交编程平台,其口号极具前瞻性:“Git 仓库托管:不再是麻烦事。”我并未开玩笑,请查证 (https://web.archive.org/web/20080514210148/http://github.com/)。早在 2008 年,业界已形成广泛共识:尽管(或者说正因为)Git 的分布式设计,实际需要中心化方式托管 Git 仓库才能使其易于使用,而这过程极其痛苦。GitHub 立志改变现状。

其平台最初(至今大部分仍是)是一个 Rails 单体应用。最初版本运行在单台(虽然性能强劲)服务器上,配备 Ruby 服务器和磁盘上的仓库副本。扩展 Rails 应用很简单:部署更多实例。但在此特殊情况下,由于涉及 Git,他们很快面临我们试图解决的这个反复出现的问题:如果 Rails 应用需要访问磁盘上的 Git 仓库,如何部署更多副本?

作为节俭的极客团队,GitHub 早期系统工程师尝试了最简单的解决方案来应对扩展挑战。他们的思路是,如果专注于分布式文件系统(而非打包文件或 Git 本身),就能保持 Rails 应用不变,将时间用于为不断增长的用户群开发更多功能,而非折腾 Git 的底层结构。非常务实,但行不通。

团队尝试了多种分布式文件系统方案:最明显的方法——使用 NFS 将所有仓库存储在中心化服务器上——很快被否定。默认的 Git 实现对文件系统语义(锁、撕裂、读取、同步等)做了大量假设,这些假设确保了在缓慢的开发者笔记本本地文件系统上的合理性能,但完全未考虑网络文件系统下的行为表现。该方案既慢又不稳定。

后续还尝试了(坦白说,回顾起来相当可怕)块级复制文件系统的技术。短暂部署过 GFS (https://en.wikipedia.org/wiki/GFS2)。更长久的部署基于 DRBD (https://en.wikipedia.org/wiki/DRBD)。它们都碰壁了。这些方案日常运维极其痛苦,且性能补偿不足。这一切都归结于磁盘上打包文件的设计。

我们已了解 Git 的图状数据结构导致往返传输代价高昂。不幸的是,类似原则也适用于底层磁盘数据。DAG 中对象的布局与它们在打包文件中的存储方式毫无关联。生成打包文件时的关键启发式原则是最小化体积:对象被随机放置、压缩存储,且关键的是很少完整存储。大多数对象以同一打包文件中另一个对象的增量形式存储。读取单个对象时,在经历图数据结构中的多个逻辑跳转后,还需经历磁盘格式中的物理跳转。

COMMIT DAGTREE /HEAD·mergecommit·c8f3root /tree·f021server.tsblob·f7a9pack.tsblob·a112README.mdblob·c430Cargo.tomlblob·ef09main~1commit·9d2aroot /tree·e8c4server.tsblob·d431pack.tsblob·b8e2README.mdblob·21aaCargo.tomlblob·92d0featurecommit·74b1root /tree·7a19server.tsblob·b02epack.tsblob·7cf1README.mdblob·5d83Cargo.tomlblob·e147merge basecommit·5ac0root /tree·2d6eserver.tsblob·8c14pack.tsblob·0f62README.mdblob·a93dCargo.tomlblob·3e81refactorcommit·3f7droot /tree·6c81server.tsblob·e205pack.tsblob·19b4README.mdblob·6a70Cargo.tomlblob·d5c2parsercommit·aa42root /tree·91feserver.tsblob·4b70pack.tsblob·f311README.mdblob·2dc8Cargo.tomlblob·80a5docscommit·ce19root /tree·0db5server.tsblob·729apack.tsblob·6e42README.mdblob·b190Cargo.tomlblob·47ddbootstrapcommit·87abroot /tree·40c2server.tsblob·1f6epack.tsblob·c2a7README.mdblob·9b51Cargo.tomlblob·34e0initialcommit·2ee4root /tree·b908server.tsblob·dc31pack.tsblob·77b2README.mdblob·e816Cargo.tomlblob·0c49↓ OLDER COMMITSpack‑7d9a.pack00005041434B00000002000000369667706B001087CB98F791BBB1A90DFB95AEA39C9096002008C2A7180F97ACF200DBBC9098D5BBD10030B5A94CB2E61004EC96B098E17C9BE5B700404A19E5C49FA44FE62B3225C446E744410050C5EB8990E84A607C5C5A3D9996324D5F0060633898968A2B4DF3066395013955B4960070A15A6898EA976BE046C3ACDE98A86F5E00803BE84A97E68450D47936985069F5B25F00902FBDE5D4C28ECFE63DB350B871E7F33A00A0BAF109D0E8F7FF556E74DAB7E6E2E71E00B058DD984BFA29344429F2E52F415382E600C0C71AD04782E703FECCE51940E862FA0600D004826A0596C94054F246984BD218C3AC00E04B2C9506ED4A02967A5071F17D9793A000F01B73670B98B79D58807A0B14E6DC5E5301002FEF9814BBB9A6E8617EE5D4B43D5AE601108478CF63F0E7F3D8BA541492E8E8FCF701205929E078E6629739693698F03127F80B0130295BE516B79BE8E68A9D1BCD23E744210140B1A5543CE88D7CD27941A14CE6C614EB0150F0E1982CE621266DD61CE5CECCF7DEE60160212997678DE7FD3C7B66EFF2E85F579E0170C2D51BAEE51554918799BF9B3F89732901808F7ED201FA87C2FD

这种跨越数 GB 数据的随机遍历(仓库的每个 Git 操作都必须执行)在网络文件系统(无论文件级还是块级复制)上难以高效运行。唯一能避免严重减速的方法是将整个文件缓存到本地。但当同一文件系统中存在数十万个仓库时,缓存方案不可行。

最终,GitHub 系统工程师屈服于现实,放弃了分布式文件系统。他们开始开发 RPC 系统,使仓库能存在于专用文件服务器上,并更新 Rails 应用以远程执行所有操作。这提供了良好的横向扩展能力,但并未解决可用性问题,也未提升最繁忙仓库的性能。毕竟,每个仓库仍仅存储在单台服务器上。

https://cursor.com/blog/git-at-any-scale#spokes-and-consistencySpokes与一致性

Spokes 最初由 GitHub 于 2013 年左右开发,现已成为行业标准。大多数 Git 托管服务都在架构中采用了 Spokes 的变体(Git 仓库的应用层复制)。Spokes 多年来表现良好的主要原因是其做出了三项基本选择,这些选择经过时间验证已被证明是最优的:

  1. 不分布 Git 本身,而是在打包文件层工作。
  2. 将所有数据以实际 Git 仓库形式存储在本地 NVMe 磁盘上。
  3. 复制 Git 数据,但保持所有副本始终完全同步。

由于我们刚讨论过的跨打包文件的随机读取模式,将原始 Git 仓库存储在 NVMe 驱动器上基本上是确保所有基础 Git 操作保持快速的必要条件。同时这也保持了克隆效率,因为无需将数据转换为 Git 客户端期望的格式。这还让你能专注于在 Git 之上构建产品,而非维护能操作特殊仓库的 Git 分叉版本。

关键的是,保持所有数据副本始终完全同步也极为重要。这一点你只有亲身经历才会明白,Git 客户端确实无法与最终一致性良好协作。如果本地 Git 客户端推送提交后立即无法通过获取读取到该提交,那就是坏消息。Git 会感到非常困惑。如果你在百个运行器上运行 CI 管道,其中三个运行器在克隆仓库后找不到待测试的提交,也是坏消息。这同样是非常糟糕的用户体验。

无论是在客户端还是后端,基于 Git 仓库的最终一致性视图操作存在诸多风险。因此,Spokes 付出了极高的复杂度成本来确保系统始终保持完全一致。让我们具体分析其含义。

Spokes 是一个基于共识的分布式系统。它通过在不同服务器上存储 Git 仓库的多个副本来工作。当你推送新数据时,协调器会将你的推送分发,使仓库的每个副本都接收一份拷贝。这种“分发“通过经典的三阶段提交(3PC)共识算法实现同步 (https://en.wikipedia.org/wiki/Three-phase_commit_protocol),仅当多数节点确认后,推送才会被接受。

QUORUM · 4/5tx #42RESTORE ALLCOORDINATORcollecting votesPARTICIPANT 1waitingPARTICIPANT 2waitingPARTICIPANT 3offlinePARTICIPANT 4waitingPARTICIPANT 5waiting1 · VOTING2 · PRE‑COMMIT3 · DO COMMIT

在深入探讨 Spokes 如何使用 3PC 之前,我们需要了解 Git 推送的工作原理。Git 推送包含两个组成部分:打包文件和引用事务。打包文件(前文已讨论)包含你推送到仓库的对象(包含你变更的二进制大对象、树和提交)。事务则通过更新一个或多个引用(例如你工作的分支)指向你刚推送的提交,从而将你的变更实际发布到仓库。

这种分离在此场景中非常方便,因为推送的提交最初并不可见(Git 术语中称为“可达“)

相似文章

Cursor Origin(1分钟阅读)

TLDR AI

Cursor Origin 是一个兼容 Git 的平台,专为 AI 代理规模的开发而设计,旨在通过优化并行 AI 代理工作流来与 GitHub 竞争。