在 Cursor 发布这篇文章的前一天,GitHub 刚刚经历了一场持续近 8 小时的全球性宕机,API、Actions、Pull Requests、Copilot 等核心服务的错误率一度飙到 20%,仓库下载的错误率更是达到了 50%。
根据 GitHub 随后发布的根因报告,是美国中部数据中心的负载均衡器网络饱和,Istio sidecar pod 因策略配置错误未能自动扩容,导致四个 HAProxy 节点的流量上限被打满,认证链路全面降级。更为要命的是,VS Code 内部一个端点的延迟触发了一个潜在的重试 bug,使得 Copilot Token Service 的流量从正常的 7,000-9,000 RPS 暴增到 70,000-100,000 RPS,直接拖慢了恢复进度。
而 Cursor 团队也非常会抓时机,在同一天推出了自己的代码托管平台 Origin。随后,其 Git 存储团队的工程师 Vicent Marti 发表了一篇名为《任意规模的 Git》的深度长文。
关于 Vicent,他本人在 GitHub 工作了十年,并亲历了 Git 托管基础设施的演进全过程。
在这篇文章中,他回顾了 Git 基础设施过去 20 年的发展历程,解释了为什么大规模托管 Git 仓库是一场噩梦,GitHub 的 Spokes 系统是怎么解决一致性问题的、又在哪里遇到了瓶颈,以及 Cursor 如何设计了一套全新的存储系统 Continuity,将 Git 仓库的事实来源从磁盘转移到了 S3 上的预写日志,从而实现了完全一致的水平扩展能力。
原文:Git at any scale, https://cursor.com/blog/git-at-any-scale
作者:Vicent Marti 日期:2026 年 8 月 18 日
◇ ◆ ◇
大规模托管 Git 代码仓库是一场噩梦。
Linus Torvalds 设计「来自地狱的信息管理器」第一个版本的时候(这其实就是 Git 的标语,可以查证),心里有一个非常具体的使用场景:他自己。
他想要一个替代 BitKeeper 的方案,而 BitKeeper 当时正被用于开发 Linux Kernel 的分布式版本控制系统,替代方案自然也得是分布式的。Linux Kernel 是一个不同寻常的软件项目:它高度去中心化,众多维护者分别负责各自的子系统。分布式版本控制天然适合这样的工作流。
二十年后,Git 已经是行业标准了。但事实上,它的分布式特性带来的阻碍多于优势。普通的开源项目并不采用去中心化的工作流,普通公司更是如此。它们享受分布式模型带来的诸多好处,比如可以离线工作、延后推送等,但同时也高度依赖集中式的托管平台。而事实证明,托管 Git 代码仓库是一件极其困难的事。
大规模托管 Git 仓库所面临的挑战,根源在于 Git 本身的设计:作为一种分布式版本控制系统,仓库的所有实例都是完全相同的。Git 服务器上的仓库和开发者笔记本电脑上的仓库没有本质区别。乍看之下,这似乎让托管变得很简单(只需在磁盘上的仓库前面部署一个 HTTP 守护进程,就能跑一个 Git 服务器了),但实际上,许多严峻的可扩展性和可靠性挑战让情况恰恰相反。
在普通 Git 仓库中,你的代码和元数据(文件、提交和树对象)会被压缩存储在 packfile 里。packfile 是一种简单的二进制序列化格式,在本地机器上处理起来很方便,但放到服务器上进行大规模管理就完全是另一回事了。packfile 是 Git 存储以及网络通信的基础构件。当你向仓库推送数据或从中拉取数据时,数据都会以 packfile 的形式传输。
Git 的设计就是如此,但认为它其实不必如此也是合理的。毕竟你没法控制 Git 客户端(至少没法在不惹恼用户、不增加大量使用阻力的前提下做到),但在自己的服务器内部,你可以做任何想做的事。没有什么规定你必须使用 packfile,Linus 不会亲自来检查。唯一的限制是,对于所有 Git 操作,你确实需要通过网络接收和发送 packfile。
多年来,尝试大规模托管 Git 仓库的公司发现,这种基于 packfile 的设计严重限制了可用性和可扩展性。packfile 是大型二进制文件,Git 必须通过文件系统来访问它们。在磁盘上的仓库前面部署 HTTP 服务器这种简单做法,扩展能力的上限很低。理想情况下,你会希望仓库存在于多块磁盘和多台机器上(这样可以并行运行大量 Git 操作,并在服务器挂掉时保持仓库可用)。但问题是,该怎么实现呢?
大致有三种方式,复杂度依次递增:分布文件系统、分布 packfile,或者分布 Git 本身。
Git 是一个内容寻址的数据存储。仓库中的所有对象(blob、tree、commit 等)都以其内容的 SHA-1 作为键。这天然适合映射到分布式键值存储上(键为 SHA-1,值为实际对象),似乎能以一种简洁的方式来扩展仓库的存储能力。但实际上行不通。
问题在于:Git 仓库的实际结构是一个有向无环图(DAG)。你可以通过 SHA 查找任何对象,但即使是仓库中最简单的操作,也必须逐步遍历这个 DAG。
DAG 遍历如果要列出仓库的近期变更,就必须处理其中的 commit。处理一个 commit 时,你会拿到一个指向其 tree 根节点的指针。从这个 tree 中,你会拿到指向每个文件和子树的指针。从原始 commit 中,你还会拿到指向其父 commit 的指针(也就是历史记录中位于它之前的 commit)。关键在于,遍历的每一步中,必须先获取前一个指针,才能知道下一个指针的值。如果每次获取都需要与分布式存储进行一次网络往返,成本很快就会变得极其高昂。
这种在对象级别分布 Git 的方法此前已被多次尝试,但往往难以在大规模场景下奏效。最有前景的一次实现,是我的前导师 Shawn Pearce 在 Google 版本控制团队工作时做的尝试。他的方法是将对象存储在分布式哈希表中。这得益于 JGit,一个以 Java 编写的自定义 Git 实现。和经典的 Java 库一样,JGit 提供了大量接口、工厂和接口工厂,可以抽象普通 Git 仓库的所有细节,包括用 DHT 替换磁盘上的 packfile。虽然这套系统能够运行,对于常规 Git 操作来说效果也还不错,但 Git 协议的限制(不管你在服务器上怎么存数据,它仍然要求通过网络发送 packfile)使得 git clone 的性能差到最终不得不放弃这一设计。
Git 开始走出 Linux Kernel 小圈子的几年后,一家充满干劲的初创公司在旧金山诞生了。GitHub 于 2008 年成立,是一个社交化编程平台,标语极具远见:「Git 代码仓库托管:不再让人痛不欲生。」我不是在开玩笑,你可以查证。早在 2008 年,人们就已形成广泛共识:尽管(或者正因为)Git 采用分布式设计,要以集中式方式托管 Git 仓库才能让它易于使用,而这件事非常痛苦。GitHub 决心改变这一点。
它的平台最初是(如今大部分仍是)一个 Rails 单体应用。最早的版本跑在一台性能强劲的机器上,上面运行着 Ruby 服务器,仓库副本则存放在同一台机器的磁盘上。扩展 Rails 应用很简单:部署更多实例就行。但在这个特定场景中,由于涉及 Git,他们很快就遇到了我们正在试图解决的老问题:如果 Rails 应用需要访问磁盘上的 Git 仓库,该怎么部署更多副本?
作为一群精打细算的怪才,GitHub 早期的系统工程师尝试了最简单的、或许能解决扩展问题的方法。他们的想法是,如果专注于分布式处理文件系统(而不是 packfile 或 Git 本身),就可以保持 Rails 应用不变,把时间花在为不断增长的用户群交付更多功能上,而不是对 Git 搞些奇技淫巧。非常务实的想法。结果并不奏效。
团队尝试了许多用于存储 Git 数据的分布式文件系统方案:最显而易见的一种是使用 NFS 将所有仓库存储在一台集中式服务器上,但很快就被放弃了。Git 的默认实现对文件系统语义(锁定、撕裂、读取、同步……)做了大量假设,这些假设能确保在本地文件系统上获得不错的性能,却完全没考虑它们在网络文件系统上会怎么样。又慢又到处是 bug。
之后,他们还尝试了在今天看来坦率说相当可怕的技术:在块级别复制文件系统。一次短暂的 GFS 部署,以及一次持续时间更久的、基于 DRBD 的部署。全都撞墙了。日常运维体验糟糕透顶,性能也不足以弥补这一点。这一切归根结底都是因为磁盘上 packfile 的设计。
我们已经看到,Git 的图状数据结构如何让网络往返的成本变得高不可攀。不幸的是,一个非常类似的原理同样适用于底层的磁盘数据。DAG 中对象的布局与它们在 packfile 中的存放方式之间是没有关联的。生成 packfile 时的关键启发式目标是尽可能缩小它的体积:对象会被随机分布在整个 packfile 中,它们会被压缩,而且至关重要的是,它们很少以完整形式存储。大多数对象都是以同一 packfile 中另一个对象为基础的增量形式存储的。在图数据结构中经过多次逻辑跳转后,读取单个对象还需要在磁盘格式中经历多次物理跳转。
packfile 布局这种跨越数 GB 数据的随机遍历,在仓库上执行的每一项 Git 操作中都会发生,根本无法与网络文件系统良好配合(无论它是在文件级别还是块级别进行复制)。要避免慢到近乎停滞,唯一的办法是将整个文件缓存在本地。但同一文件系统中有数十万个仓库的时候,缓存是不可行的。
最终,GitHub 的系统工程师硬着头皮放弃了分布式文件系统。他们开始开发 RPC 系统,让仓库可以存放在专用文件服务器上,并改造 Rails 应用使所有操作都能远程执行。这带来了相当可观的水平扩展能力,但并没有解决可用性问题,也没有改善最繁忙仓库的性能。毕竟,每个仓库仍然只存储在一台机器上。
Spokes 最初由 GitHub 于 2013 年前后开发,如今已成为行业标准。大多数 Git 托管服务都在架构中采用了 Spokes 方案的某种变体(即针对 Git 仓库的应用层复制)。Spokes 能够长期有效运行,主要是因为它做出了三个根本性的选择,而这些选择后来被证明是最优的:
它不分发 Git 本身,而是在 packfile 层面运作。它将所有数据以真正的 Git 仓库形式存储在本地 NVMe 磁盘上。它复制 Git 数据,但始终让所有副本保持同步一致。
鉴于我们刚才讨论过的 packfile 随机读取模式,要确保所有基本 Git 操作保持快速,将原生 Git 仓库存储在 NVMe 驱动器上几乎是必需的。这样也能让克隆保持高效,因为无需将数据转换为 Git 客户端所需的格式。此外,你可以专注于在 Git 之上构建产品,而不必自行维护一个能够处理特殊仓库的 Git 分支版本。
让所有数据副本始终保持同步一致同样至关重要。这一点往往要吃过苦头才会明白:Git 客户端确实无法很好地应对最终一致性。如果本地 Git 客户端推送了一个 commit,却在 fetch 后立即读不到它,问题就严重了。Git 会对此非常困惑。如果你在一百个运行器上跑 CI 流水线,其中三个在克隆仓库后找不到本应测试的 commit,问题同样严重。这也会带来极差的用户体验。
无论是在客户端还是后端,面对 Git 仓库的最终一致性视图都会遇到许多棘手问题。因此,Spokes 不惜承担很高的复杂性成本,以确保系统始终完全一致。
Spokes 是一个基于共识的分布式系统。它会在不同服务器上存储你的 Git 仓库的多个副本。每当你推送新数据时,协调器都会将该推送扇出,使仓库的每个实例都收到一份副本。这种「扇出」通过一种名为 3PC(三阶段提交)的经典共识算法进行协调,因此只有在大多数节点确认后,推送才会被接受。
三阶段提交跟 Git 完全无关。它是一种共识算法,可以确保系统中的所有节点一致同意提交或回滚一项事务;整个过程需要三次往返。它与两阶段提交非常相似,但额外引入了「预提交」阶段,使系统能够在协调器于事务中途掉线时进行恢复。
三阶段提交在进一步讨论 Spokes 如何使用 3PC 之前,我们需要先了解 Git 推送的工作原理。一次 Git 推送包含两个组成部分:一个 packfile 和一个引用事务。前面已经介绍过的 packfile 包含要推送到仓库的对象(blob、tree,以及包含变更的 commit)。事务则通过更新一个或多个引用(例如正在处理的分支),使其指向刚刚推送的 commit,从而真正将变更发布到仓库。
这种分离在这里非常方便,因为在指向它的引用更新之前,推送的 commit 是不可见的(用 Git 术语来说,就是「不可达」的)。这意味着,我们可以先同时将 packfile 扇出到所有主机(此处无需同步),然后对引用事务执行三阶段提交,以此为推送实现共识。引用事务比 packfile 小得多,同步也就更快了。Git 本身支持准备引用事务:它可以锁定引用,验证现有值是否符合预期,然后一直持有该锁,直到收到该事务的提交或中止命令。
Spokes 会先分发 packfile,然后对每次推送的事务执行三阶段提交。
Spokes 推送通过这一设计,我们确保了每次推送都在所有副本间完全同步。随后,读取操作(fetch、clone)可以安全地路由至任意单个副本,因为每个副本始终都是最新的。
这基本上就是 Spokes 的工作方式了,过去 13 年来一直运行得相当不错。当然,Spokes 并不完美,没有任何系统是完美的。到了 2026 年,人们使用 Git 仓库的方式已经发生了巨大变化,而我们也在这个过程中积累了许多构建分布式系统的重要经验。时间和经验告诉了我们,Spokes 的哪些选择最终被证明是最优的,哪些则并非如此。
后来被证明至关重要的一个缺陷,是 3PC 的水平扩展能力有限。
Spokes 最初发布时,每个仓库三个副本是最佳平衡点。三个副本足以服务一般仓库,且还有富余容量;即使一台机器宕机,也有足够的冗余能够继续接受推送。
到了 2026 年,情况已经完全不同了。如今,企业的普通仓库已经是庞大的 monorepo。三个副本不足以承载这类仓库的流量了,尤其是 CI 流量。当然,除了令人畏惧的规模化尾延迟,没有什么因素会阻止 Spokes 用超过三个副本来运行。三阶段提交与 Git 事务模型的契合确实非常优雅,但作为一种共识算法,它存在根本性的限制:每一步的延迟都受集群中最慢服务器的制约。集群中的副本越多,推送吞吐量就越低。
这种扩展性限制在另一个方向上也同样存在。AI Agent 大规模使用 Git 仓库时,往往不会用 monorepo,而是创建海量的小型仓库,其中许多是一次性的,绝大多数几乎无人使用。Spokes 在这种场景下表现不佳,因为它仍然要求每个仓库都用三个副本。这三个大部分时间处于空闲状态的副本无法缩减,否则系统就不再完全一致,并可能发生数据丢失。对于三阶段提交来说,下限始终过高,上限又始终过低。
另一个缺陷,事先无法预见,却会在亲身经历后痛苦地显现出来:Spokes 在大规模运维时可能会相当棘手。
由于磁盘上的仓库始终是共识的事实来源,每个仓库的每个副本都至关重要。你必须将仓库视为「宠物」,而非「牲畜」。
这首先意味着,你需要确切知道每个仓库在哪里。这会引入对外部数据库的依赖(以及潜在的可用性问题);该数据库必须维护一张庞大的路由表,将每个仓库映射到存有其副本的每台机器。还必须为每个仓库计算校验和,并在表中持续更新,以确保仓库在磁盘上保持有效。一旦仓库出了问题(相信我,问题时常会出现的,Git 在实际使用中可能非常挑剔),你必须检测到问题并安排修复作业,使其恢复到健康状态。而且必须非常迅速地完成,因为磁盘上的仓库仍然是事实来源。损坏的副本与缺失的副本一样糟糕。如果三个副本中有两个损坏了,系统就无法再接受推送:法定人数不够了。
Continuity(简称 Cnt)是我们在 Cursor 开发的 Git 存储系统。其理念非常明确:借鉴 Spokes 的成功之处,并解决那些经过多年实践后我们已知的问题。
Cnt 是一个简单的系统(系统若不简单,就不可能易于运维)。其核心基础组件是预写日志(WAL),我们将其存储在兼容 S3 的对象存储中。在生产环境中,我们直接使用 S3,但它也可以部署在任何云环境中。
当仓库收到推送时,我们会将其作为 WAL 条目存储到 S3。在推送完全持久化之前,我们绝不会确认该推送。 每次推送都存储为独立对象;我们会将推送的 packfile 写入磁盘,同时上传到 S3。不过,上传 WAL 条目并不意味着将其发布。只有在仓库的本地副本上成功准备好引用事务,并在 WAL 索引文件中记录指向该 WAL 条目的指针后,推送才会可见;该索引文件本身也是存储中的一个对象。这使得所有推送都可线性化。
WAL 推送流程我们尽量避免每次推送只做一次 S3 写入,因为在繁忙的仓库中,这会因 S3 PUT 操作的延迟而为推送吞吐量设下硬性上限。借助精心调优的批处理实现,且只需将引用事务与单个本地仓库同步而无需与法定人数的副本同步,我们的系统可以按磁盘允许的最高速度来接收推送。
仓库的本地副本当然是存储在高速 NVMe 磁盘上的普通 Git 仓库。我们采用了和 Spokes 相同的做法,因为我认为 Spokes 在这一点上做得完全正确。这让我们能够复用 Git 社区出色的开源成果,包括上游 Git 客户端及其众多性能优化。我们因此能专注于交付新功能,而不是对 Git 做些奇怪的改造。
我们已经看到,Spokes 集群难以运维的一个原因是:你必须准确掌握每个仓库在各服务器上的位置。Cnt 的做法则截然不同。每个仓库位于哪里?答案是「任何地方」。这并不重要。我们将仓库视为磁盘上的热缓存,但唯一的事实来源始终是 S3 中的预写日志。系统是无状态的,没有路由表(也就无需运维关系型数据库了)。如果在某台主机上访问仓库时,本地磁盘上没有该仓库,我们就从 WAL 把它恢复出来。我们可以非常高效地完成这个操作,但当然不希望一直这么做,因为这会造成浪费。在生产环境中,我们使用 rendezvous hashing 将仓库 ID 映射到预期存放它的节点列表。路由仓库所需的全部状态,就是仓库 ID 和集群中当前健康节点的集合。但即使这些状态不同步(比如某个节点变得不健康),也完全没问题。我们只需在下一个节点上恢复该仓库就行了。
那共识呢?选举呢?某个仓库由哪台服务器当主节点?这同样不重要。这里没有状态,也没有共识。任何服务器都可以当主节点。对预写日志的所有更新都会通过 S3 上的原子比较并交换(CAS)操作来同步,因此任何仓库实例接收推送都始终是安全的。同样,和路由一样,让任意服务器当主节点并不是最高效的做法(这会导致 CAS 重试,从而可能延迟推送),因此在实践中,我们始终选择同一台服务器作为主节点,也就是 rendezvous hashing 排名列表中的第一台服务器。但在边缘情况下,比如发生部署、故障转移或网络短暂波动时,我们并不在意究竟哪台服务器是主节点。
CAS 竞争系统的设计原则是:降级时始终保持正确,健康时始终保持快速。
将预写日志存储在 S3 中,为系统扩展带来了无限可能。我们可以拥有任意数量的副本,因为 S3 的可扩展性无与伦比,所有副本都能直接从那里追赶进度。我们通过在集群中传播 UDP gossip 数据包来实现乐观复制。这些数据包包含每个副本在每次推送后直接从 S3 追赶进度所需的全部元数据。
「这太疯狂了,」我仿佛听见你隔着时空在屏幕后嘀咕道,「UDP 不是可靠的传输协议。」
当然不是。分布式系统中没有任何东西是可靠的。网络不可靠,路由不可靠,拓扑也不可靠。但没关系,因为这并不重要。每个副本都知道自己已追赶到的 WAL 索引最新版本的 ETag。当你在某个副本上执行读取操作时,我们会使用预期的 ETag 向 S3 发起条件 GET 请求。如果返回不带正文的 304 响应(这几乎是即时操作,平均不到 10ms,因为它只涉及 S3 元数据),就意味着副本已是最新状态,可以立即处理 fetch 或 clone 请求。如果返回 200 响应,则会附带 WAL 索引的最新版本;我们会先据此追赶进度,再处理读取请求。
即使复制用的 UDP gossip 数据包丢了,或因拓扑变化而到达了错误的服务器,也无关紧要。所有副本上的所有读取都是完全一致的,因为它们都会根据作为事实来源的 S3 来验证。
gossip 复制系统的设计原则始终是:降级时保证正确,健康时保持快速。
这带来了两方面的好处。由于系统始终保持一致,在其上构建基础设施就很简单了,我们(智能体、Web 界面和客户端)始终看到仓库的全局一致视图。而由于系统可以向两个方向扩展,每个仓库都能获得恰到好处的副本数量。大型 monorepo 可以部署到数百个副本上,以承载其 CI 任务产生的全部负载。由 Agent 创建的数百万个小型仓库则可各由一个副本来服务;由于 S3 是事实来源,我们不需要多个副本来确保可用性。实际上,空闲仓库甚至不需要副本:当某个副本一段时间没有收到流量时,我们会从节点磁盘中将其垃圾回收掉;下次收到 fetch 请求时,再直接从 WAL 将其恢复出来就好了。
预写日志需要定期压缩。不能任由日志无限增长:完整恢复时需要重放每一条记录,记录越多成本就越高。
巧合的是,普通 Git 仓库也需要定期压缩,尽管 Git 并不基于 WAL。我们已经看到,Git 仓库的基本存储单元是 packfile。每次向仓库的远程副本推送,或从远程副本拉取到本地时,都会创建一个新的 packfile。这样下去是行不通的:每个 packfile 都有自己的索引,Git 可以借此高效查找其中包含的对象,但这种查找效率仅限于单个 packfile。如果要查找某个特定对象,而仓库中有 100 个 packfile,你就需要逐一打开它们的索引并查找该对象,直到在其中一个里找到。一项操作即使本身高效,如果必须执行数百或数千次,那也谈不上高效了。
现代 Git 已经非常擅长规避这个问题了,现在支持多 pack 索引和增量几何压缩。但最终还是得对磁盘上的 Git 仓库进行重新打包。过去,这一直是 Spokes 等系统的可用性问题,因为重新打包是一项非常消耗 CPU 的操作,即使以增量方式执行也是如此,并且必须在系统的所有副本上都执行。对于同一仓库,如果意外在两个或更多 Spokes 节点上触发了维护操作,很容易导致仓库发生故障转移。
在我们的系统里,压缩成本被分摊到了多个操作中。只有主节点执行压缩,压缩结果会同时应用于磁盘上的仓库以及 WAL。由于所有副本都会跟随 WAL,它们也就跟随了压缩事件。副本不需要执行重新打包;它们只需从 S3 下载已压缩的 pack,用带宽换取 CPU。
几何压缩复制和压缩是决定 Git 存储系统在负载下性能的两个关键因素。正如刚才所见,它们密切相关:仓库每秒接收的推送越多,读取性能下降得越明显,因为必须压缩每次推送产生的 packfile 才能保持 Git 操作的高效性。如果要复制这些推送,就必须复制压缩结果,或在每个副本上独立执行压缩。
Continuity 的 WAL 优先设计提供了完全一致的水平扩展能力:你可以部署任意数量的副本,只读 Git 操作的吞吐量会随副本数量线性增长。由于集群中的所有副本都完全一致,我们不仅能扩展 Git 协议(克隆、fetch),还能扩展 Origin 在仓库之上执行的所有 RPC 操作(Web UI 交互、REST API、所有 Agent 接口等)。
我们已经使用多达 100 个副本进行了合成压力测试,观察到读取性能稳定地线性扩展,且推送吞吐量没有任何回归。
克隆吞吐量
推送吞吐量集群的推送吞吐量取决于我们更新 S3 上 WAL 的延迟。使用 S3 标准版时,我们可以在压缩数据并将压缩后的数据复制到所有其他节点的同时,持续处理最高 120 次推送/秒。我们还在 S3 Express One Zone 上部署了高性能集群,其 PUT 操作延迟低得多。在该环境中,我们可以接收超过 300 次推送/秒,实际瓶颈在于 Git 压缩磁盘上数据的速度。 我们正在探索创新的数据磁盘布局方式,以降低压缩的影响:我们的目标是在不降低严格的持久性和一致性保障的前提下,持续优化 Git 仓库接收代码的速度。
S3 是一项出色的技术。由 S3 API 开创的对象存储理念,已经成为大型数据存储系统中极其强大的基础构件,这一点同样适用于托管 Git 仓库。这里介绍的设计在许多方面都很新颖,但并非首个将 packfile 放到对象存储里的方案。Azure DevOps(微软自家的 GitHub 竞争产品)拥有一套非常成功的 Git 存储系统:将 packfile 存储在对象存储中,并将引用存储在关系型数据库(MS SQL Server)中。此类系统存在许多权衡。关系型数据库能够很好地扩展以处理大型引用事务。但相应地,你也必须运维关系型数据库。我们坚信 Git 数据的一致性比任何其他因素都更重要。正是这一点让我们最终选择设计了一个不依赖外部数据库、基于 WAL 的系统。
生产环境中的 Git 仓库可能出现很多问题:静默数据损坏、重新打包时的缺陷、推送期间的竞争条件。这其中充满了各种边缘情况。大多数问题已在 Git 上游得到了修复,但并非全部。没有任何系统毫无缺陷,即使是开源且广泛部署的系统也不例外。我们的一致性模型确保能够跟踪仓库中发生的每一项基础操作。在推送完全持久化到 WAL 之前,我们绝不会确认该推送。我们会将所有推送线性化。我们访问的每个仓库的每个视图始终完全一致。
由于每次推送都记录在了 WAL 中,我们可以查看仓库曾经经历的每个状态。我们拥有所有推送和重新打包操作的完整溯源数据。我们可以回退或快进每个副本。我们无需与任何外部数据库同步状态,无论该数据库是只存储引用,还是存储所有对象数据。当(不是如果)我们遇到 Git 中的缺陷时,都能精确定位发生了什么并将其还原。除了 Git 中已有的缺陷外,我们引入的新缺陷极少,因为在整个过程中,所有 Git 操作都是使用现成工具链在磁盘上的普通 Git 仓库中执行的。
我们深知,托管他人的源代码责任重大。我相信,读过并理解了这篇博客文章的每个人都同样清楚这一点。如果开发者无法向其 Git 仓库推送代码或从中拉取代码,公司就可能陷入停摆。CI 系统宕机五分钟造成的生产力损失很难用金额来衡量,但无论从哪个角度看,都是一笔巨大的损失。
Agent 从根本上改变了我们使用软件的方式,而且在许多方面让这一问题变得更加严峻:更多代码、更多 PR、更多 CI 运行。版本控制是这一切的核心,也可能是最难在短时间内改变的系统。
过去数月,我们在 Cursor 内部一直面临着这些挑战,并投入了大量心力来构建一个能为我们解决这些问题、也希望能为客户解决这些问题的平台。我们当前的重点是提供尽可能顺畅的迁移路径,帮助你获得更高的可靠性、更佳的性能和更大的规模,同时让迁移过程尽可能轻松无痛。
Origin 并非一项实验。它汇聚了在构建这类系统方面拥有数十年经验的人士的成果,他们深刻理解其中挑战的艰巨性。我们拥有经实践证明行之有效的工程与运营理念,并坚定承诺会随着版本控制领域的发展持续演进这一理念。
我们希望你能信任我们和我们的平台:Origin。

◇ ◆ ◇
https://cursor.com/blog/git-at-any-scale