研究解读 · 小互解读

Cursor 工程师详解 Git 托管 20 年难题:如何重造 Git 托管——Continuity 架构详解

从 Git 的 DAG 与 packfile,到 GitHub Spokes,再到以 S3 WAL 为事实来源的 Continuity:一篇讲清大规模 Git 托管为何困难、Cursor 怎样解决的完整解读。

一分钟速览
  • Vicent Martí 长期参与 libgit2 与 GitHub 的 Git 基础设施工作。本文既回顾大规模 Git 托管的架构演进,也为 Cursor 的 Origin 平台解释技术底座。
  • Git 的 DAG、packfile 与强一致性要求,使对象数据库、网络文件系统和固定副本架构分别在规模化时撞墙。
  • Continuity 以 S3 WAL 为事实来源、本地 Git 为热缓存,让服务副本按流量从 0 向上伸缩;100、120、300 仍是缺少完整测试条件的 Cursor 自报结果。

这篇文章来自 Cursor 官方博客,作者是 Vicent Martí。它讨论的不是编辑器功能,也不是模型能力,而是一个更底层的问题:为什么大规模托管 Git 极其困难,以及 Cursor 为什么要为自己的代码托管平台重造一套 Git 存储系统。

Vicent Martí 现在在 Cursor 做系统工作,此前任职于 PlanetScale 和 GitHub。2010 年,他曾在 Google Summer of Code 期间参与 libgit2;后来在 GitHub 系统团队长期处理 clone、fetch、packfile 和大型仓库性能。2015 年,他记录过 GitHub 如何将 Git 网络操作的平均 CPU 时间降低 90% 以上,并把相关优化贡献回上游 Git。

这个背景很重要。本文不是 AI 公司对 Git 架构的二手整理,而是一位长期处理 Git 底层与托管系统的工程师,为 Cursor 新平台作出的第一方技术说明。

2026 年 6 月,Cursor 在 Compile 大会上公布了新的 Git 平台 Origin。两个月后,这篇文章首次系统解释了 Origin 的底层存储系统 Continuity。因此,文章同时承担两项任务:回顾过去二十年 Git 托管踩过的坑;证明 Cursor 有能力从“帮助用户写代码”的工具,走向“替用户保管软件历史”的基础设施。

整篇文章的核心可以先压缩成一句话:

Continuity 保留本地 NVMe 上的原生 Git 仓库,但把 S3 中的预写日志 WAL 设为唯一事实来源。这样,磁盘副本就能从必须精心维护的关键数据,变成可以按流量增加、销毁和重建的热缓存。

一、核心痛点:为什么大规模托管 Git 是一场噩梦

Git 最初是为 Linux Kernel 这类高度去中心化、允许离线工作的项目设计的。每个开发者都拥有完整仓库,服务器上的仓库和开发者电脑里的仓库,本质上使用同一套实现。

这让小型 Git 服务很容易搭建:在磁盘仓库前放一个 HTTP 入口就能运行。但一旦进入 GitHub、GitLab 或企业级代码托管的规模,这种“服务器和笔记本完全一样”的设计反而带来三层麻烦。

1. Git 的底层数据是一张图

Git 中的 commit、tree、blob 等对象通过 SHA 相互引用,构成有向无环图,也就是 DAG。

如果要读取一个 commit,系统必须先拿到这个 commit,才能知道它的根 tree 和父 commit 在哪里;拿到 tree,才能知道下一层文件与子目录的地址。后一步需要的键,藏在前一步的结果里。

所以,把每个 Git 对象拆开存进分布式键值数据库,看起来很符合“内容寻址”的直觉,实际却会把一次本地遍历变成大量串行网络往返。很多请求无法预先并发,因为下一步访问什么还不知道。

2. Packfile 又增加了一层物理跳转

Git 不会把每个对象都完整保存。为了节省空间,它会把大量对象压进 packfile,并用 delta 记录一个对象相对于另一个 base 对象的差异;base 本身还可能继续依赖另一个 delta。

于是,读取一个逻辑对象时,Git 不仅要沿 DAG 找指针,还可能在数 GB 的 packfile 中跨越多个物理位置,才能把对象还原出来。本地 NVMe 可以把这些随机读取压到很低的延迟;一旦跨过网络文件系统,每一次跳转都要重新支付网络成本。

原文交互 · 01—02Git 在逻辑层和物理层各“跳”一次

操作提示:暂停自动播放,或用“上一步 / 下一步”逐次查看远程读取。

远程对象遍历对象数据库为什么拖慢 history walk
来源页 Git DAG 远程对象读取动画的静态 SVG
只知道 main 指向哪个 commit1 / 6

下一批对象的地址还没有暴露。

Packfile 随机读取delta 为什么还要继续找 base
来源页 packfile 随机读取动画的静态 SVG
定位目标对象1 / 6

索引只告诉 Git 对象在 packfile 的哪个位置。

原文是两个自动 SVG 演示;此处保留原始底图,并恢复播放、暂停和单步观察。

3. Git 不能容忍随便的“最终一致性”

很多互联网系统可以接受数据晚几秒同步,Git 很难。

开发者刚刚 git push 成功,下一秒 git fetch 却找不到刚推送的 commit,客户端会进入异常状态;一百台 CI Runner 同时 clone,其中几台读不到本该测试的 commit,就会产生随机、难复现的失败。

因此,大型 Git 托管必须同时满足三个看似冲突的要求:本地 Git 操作要快,读取要能横向扩展,所有副本又必须对已经发布的仓库历史保持一致。

二、历史演进:前人踩过哪些坑

文章最有价值的部分之一,是它没有直接宣布 Continuity 多么先进,而是先回顾几代架构为什么失败。这样才能看懂 Cursor 的方案究竟解决了什么。

1. 分布式对象存储:存得优雅,clone 却慢得无法接受

Google/JGit 团队曾尝试把 Git 对象放进分布式哈希表 DHT。普通操作可以运行,但 DAG 的串行读取成本很高;更致命的是,不管服务器内部怎样存储,Git 客户端最后仍要求通过网络接收 packfile。

这意味着服务器必须把分散对象重新遍历、筛选、压缩并组装成 pack。Shawn Pearce 后来的复盘显示,linux-2.6 仓库的一次 clone 可能耗时 15 到 30 分钟。团队最终放弃对象级 DHT,转而保留原生 pack 和 index。

这次失败说明:Git 对象适合按 SHA 寻址,不等于完整 Git 工作负载适合对象级远程访问。

2. 分布式文件系统:保留了 Git,也保留了所有本地假设

GitHub 早期先后尝试过 NFS、GFS/GFS2 和 DRBD 等文件级或块级复制方案。想法很务实:Rails 应用不改,Git 仓库也不改,只把底层文件系统分布出去。

问题是,Git 默认实现对本地文件锁、同步、原子更新和缓存做了大量假设;packfile 的随机读取又会把一次操作拆成很多跨网络跳转。为了不慢到停滞,只能尽量把整个 packfile 缓存到本地,但同一文件系统需要容纳几十万个仓库时,这条路根本不可持续。

GitHub 后来退回一个更朴素的方案:把普通 Git 仓库保存在专用文件服务器的本地磁盘上,Rails 通过 RPC 把操作送到仓库所在机器执行。这保住了原生 Git 性能,却仍然让单个仓库受限于一台机器。

3. Spokes:第一次同时保住性能与一致性

2013 年前后,GitHub 开发了后来被称为 Spokes 的系统。它奠定了现代 Git 托管的基本形态:

  • 每个副本都是 NVMe 上的原生 Git 仓库;
  • 数据在 packfile 层复制,不重写 Git 的对象模型;
  • 所有关键副本保持同步一致,读取可以交给任意副本。

一次 push 包含两部分:packfile 保存新对象,引用事务把分支指向新 commit。Spokes 可以先异步把较大的 pack 扇出到副本,再用三阶段提交 3PC 协调很小的引用事务。多数节点确认后,引用才真正提交,push 才向客户端返回成功。

原文交互 · 03—04Spokes:节点故障与长尾延迟都会进入提交路径

操作提示:点击参与者切换在线状态;拖动滑杆改变副本数和单程延迟。

3PC 故障实验5 / 5 个参与者在线,可以提交
来源页三阶段提交交互图的静态 SVG
Spokes 吞吐模拟器副本越多、网络越慢,协调器等待越久
来源页 Spokes push 动画的静态 SVG
SPOKES3PC 分阶段协调 · 单程延迟 20 ms

5 个副本都进入协调路径;提高延迟会直接拉长提交周期。这里只展示因果关系,不推算生产吞吐量。

这两项交互恢复的是原文的核心论证:固定副本不仅占空间,还直接进入同步提交的等待时间。

Spokes 运行多年并不是因为它“落后”,而是因为它正确保留了本地 Git、packfile 和强一致性。Continuity 后来也没有推翻这些选择。

真正的问题,是 Spokes 的每个关键副本同时承担了三种责任:

  1. 服务 clone、fetch、Web UI 和 API 请求;
  2. 保存不能丢失的耐久数据;
  3. 参与每次 push 的写入协调。

对热门 Monorepo 而言,三个副本不够服务 CI,继续增加副本却会让更多节点进入 3PC,push 更容易被最慢节点拖住。对 Agent 创建的海量低流量、一次性小仓库而言,三个长期在线副本又严重浪费。

所以原文对 Spokes 的总结很准确:副本数量的下限始终过高,上限又始终过低。

此外,磁盘副本本身就是事实来源。平台必须用外部路由表记录每个仓库在哪些机器上,持续检查校验和、发现损坏并迅速修复。每个副本都像必须精心照料的“宠物”,坏了不能直接扔掉重建。

三、Cursor 的解法:Continuity 架构

Cursor 为 Origin 自研的 Continuity,核心不是抛弃 Git,也不是把 commit、tree、blob 全部直接改存成 S3 对象。

它保留 Spokes 最成功的部分:本地 NVMe 上仍然运行普通 Git 仓库,clone、fetch、引用事务和 repack 继续使用成熟的 Git 工具链。真正改变的是:谁有资格定义仓库的真实状态。

架构维度 Spokes Continuity
事实来源 多台关键节点上的磁盘 Git 仓库 S3 兼容对象存储中的 WAL
本地仓库角色 服务副本、耐久副本、共识参与者 可重建的高性能热缓存
写入一致性 多副本 3PC 协调引用事务 本地事务准备 + S3 ETag 条件写
路由 外部数据库记录仓库副本位置 Rendezvous hashing 计算优先节点
读扩展 增加副本也会增加协调代价 服务副本可独立按流量增加或回收

在 Spokes 中,“仓库在哪里”意味着必须知道哪几台机器保存着关键副本。在 Continuity 中,仓库最终存在于 WAL;任何健康节点都可以读取日志并恢复一份本地 Git 仓库。

Rendezvous hashing 仍会把同一个仓库稳定映射到一组优先节点,以提高缓存命中率、减少写入竞争。但这个映射只负责性能,不负责正确性。即使节点列表短暂不一致,最坏结果也是另一台机器重新恢复仓库,而不是出现两段互相冲突的历史。

Continuity 真正完成的是职责拆分:

  • S3 WAL 负责耐久性、完整操作顺序和恢复依据;
  • 本地 Git 仓库负责高速执行 Git 操作;
  • 副本数量只负责匹配当前读取流量。

四、Continuity 的关键技术实现

1. WAL 优先:一次 push 在哪里正式生效

一次写入大致经过六步:

  1. 前端接收客户端上传的 packfile 和引用事务;
  2. packfile 写入本地仓库,同时作为不可变 WAL 条目上传 S3;
  3. 本地 Git 准备引用事务,验证旧值并锁定引用,但暂不提交;
  4. 前端读取 S3 上的 WAL 索引与当前 ETag;
  5. 前端把新条目指针追加到索引,并用 If-Match 条件写回;
  6. 索引更新成功后,本地引用事务才提交,服务器才确认 push。

第五步是线性化点。两个前端可能同时上传 WAL 条目,但如果它们拿着同一个旧 ETag 更新索引,只有一个会成功;失败者收到 412 Precondition Failed,重新读取最新索引,把自己的写入排到下一个位置再重试。

原文交互 · 05—06WAL 写入与 CAS 竞争,都要按时间顺序看

操作提示:点击阶段,或用播放和步进控件查看 WAL push 与 CAS 竞争。

一次 WAL push线性化点在索引 CAS,而不是上传 pack
来源页 Continuity WAL push 动画的静态 SVG
接收 push1 / 7

前端接收 packfile,并开始建立对象索引。

两个前端争抢同一 ETagWAL 条目不冲突,索引顺序只能有一个赢家
来源页两个前端竞争 CAS 的静态 SVG
A、B 分别上传 WAL1 / 8

不可变对象使用不同键名,因此上传阶段不会互相覆盖。

两个演示都保留无脚本静态底图;启用脚本后可逐步查看每个状态与因果。

这相当于把原来需要多个关键副本参与的写入协调,收敛成对一个很小的 WAL 索引对象进行比较并交换,也就是 CAS。健康状态下,优先节点可以减少竞争;故障切换时,正确性仍由 S3 条件写决定,而不是依赖某台机器永远担任主节点。

2. UDP Gossip + ETag:消息可以丢,历史不能旧

写入完成后,主节点通过 UDP gossip 通知其他副本提前追赶 WAL。UDP 可能丢包、乱序,甚至把通知发给已经不负责该仓库的节点,但它只承担性能优化,不承担正确性。

每个副本在真正处理 fetch 或 clone 前,还会拿本地记录的 ETag 向 S3 发起条件读取:

  • 返回 304 Not Modified:WAL 索引没变,本地副本已经是最新,可以直接服务;
  • 返回 200 OK:S3 存在新版索引,副本先下载并重放缺失 WAL,再响应客户端。
原文交互 · 07自己丢一次 gossip,才能看清两条读取路径

操作提示:切换“丢弃 UDP gossip”并逐步播放,对比 304 快路径和 200 追赶路径。

复制与读取正确性
来源页 gossip 与条件读取复制图的静态 SVG
客户端 push 到主节点1 / 7

主节点先把新 WAL 条目和索引提交到 S3。

当前路径:gossip 到达,副本提前追赶;fetch 时条件 GET 返回 304。

开关只改变性能路径,不改变最终正确性:两条路径都必须在返回客户端前确认最新索引。

原文称这种只检查 S3 元数据的 304 请求平均低于 10 毫秒。这是 Cursor 自己的生产观察,不是 AWS 对所有地区和负载的普遍延迟承诺。

3. 弹性副本:从 0 到上百个,只跟流量走

WAL 成为事实来源后,本地仓库才真正变成缓存。

  • 热门 Monorepo 可以增加到几十乃至上百个服务副本,分担 clone、fetch、Web UI、REST API 和 Agent RPC;
  • 普通仓库可能只需要一个本地副本;
  • 长期没有流量的仓库可以从节点磁盘回收,本地副本数量降到 0,下次访问时再根据 WAL 重建。

这里的“0 副本”只是零个本地服务缓存,不是仓库数据消失。原文没有承诺所有仓库都能“秒级恢复”,所以不能把重建速度写成普遍保证。

4. 单节点压缩:用带宽替换重复 CPU

每次 push 都会产生新 packfile。pack 越来越多,即使单次索引查询很快,重复数百或数千次也会拖慢 Git,所以仓库仍要定期 repack。

Spokes 的多个关键副本可能重复执行高耗 CPU 的压缩。Continuity 只让当前主节点执行 repack,并把压缩结果同时应用到本地仓库与 WAL;其他副本不再重新计算,只从 S3 下载已经压好的 pack,用网络带宽替换重复计算。

原文交互 · 08—09本地副本不断消失和重建,WAL 压缩也不断推进

操作提示:播放冷热变化,或逐个加入 pack,再触发 compaction 观察边界移动。

副本随流量伸缩热门 monorepo:6 个本地副本;冷仓库:0—1 个
来源页仓库热度与弹性副本图的静态 SVG

虚线空位表示本地缓存已回收;仓库事实仍保存在 S3 WAL,而不是随副本一起消失。

增量 WAL 压缩当前 3 个 pack,尚未触发压缩
来源页 WAL 增量压缩图的静态 SVG
basea1b7
COMPACTION FRONTIER
#97 base.wal
#98 a1.wal
#99 b7.wal

每次新增或压缩都先更新 WAL 索引;达到阈值后,主节点把多个小 pack 合并成一个较大 pack。

“0 副本”只表示没有常驻本地服务缓存;这里没有把它写成数据归零,也没有声称普遍秒级恢复。

5. WAL 的另一项价值:完整溯源与恢复

生产 Git 仓库会遇到静态数据损坏、repack 缺陷、push 竞争条件和 Git 自身的边缘错误。Continuity 把每次 push 和压缩事件写入 WAL,因此可以追踪仓库经历过的每个基础状态。

副本既能向前恢复到最新状态,也能回退到错误发生前。工程师可以定位是哪次 push 或 repack 引入问题,再从操作日志重建仓库。

这也是 Continuity 与“packfile 放对象存储、refs 放关系型数据库”方案的重要区别:它试图让一份日志同时承担持久化顺序、引用变化和恢复历史,避免再维护两套必须严格同步的权威状态。

五、性能表现,以及这些数字没有证明什么

Cursor 给出了三组最醒目的内部测试结果:

  • 在最多 100 个副本的合成压力测试中,读取吞吐量随副本数近似线性增长,push 吞吐量没有明显回归;
  • 使用 S3 Standard 时,集群可持续处理最高约 120 次 push/秒,同时完成压缩与结果复制;
  • 使用延迟更低的 S3 Express One Zone 时,吞吐量超过 300 次 push/秒,瓶颈转向本地 Git 压缩。
原文静态图 · 10—11只有最后两张是静态性能图表

前九个是 SVG 动画或交互组件;这两张才适合作为静态图直接插入。

只读副本增加时 clone 吞吐量近似线性增长

读:最多 100 个副本下近似线性扩展

支持“读容量可通过增加副本扩展”这一窄结论,不等于任何负载都完美线性。

S3 Standard 与 Express One Zone 的 push 吞吐量测试

写:Standard 最高 120 push/s,Express 超过 300

对象存储延迟明显影响写入上限,说明权威日志层仍约束写性能。

证据边界:原文没有完整公开测试硬件、对象分布、请求混合、P95/P99、故障注入与长期生产 SLO。这些是 Cursor 自报结果,不是行业通用承诺。

媒体口径已修正:九个交互演示,加两张静态图表。

这些数字支持的结论很具体:把事实来源移出副本集群后,增加读副本不再必然增加每次 push 的协调参与者。Continuity 的读写解耦方向获得了内部测试支持。

但它们不能直接证明 Origin 已经达到 GitHub 级别的长期生产可靠性。文章没有公开完整硬件、仓库对象分布、push 大小、请求混合、P95/P99 延迟、故障注入、跨区域容灾、长期 SLO 或可复现实验数据;测试对象主要是 Cursor 自己的 Monorepo everysphere

因此,“读取近似线性扩展”比“完美线性扩展”准确;“空闲仓库可以从 WAL 重建”也比“下次访问一定秒级恢复”准确。

回到产品层,Continuity 是 Cursor Origin 代码托管平台的技术底座。Cursor 的判断是,Agent 会制造更多代码、PR、CI 运行和一次性仓库,版本控制会从开发者偶尔操作的工具,变成大量自动化系统持续争用的协调层。

这篇文章真正说明的,不只是 Cursor 做出了一个更快的 Git 后端,而是它正在扩大自己的责任边界:从帮助开发者生成代码,走向替开发者保存软件历史。

架构设计已经讲清楚了,内部数据也证明方向可行;但代码托管最终靠的不是一篇漂亮的工程文章,而是几年之后,每一次普通、无聊、没有新闻价值的 push,仍然能够完整找回来。

来源
任意规模的 GitVicent Martí·2026-08-18·查看主材料
本站说明
Continuity 的 100 副本、120/300 次 push 每秒均来自 Cursor 自报合成测试;缺少完整测试环境与可复现数据。