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 可以把这些随机读取压到很低的延迟;一旦跨过网络文件系统,每一次跳转都要重新支付网络成本。
操作提示:暂停自动播放,或用“上一步 / 下一步”逐次查看远程读取。
下一批对象的地址还没有暴露。
索引只告诉 Git 对象在 packfile 的哪个位置。
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 才向客户端返回成功。
操作提示:点击参与者切换在线状态;拖动滑杆改变副本数和单程延迟。
5 个副本都进入协调路径;提高延迟会直接拉长提交周期。这里只展示因果关系,不推算生产吞吐量。
Spokes 运行多年并不是因为它“落后”,而是因为它正确保留了本地 Git、packfile 和强一致性。Continuity 后来也没有推翻这些选择。
真正的问题,是 Spokes 的每个关键副本同时承担了三种责任:
- 服务 clone、fetch、Web UI 和 API 请求;
- 保存不能丢失的耐久数据;
- 参与每次 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 在哪里正式生效
一次写入大致经过六步:
- 前端接收客户端上传的 packfile 和引用事务;
- packfile 写入本地仓库,同时作为不可变 WAL 条目上传 S3;
- 本地 Git 准备引用事务,验证旧值并锁定引用,但暂不提交;
- 前端读取 S3 上的 WAL 索引与当前 ETag;
- 前端把新条目指针追加到索引,并用
If-Match条件写回; - 索引更新成功后,本地引用事务才提交,服务器才确认 push。
第五步是线性化点。两个前端可能同时上传 WAL 条目,但如果它们拿着同一个旧 ETag 更新索引,只有一个会成功;失败者收到 412 Precondition Failed,重新读取最新索引,把自己的写入排到下一个位置再重试。
操作提示:点击阶段,或用播放和步进控件查看 WAL push 与 CAS 竞争。
前端接收 packfile,并开始建立对象索引。
不可变对象使用不同键名,因此上传阶段不会互相覆盖。
这相当于把原来需要多个关键副本参与的写入协调,收敛成对一个很小的 WAL 索引对象进行比较并交换,也就是 CAS。健康状态下,优先节点可以减少竞争;故障切换时,正确性仍由 S3 条件写决定,而不是依赖某台机器永远担任主节点。
2. UDP Gossip + ETag:消息可以丢,历史不能旧
写入完成后,主节点通过 UDP gossip 通知其他副本提前追赶 WAL。UDP 可能丢包、乱序,甚至把通知发给已经不负责该仓库的节点,但它只承担性能优化,不承担正确性。
每个副本在真正处理 fetch 或 clone 前,还会拿本地记录的 ETag 向 S3 发起条件读取:
- 返回
304 Not Modified:WAL 索引没变,本地副本已经是最新,可以直接服务; - 返回
200 OK:S3 存在新版索引,副本先下载并重放缺失 WAL,再响应客户端。
操作提示:切换“丢弃 UDP gossip”并逐步播放,对比 304 快路径和 200 追赶路径。
主节点先把新 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,用网络带宽替换重复计算。
操作提示:播放冷热变化,或逐个加入 pack,再触发 compaction 观察边界移动。
虚线空位表示本地缓存已回收;仓库事实仍保存在 S3 WAL,而不是随副本一起消失。
#98 a1.wal
#99 b7.wal
每次新增或压缩都先更新 WAL 索引;达到阈值后,主节点把多个小 pack 合并成一个较大 pack。
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 压缩。
前九个是 SVG 动画或交互组件;这两张才适合作为静态图直接插入。
读:最多 100 个副本下近似线性扩展
支持“读容量可通过增加副本扩展”这一窄结论,不等于任何负载都完美线性。
写: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,仍然能够完整找回来。
Continuity 如何拆开 Git 托管死结?
关键不是把 Git 搬进 S3,而是拆开“耐久事实”与“服务副本”:S3 WAL 保存事实,NVMe 原生仓库变成可增、可删、可重建的热缓存。
Git 有一股“本地引力”
对象地址藏在前一步结果里,packfile 又带来随机跳转;push 成功后还必须立刻对 fetch 和 CI 可见。跨网络访问会逐次放大成本。
Spokes 把三件事焊在同一批副本上
原生 Git、packfile 复制和 3PC 保住了性能与强一致性,却让每个副本同时承担服务、耐久与写入协调。
关键一刀:把“事实”从副本中抽出来
Continuity 不抛弃 Git,只改变权威边界:S3 WAL 保存操作顺序和恢复依据,本地 NVMe 原生仓库只负责高速执行。
两道门:写入定顺序,读取先验新
正确性不押在某台主机或 UDP 通知上,而押在 S3 条件操作上。Gossip 只负责提前追赶。
证据支持“解耦有效”,还不支持“终局已成”
副本可从 0 到上百个,热门仓库扩读、冷仓库回收;但公开证据仍是 Cursor 自报测试。最终考题,是多年后每次普通 push 仍能完整找回。
一座 Git 仓库,怎样摆脱副本枷锁
管理员本地跑得飞快。可一上规模,读、写、历史都得同时守住。
难题不是“把 Git 放上服务器”,而是让它在大规模下仍然又快又一致。
管理员下一把钥匙,偏偏藏在这次读取的结果里。
对象适合按 SHA 寻址,不等于 Git 工作负载适合对象级远程访问。
管理员仓库没拆,可每次随机跳转都跨了一次网络。
网络文件系统保留了 Git,也保留了 Git 对本地锁、原子更新和低延迟随机读取的假设。
管理员副本下限太高,上限又太低。多一台服务节点,也多一个协调负担。
Spokes 守住了本地性能与强一致性,却把服务、耐久和每次 push 的协调绑在同一组关键副本上。
管理员别让服务副本保管真相。真相进 WAL,本地 Git 只负责跑得快。
Continuity 没有抛弃原生 Git;它改变的是谁有资格定义仓库的真实状态。
管理员账本先排定唯一顺序,本地引用才提交。撞车的写入重读后再来。
ETag 条件写是线性化点:成功者前进,失败者收到 412 后读取新索引并重试。
管理员热就加,冷就收。扔掉的是缓存,不是仓库历史。
副本数量终于只跟读取流量走:可扩到很多,也可降到零个本地服务副本。
管理员方向可行,已经有数据;通用可靠性承诺,还没有。
100 个副本、最高约 120 与超过 300 push/s,都是 Cursor 自报测试,不是所有负载和环境的普遍承诺。
