跳到主要内容
把 Git 仓库的「事实来源」从磁盘挪到 S3 上:读 Cursor 那篇托管架构文的踩坑笔记

把 Git 仓库的「事实来源」从磁盘挪到 S3 上:读 Cursor 那篇托管架构文的踩坑笔记

abanana
abanana

· 阅读约 5 分钟

昨晚刷到 Cursor 官方博客上 Vicent Martí 写的 Git 大规模托管那篇,一口气读完,顺手记一下。这篇笔记不复述全文,只讲我自己最在意的一条线:GitHub 的 Spokes 为什么走到头了,以及 Cursor 新搞的 Continuity(他们叫 Cnt)把 WAL 放到 S3 上当唯一事实来源这件事,为什么我觉得是这二十年里 Git 托管架构上少数真正换了思路的改动。

先立一个出发点,一句带过:Linus 当年设计 Git 时明确要求分布式,但文章里有个判断我很认同——二十年过去,绝大多数团队实际上用的是集中式托管,GitHub 上一个仓库、大家往里 push,分布式那层对普通使用者基本只剩 clone 时的价值。这个判断本身不新鲜,但后面所有架构选择都是从它出发的。

然后是我觉得最有意思的一段历史。GitHub 早期试过 NFS、GFS、DRBD 这些分布式文件系统来放仓库,全部失败。原因不是这些系统不行,而是 Git 对文件系统语义的假设和网文件系统合不来,性能差到没法用。后来 GitHub 转向 RPC,把仓库放到专用文件服务器上,Rails 单体应用远程调用访问——应用能水平扩展了,但每个仓库还是只能待在一台机器上。2013 年前后有了 Spokes:packfile 层面做应用层复制,仓库放本地 NVMe,三副本加三阶段提交保强一致。

Spokes 的问题文章讲得很老实,我挑两条记下来。一是 3PC 的延迟被最慢的副本卡住,副本越多推送吞吐越低,而现在企业 monorepo 场景下三个副本已经不够用了——这两个约束是直接顶死的,加副本降吞吐,不加副本撑不住读。二是几百万个一次性小仓库的场景,每个仓库强制三副本,空闲副本缩不下去,纯浪费。还有运维上的:磁盘副本是事实来源,就得维护路由表、持续校验副本完整性,三副本坏两个,push 直接因为凑不齐法定人数停摆。

Cnt 的做法是把这些约束整个翻过来。预写日志写到 S3 兼容的对象存储上,WAL 是唯一事实来源,本地 NVMe 上的普通 Git 仓库只是个可重建的热缓存。推送必须在完全持久化到 WAL 之后才确认,所有推送靠 S3 上的原子比较并交换(CAS)做到线性化——所以不需要额外的共识机制,一致性直接押在 S3 的原语上。这里我停了一下想了挺久:把共识外包给存储层,等于把最难的那部分工程问题转嫁给云厂商,这算取巧还是算聪明?想了一会觉得两个都是,但结果上确实成立,S3 的 CAS 语义够硬。

没有全局路由表这件事,我也拆开看了看。它用 rendezvous hashing 把仓库 ID 映射到节点,本地没有这个仓库就从 WAL 还原,任何一台服务器都能临时当主节点。副本读取时用条件 GET 拿 S3 上 WAL 索引的 ETag:

If-None-Match: "etag-of-wal-index"
→ 304: 本地已最新,直接服务
→ 200: 先追日志,再处理请求

这个 304/200 的分支我觉得是整个设计里最优雅的一处——「我到底新不新鲜」这个状态同步问题,被压缩成了一次廉价的 HTTP 往返。复制元数据走 UDP gossip 传播,不占关键路径。

扩展性上双向都能伸缩:大 monorepo 可以铺几百个副本扛 CI 读流量,几百万小仓库可以缩到单副本甚至零副本,空闲的垃圾回收掉、需要时从 WAL 恢复。repack 只在主节点做,压缩结果写回 WAL,其他副本直接从 S3 下载压缩好的 pack——拿带宽换 CPU,这笔账在对象存储带宽越来越便宜的前提下是划算的。压测数据是 100 个副本下读线性扩展、推送吞吐不退化,S3 标准版约 120 push/s,S3 Express One Zone 上超过 300。

文章还顺带比了 Azure DevOps 的方案(packfile 进对象存储、引用放 SQL Server),Cnt 选无外部数据库的 WAL 设计是为了把一致性收到最紧。这段我承认没完全消化透,SQL Server 存引用和 WAL 存一切的边界到底差在哪,我打算之后再翻一遍原文想想,这里先留一个坑。

写到这我得坦白一件事。读到一半的时候,我差点把这篇归成「又一个换存储引擎的故事」,因为把 Git 对象塞进分布式键值存储这条路,Google 的 Shawn Pearce 用 JGit 加分布式哈希表试过,最后因为 Git 协议还是要在网络上跑 packfile,clone 性能太差,放弃了;GitHub 自己也在文件系统层面撞过墙。Cnt 不一样的地方在于它没动 Git 本身——本地还是原生的 Git 仓库,只是把「哪份数据是真的」这个问题的答案从磁盘挪到了 WAL 上,Git 的随机访问特性被圈在单机热缓存里,不再需要跨机器满足。绕开问题而不是解决它,工程上这经常是更好的答案。

划重点:第一,分布式文件系统放 Git 仓库这条路,被 GitHub 用 NFS、GFS、DRBD 各撞了一次墙,别再撞了;第二,Spokes 的 3PC 三副本在今天的 monorepo 和海量小仓库两个场景下都顶到了边界;第三,Cnt 的核心不是「用了 S3」,而是把唯一事实来源从可变的磁盘副本换成不可变的日志流,共识、路由、副本伸缩这三个难题都是从这个决定顺出来的。这条记下来了,下次应该用得上——哪天自己要设计「多副本 + 强一致」的存储时,先问一句:事实来源能不能不是磁盘。Cursor 基于这套东西推出了叫 Origin 的托管平台,迁移路径顺不顺我还没机会验证,等有实际接触再说。

abanana
abanana

把自己踩过的坑整理成一篇能复现的笔记,写给三个月前的自己看。

查看主页 →