← 文章 / 开源项目
Hacker News 17小时前 · 2026-08-21 14:50:28 · 6 阅读

任意规模下的 Git

博客 / 研究

大规模托管 Git 仓库是一场噩梦。当年 Linus Torvalds 设计第一版来自地狱的信息管理器(这其实是 Git 的官方标语,不信去查)时,他心里有一个非常明确的使用场景:他自己。他想用 Git 取代 BitKeeper——当时用于 Linux 内核开发的分布式版本控制系统。当然,新方案也必须是分布式的。Linux 内核是一个与众不同的软件项目,它极度去中心化,不同子系统由不同的维护者分别管理。分布式版本控制系统天然契合这种工作流。

二十年后,Git 已成为行业标准,但事实是,它的分布式特性带来的更多是阻碍而非优势。普通的开源软件项目并不按去中心化的方式运作,普通公司更不会。它们虽然享受着分布式模型的种种便利(比如离线工作、延迟推送等),但本质上还是高度依赖一个中心化托管平台。而托管一个 Git 仓库,实际上是一件极其困难的事。

#Git 难在哪里?

大规模托管 Git 仓库的挑战,根植于 Git 本身的设计:作为一种分布式版本控制系统,仓库的每个实例都是完全相同的。服务器上的仓库与开发者笔记本上的仓库没有任何区别。虽然乍看之下这让 Git 仓库的托管似乎很简单(只需在一个磁盘仓库副本前架个 HTTP 守护进程,Git 服务器就开张了!),但实际上,其中涉及大量棘手的可扩展性和可靠性难题,远没那么轻松。

在普通的 Git 仓库中,你的代码和元数据(文件、提交、树对象)会被压缩后存放在包文件(packfiles)中——这是一种简单的二进制序列化格式,在本地机器上处理起来很方便,但在大规模服务器端管理时却并不理想。包文件既是 Git 存储的基石,也是 Git 网络通信的基石。每当你向仓库推送或拉取数据时,传输的都是包文件。

Git 的设计本身就是这样,但要认为「必须如此」也并非没有道理。毕竟,客户端你管不了——至少在不惹恼用户、增加大量摩擦的前提下做不到——但在你自己的服务器内部,你想怎么来就怎么来。没有什么强制你必须用 packfile,Linus 也不会跑来检查。唯一的限制是:在网络传输中,所有 Git 操作收发的数据必须以 packfile 形式进行。

多年来,那些尝试大规模托管 Git 仓库的公司逐渐意识到,这种基于 packfile 的设计严重制约了可用性和可扩展性。Packfile 是体积较大的二进制文件,Git 访问它们时必须放在文件系统上。简单地在磁盘上的仓库前面架一个 HTTP 服务器,这种方式天花板很低。理想情况下,你希望仓库分布在多块磁盘和多台机器上(这样可以并行处理大量 Git 操作,并在服务器宕机时保持仓库可用)。但该怎么做呢?

大致有三种可行方案,复杂度依次升高:分布式文件系统、分布式 packfile,或者把 Git 本身分布式化。

#抛弃 packfile 的 Git

Git 是一个内容寻址的数据存储系统。Git 仓库中的所有对象(blob、tree、commit 等)都以内容的 SHA-1 作为键。这天然就非常契合分布式键值存储(键是 SHA-1,值是实际对象),本可以提供一种简洁的仓库存储扩展方案。但这条路实际上走不通。

问题在于:Git 仓库的真实结构是一个有向无环图(DAG)。你可以按 SHA 查找到任意对象,但要执行哪怕最简单的操作,都必须一步步遍历 DAG。

想要在仓库中列出最近的变更,就必须处理它的提交。处理提交时,你会拿到指向其树对象的根节点指针;顺着这棵树,又能拿到指向各个文件和子树的指针;而从原始提交出发,还能拿到指向其父提交(即历史中前一条)的指针。关键在于,这个遍历过程的每一步,只有取回前一个指针之后,你才能知道下一个指针的值。如果每一次取值都要往返一次分布式存储,开销会急剧膨胀。

在对象层面把 Git 分布式化的做法,过去已经被尝试过很多次,而且几乎都无法扛住规模考验。其中最接近成功的一次,是我的前导师 Shawn Pearce 在 Google 版本控制系统团队工作时做出的:他把对象存储在一张分布式哈希表里。这一方案能成立的前提是 JGit——一个用 Java 写的定制版 Git 实现。和所有经典的 Java 库一样,JGit 提供了足够多的接口、工厂以及工厂的工厂,把普通 Git 仓库的种种细节都抽象掉,包括用分布式哈希表替换磁盘上的 packfile。虽然这套系统能跑起来,也能满足日常 Git 操作的需求,但 Git 协议本身的限制——不管服务器怎么存数据,往线上传的终归还是 packfile——让 git clone 的性能差到无法接受,整个方案最终被放弃。

#GitHub 与文件系统

在 Git 开始走出 Linux 内核圈子的几年后,一家充满干劲的初创公司在旧金山诞生了。2008 年,GitHub 作为社交化编程平台上线,配了一句颇有先见之明的标语——"Git 仓库托管:不再让人头疼"。这可不是我编的,自己去看。早在 2008 年,人们就已经普遍认同:尽管 Git 天生是分布式的(或许正因如此),大家其实还是需要一个中心化的方式来托管 Git 仓库,才能真正好用;而这件事,做起来非常痛苦。GitHub 决心要改变这种状况。

他们的平台最初(至今主体仍是)是一个 Rails 单体应用。最早的版本运行在一台配置相当强悍的单机上,上面跑着 Ruby 服务器,仓库直接以文件形式存放在本地磁盘。Rails 应用的水平扩展很简单:多部署几个实例就行。但这个场景比较特殊,因为涉及到 Git,所以很快撞上了本文要回答的那个老问题:如果 Rails 应用要访问磁盘上的 Git 仓库,怎么把仓库也一并部署到多台机器上?

早期那批系统工程师都是些讲究务实的"怪人",他们先试了最简单的那条路,想着如果能把文件系统(而不是 packfile 或 Git 本身)做成分布式的,就不用改动 Rails 应用,可以把精力继续放在给飞速增长的用户群做新功能上,而不是在 Git 这件事上搞花样。想法很务实。可惜行不通。

团队尝试了多种用分布式文件系统承载 Git 数据的方案:最直观的一个是用 NFS 把所有仓库集中放在一台服务器上,很快就被否了。Git 的默认实现对文件系统语义做了一系列假设(加锁、删写、读取、同步等等),这些假设在开发者那台慢速笔记本的本地盘上能保证还可以的性能,但它完全不考虑这些操作在网络文件系统上的表现。结果就是既慢,又满是 bug。

之后他们又转向了在块级别做文件系统复制的方案——现在回头看,简直堪称"恐怖"。先短暂地上过 GFS,后改为基于 DRBD 的方案并运行了较长时间。这些尝试最终都撞了墙:日常运维体验极其糟糕,性能上也毫无优势。问题的根源,归根结底是磁盘上 packfile 的设计。

前面我们已经看到,Git 这种图状数据结构让来回往返操作的代价高得难以承受。不幸的是,同样的问题也存在于底层磁盘数据上。DAG 中对象的布局和它们在 packfile 中的存放位置之间没有任何对应关系。生成 packfile 时最核心的优化目标就是压缩体积:对象在 pack 内随机排列,经过压缩,而且关键在于它们通常不会被完整存储——大多数对象都以 delta 形式叠加在同一 packfile 中的另一个对象之上。因此,读取单个对象时,在图数据结构中经过多次逻辑跳转之后,还要在磁盘格式上跟着做多次物理跳转。

这种动辄跨越数十 GB 数据的随机访问发生在每一次 Git 操作中,根本无法在网络文件系统上顺畅运行(无论它在文件级别还是块级别做复制)。唯一的可行办法是把整个文件缓存到本地——但当同一个文件系统里托管着数十万个仓库时,缓存这条路也行不通。

最终,GitHub 的系统工程师们痛下决心,放弃了文件系统层面的分布式方案。他们开发了一套 RPC 系统,让仓库存放在专用的文件服务器上,并改造 Rails 应用改为远程执行所有操作。这带来了相当程度的水平扩展能力,但既没有解决可用性问题,也没有改善最热门仓库的性能。毕竟每个仓库仍然只存放在一台机器上。

#Spokes 与一致性

Spokes 最初由 GitHub 在 2013 年前后开发,后来逐渐成为业界标准方案。多数 Git 托管服务在其架构中都采用了 Spokes 思路的某种变体(即在应用层面对 Git 仓库进行复制)。Spokes 能稳定运行这么多年,主要归功于它做出了三个被时间证明是最优的根本性选择:

  1. 它不直接分布式 Git 本身,而是工作在 packfile 层面;
  2. 它以完整的 Git 仓库形式把所有数据存放在本地 NVMe 磁盘上;
  3. 它对 Git 数据做复制,但保证所有副本始终保持一致。

基于前面提到的 packfile 上的随机读取模式,把普通 Git 仓库存放在 NVMe 硬盘上基本上是硬性要求,这样才能保证所有基础 Git 操作的速度。NVMe 也让克隆保持高效,因为你不需要把数据转换成 Git 客户端期望的格式。它还能让你专注于在 Git 之上构建产品,而不用自己维护一个能处理这些特殊仓库的 Git 分支版本。

同样关键的是,所有数据副本要始终严格保持一致。这一点吃过亏才会明白:Git 客户端和最终一致性(eventual consistency)完全 合不来。如果本地 Git 客户端推送了一个 commit,紧接着在一次 fetch 之后却读不到它,那就麻烦了——Git 会直接懵圈。如果你让 CI 流水线跑在一百个 runner 上,其中三个在克隆仓库后找不到本该测试的 commit,同样是麻烦,用户体验也非常糟糕。

无论是在客户端还是后端,对 Git 仓库采用最终一致性会带来很多坑。因此 Spokes 付出了很高的复杂度代价,来确保系统始终保持完全一致。下面具体看看这意味着什么。

Spokes 是一个基于共识(consensus-based)的分布式系统。它的做法是把 Git 仓库的多个副本分别存放在不同服务器上。每当你推送新数据时,一个编排器(orchestrator)会把推送扇出(fan out)开,让仓库的每一份实例都收到一份副本。这个扇出过程通过经典共识算法 3PC(三阶段提交) 同步,只有在大多数节点确认之后,推送才会被接受。

1 · VOTING  2 · PRE-COMMIT  3 · DO COMMIT

要进一步讲 Spokes 如何使用 3PC,得先理解一次 Git push 是怎么工作的。Git push 包含两个部分:一个 packfile 和一个引用事务(reference transaction)。前面已经提过,packfile 装着你推送到仓库的对象(blob、tree,以及包含你改动的 commit)。引用事务才是真正把改动发布到仓库的部分——它会更新一个或多个引用(比如你正在操作的那个分支),让它们指向你刚刚推送的 commit。

这种分离在这里非常方便,因为推送的提交在指向它的引用更新之前都是不可见的(在 Git 的术语里叫"reachable")。这意味着我们可以这样实现推送的一致性:先把 packfile 同时分发到所有节点(这一步不需要同步),然后再用引用事务做三阶段提交。引用事务比 packfile 小得多,同步起来也快得多。Git 本身支持准备引用事务:它能锁定引用、验证现有值是否符合预期,并一直持有锁,直到收到该事务的提交或中止指令。

Playback speed0.010xReplicas5One-way latency20msUPLOADPREPARELOCKCOMMIT

借助这种方案,我们能确保每次推送都完全同步到所有副本。之后,读取操作(fetch、clone)就可以安全地路由到任意单个副本,因为每个副本始终都是最新的。

Spokes 本质上就是这么工作的,过去 13 年里它一直运行得相当不错。当然,Spokes 并不完美——没有任何系统是完美的。到了 2026 年,人们使用 Git 仓库的方式已经发生了巨大变化,沿途我们也学到了许多关于构建分布式系统的重要经验。时间和实践已经证明,Spokes 当年的哪些选择是最佳的,哪些不是。

其中一个被证明至关重要的缺陷是三阶段提交(3PC)的水平扩展能力受限。Spokes 最初发布时,每个仓库三个副本是最佳选择。三个副本足以服务普通仓库,容量绰绰有余,也能在某台机器宕机时保持足够的冗余来继续接收推送。

到了 2026 年,情况已然大为不同。企业级公司的平均代码仓库已变成了庞大的单体仓库。三个副本不足以应对这类仓库的流量,尤其是 CI 场景下的需求。Spokes 当然支持运行超过三个副本,但随之而来的就是令人头疼的规模化尾延迟问题。三阶段提交与 Git 事务模型非常契合,但作为一种共识算法,它存在根本性局限:每一步的延迟都受限于集群中最慢的那台服务器。副本越多,集群的推送吞吐量反而越差

这种可扩展性约束在另一个方向上也同样存在。当 agent 在大规模场景下与 Git 仓库协作时,它们往往不会在单体仓库内工作,而是创建大量的小型仓库——其中许多是一次性的,大部分几乎无人问津。Spokes 在这种场景下就力不从心了,因为它依然要求每个仓库配备三个副本。三个基本处于空闲状态的副本,却又无法削减,否则系统就无法保证强一致性,进而可能导致数据丢失。对三阶段提交来说,下限总是太高,上限总是太低。

还有一个事先根本无法察觉、只有亲身经历过才会恍然大悟的缺陷,那就是 Spokes 在规模化运维时颇为粗糙。由于磁盘上的仓库始终是达成共识的唯一依据,每一份仓库副本都至关重要。你必须像呵护宠物一样去对待这些仓库,而不是像管理牲畜那样批量处置。

这意味着,首先,你必须精确掌握每个仓库的位置。这带来了一项外部依赖(以及潜在的可用性问题):需要一个外部数据库维护一张庞大的路由表,将每个仓库映射到其所有副本所在的机器。每个仓库还必须做校验和,并不断将校验和更新到路由表中,以确保仓库在磁盘上的有效性。一旦仓库出现问题(相信我,问题总是层出不穷——Git 在实践中相当挑剔),你必须及时发现并安排修复任务使其恢复健康。而且速度必须快!因为再次强调,磁盘上的仓库才是事实的来源。损坏的副本和丢失的副本同样致命。如果三份副本中有两份损坏,系统就无法继续接收推送——达不到仲裁数量。

#Continuity

Continuity 是我们在 Cursor 开发的 Git 存储系统,思路非常明确:借鉴 Spokes 所有做得好的地方,同时修复那些经过多年实践后已被证实的缺陷。

Continuity 是一个简洁的系统(如果不够简洁,就无法做到易于运维)。其核心原语是一个预写日志(WAL),我们将其存储在兼容 S3 的对象存储中。生产环境中我们直接运行在 S3 上,但设计上支持部署到任何云平台。

当仓库收到推送时,我们会将推送作为一条 WAL 条目存入 S3。在推送完全持久化之前,我们绝不会向客户端确认成功。每次推送作为独立的对象存储:我们同时将推送的 packfile 写入磁盘并上传到 S3。但仅上传 WAL 条目并不意味着发布该推送。一次推送只有在成功地在仓库的本地副本上准备好引用事务,并将指向该 WAL 条目的指针记录到 WAL 索引文件中后,才对外可见,而 WAL 索引文件本身也是存储中的一个独立对象。这强制所有推送都是可线性化的。

PUSHINDEXUPLOADGETLOCKPUTREF TXN◀后退暂停前进▶

我们尽量避免每次 push 都做一次 S3 写入,因为在活跃的仓库中,这会让 push 吞吐量被 S3 PUT 操作的延迟死死卡住。通过精心调优的批量实现,加上只需把引用事务同步到单个本地仓库而非副本仲裁,我们构建了一个可以按磁盘写入速度上限来接收 push 的系统。

仓库的本地副本自然就是一个普通的 Git 仓库,存放在一块速度极快的 NVMe 固态硬盘上。我们采用了 Spokes 的做法,因为我认为 Spokes 在这点上做得非常到位。这让我们能复用 Git 社区所有出色的开源工作,包括上游 Git 客户端及其众多性能优化。这样我们就能专注交付新功能,而不是在 Git 本身之上做奇奇怪怪的改造。

#共识机制

我们看到,Spokes 集群难以运维的一个原因是:精确追踪每个仓库在每台服务器上的位置极为重要。Continuity 在这一点上的做法截然不同。仓库到底放在哪儿?答案是"哪儿都行"。这根本不重要!我们把仓库当作磁盘上的一个温数据缓存来处理,但唯一的真相来源始终是 S3 中的预写日志(WAL)。整个系统是无状态的,没有路由表(也不需要运维关系型数据库——堪称福报)。如果访问某台主机时本地磁盘上没有这个仓库,我们就直接从 WAL 把它物化出来。虽然这件事我们可以做得非常高效,但当然不想无时无刻都这样做,因为那样太浪费了。在生产环境中,我们使用 rendezvous hashing(会合哈希)将仓库 ID 映射到我们预期该仓库所在的节点列表。我们路由仓库所需的全部状态,就只有仓库 ID 和集群中当前健康节点的集合。就算这些状态对不上(比如某个节点变得不健康了),也完全没关系——下一轮到哪台节点,我们就把仓库物化到那里就行。

那一致性呢?选举呢?某个仓库的主节点到底是哪台服务器?这些统统不重要!这里没有状态,也没有共识。任何服务器都可以是主节点。对预写日志的所有更新,都通过 S3 上的原子比较并交换(CAS)操作来同步,因此任何仓库实例接收 push 都是安全的。和路由一样,让任意服务器充当主节点并不是最高效的做法(会导致 CAS 重试,进而拖慢 push),所以实际中我们总是选同一台服务器作为主节点,也就是 rendezvous hashing 排序列表里的第一名。但在边界场景下——比如部署、故障转移、网络抖动——我们并不在乎主节点具体是哪台。系统在降级时始终保持正确,在正常时始终保持快速。

UPLOADRACECONFLICTREFETCHREBASERETRYDONE◀BackPauseNext▶

#复制

把预写日志放在 S3 里,为扩展打开了一扇大门。我们可以有任意数量的副本——这里的"任意"不是夸张——因为 S3 的扩展能力无可匹敌,所有副本都直接从 S3 追赶进度。我们采用乐观复制,在集群内通过 UDP gossip 包传播数据。每个包都包含每条副本在每次 push 后直接从 S3 追赶所需的全部元数据。"这也太离谱了吧,"我仿佛听到你隔屏隔时空地嘀咕。"UDP 根本不是可靠传输。"它当然不是。分布式系统里没有什么是可靠的!网络不可靠,路由不可靠,拓扑也不可靠。但这无所谓。每条副本都知道自己已经追赶到的 WAL 索引最后版本的 ETag。当你对副本发起读操作时,我们用预期的 ETag 对 S3 做一次条件 GET。如果返回的是不带 body 的 304(顺便说一句,这个操作快得离谱——平均不到 10 毫秒,因为它只是 S3 的一次元数据操作),说明副本是最新的,可以直接提供 fetch 或 clone 服务。如果返回的是 200,则会拿到最新版本的 WAL 索引,副本先追赶再响应读取。

丢弃 UDP 节点间广播数据包 PUSH COMMIT GOSSIP REPLICATE FETCH GET · 304 SERVE ◀后退 暂停 前进▶

就算这条 UDP 复制包丢失了,或者因为拓扑变化被送到了错误的服务器,也无关紧要。所有副本上的读取都是完全一致的,因为它们都对照着 S3 这个唯一数据源做了校验。系统的设计目标是:降级时保证正确性,状态正常时保证高性能。

这一点带来了两方面的好处。首先,由于系统始终保持一致,在它上面构建基础设施就变得非常简单——我们的 agent、Web 界面、客户端拿到的都是全局一致的仓库视图。其次,系统在两个方向上都能伸缩,每个仓库恰好获得所需数量的副本。大型 monorepo 可以部署到数百个副本上来扛住 CI 任务带来的流量;agent 创建的上百万个微仓库各自只需要一个副本就够了——不需要更多,因为 S3 已经是数据源。实际上,一个空闲仓库连一个副本都不需要:当某个副本长时间没有流量时,我们就把它从节点磁盘上回收掉,等下次 fetch 请求进来时,再从 WAL 重新物化出来。

#压缩

预写日志(WAL)需要定期压缩。日志不能无限增长:因为全量恢复时要重放每一条记录,条目越多,恢复代价越高。

有意思的是,普通的 Git 仓库也——注意,虽然 Git 不是基于 WAL 的——同样需要定期压缩。我们已经知道,Git 仓库的基本存储单元是 packfile。每次你向远程仓库 push,或向本地仓库 fetch 时,都会生成一个新的 packfile。但这种机制不能无限扩展:每个 packfile 都附带一份索引,让 Git 能高效查找其中的对象,而这种高效只对单个 packfile 成立。如果你想找某个特定对象,而仓库里有 100 个 packfile,那就得依次打开每个索引文件逐个查找,直到在其中某个里命中为止。一项本来高效的操作,如果要重复执行成百上千次,就不再高效了。

现代 Git 在这方面已经做得相当好,它支持多包索引(multi-pack indexes)和增量几何压缩(incremental geometric compaction)。但终究逃不过一步:对磁盘上的 Git 仓库重新打包。一直以来,这对 Spokes 这类系统来说都是个持续的可用性难题,因为 repack 是个非常吃 CPU 的操作,即便增量执行也是如此,而且必须在系统的所有副本上完成。一旦不小心在两个或更多 Spokes 节点上对同一个仓库触发了这个维护操作,仓库就会直接故障转移。

在 Continuity 里,我们把压缩的开销做了分摊。只有主节点做压缩,压缩的结果同时作用于磁盘上的仓库WAL。所有副本跟随 WAL,自然也就跟随压缩事件。副本不重新打包,只是从 S3 下载已经压缩好的包文件,用带宽换 CPU。

#可扩展性

副本机制和压缩机制是决定 Git 存储系统在高负载下表现如何的两个关键因素。正如刚才所见,两者本质上紧密关联:仓库每秒接收的推送越多,读性能就下降得越厉害,因为每次推送产生的 packfile 都必须经过压缩,Git 操作才能保持高效。如果把这些推送也复制到多个副本上,那压缩要么跟着复制走,要么在每个副本上各自独立执行。

Continuity 的 WAL 优先架构带来了完全一致的横向扩展能力:你可以部署任意数量的副本,只读 Git 操作的吞吐量随副本数线性增长。由于集群中所有副本完全一致,这让我们既能横向扩展 Git 协议(clone、fetch),也能横向扩展 Origin 在仓库之上提供的所有 RPC 操作(Web UI 交互、REST API、所有 agentic 接口等等)。

我们做过最多 100 个副本的合成压力测试,读取性能呈现出稳定的线性扩展,同时推送吞吐量没有任何回退。

集群的 push 吞吐取决于我们在 S3 上更新 WAL 的延迟。使用 S3 Standard 时,在对压缩数据进行压缩并复制到所有其他节点的过程中,可以维持最高 120 次 push/秒。我们还在 S3 Express One Zone 上部署了高性能集群,其 PUT 操作延迟要低得多。在该配置下,吞吐量可超过 300 次 push/秒,此时瓶颈实际上变成了 Git 压缩磁盘数据的速度。我们正在探索创新的磁盘数据布局方式,以降低压缩带来的影响:目标是在不放松持久性和一致性保障的前提下,持续提升 Git 仓库的代码摄入速度。

  • S3 Standard
  • S3 Express One Zone

everysphere(Cursor 的 monorepo)的 Push/Clone 吞吐。
所有 push 操作均为线性一致,并在确认前持久化到外部存储。
所有 clone 操作均完全一致。

  • S3 Standard
  • S3 Express One Zone

everysphere(Cursor 的 monorepo)的 Push/Clone 吞吐。
所有 push 操作均为线性一致,并在确认前持久化到外部存储。
所有 clone 操作均完全一致。

  • S3 Standard
  • S3 Express One Zone

everysphere(Cursor 的 monorepo)的 Push/Clone 吞吐。
所有 push 操作均为线性一致,并在确认前持久化到外部存储。
所有 clone 操作均完全一致。

#WAL 即真相

S3 是一项伟大的技术。由 S3 API 开创的 Blob 存储概念,已经成为大型数据存储系统中非常强大的基础组件,托管 Git 仓库自然也不例外。本文介绍的设计在很多方面都有创新,但把 packfile 当作 blob 来存储并不是首创。Azure DevOps(微软自家的 GitHub 竞品)的 Git 存储系统就非常成功,它把 packfile 存进 blob 存储,把引用存在关系型数据库(MS SQL Server)里。这样的方案有许多权衡取舍:关系型数据库在处理大量引用事务时扩展性不错,但你得自己去运维它。我们坚信,Git 数据的一致性比其他任何考量都重要。正是这一点,让我们最终选择了基于 WAL(预写日志)、不依赖外部数据库的设计方案。

生产环境中的 Git 仓库会遇到各种问题:静态数据损坏、repack 时的 bug、push 时的竞争条件……几乎全是边界场景。其中大部分问题在 Git 上游已经被解决了,但并非全部。没有任何系统能做到零 bug,哪怕是那些开源且被广泛部署的系统也不例外。我们的数据一致性模型确保能追踪仓库上发生的每一个核心操作:在 push 完全持久化到 WAL 之前,我们绝不会对其确认;我们对所有 push 做线性化处理;访问仓库的任何视图时,看到的都是完全一致的状态。由于每次 push 都记录在 WAL 中,我们可以查看仓库经历过的任何状态,拥有所有 push 和 repack 操作的完整来源数据,能对每个副本做回退和快进,也不必和任何外部数据库同步状态——无论是只存引用的数据库,还是存全部对象数据的数据库。一旦(不是如果)我们在 Git 中遇到 bug,可以精确定位发生了什么并回滚。而且除了 Git 已有的 bug 之外,我们引入的新 bug 也极少,因为所有 Git 操作都是在磁盘上的普通 Git 仓库上、借助现成工具完成的。

#Origin

我们都深知托管他人源代码的重要性,想必读到这篇文章的每一位读者也都心知肚明。如果开发者无法访问代码,整个公司可能会陷入瘫痪。

原始来源: Hacker News

评论 (0)