资讯 一千个 agent 同时 push:Aspen 把 Git 服务端从磁盘目录换成 Postgres + 对象存储

2026-11-08 21:31:15

一千个 agent 同时 push:Aspen 把 Git 服务端从磁盘目录改成 Postgres + 对象存储

经典 Git 服务器把每个仓库当成磁盘上的一个目录。这个模型在「几十个人、每天几十次 push」下没问题,换成成百上千个 agent 各自推自己的分支、CI 在每次 push 后都 fetch,瓶颈立刻暴露出来:每次 push 要对仓库加锁,每次 fetch 服务器都要为这个客户端现做一次 pack。服务器的 CPU 时间花在锁和重复压缩同样的对象上,工作量随客户端数量增长,而不是随新代码量增长。

Aspen 是把这套假设整个换掉的实现。

  • 项目地址:(Apache-2.0,pre-alpha)
  • 线上 demo:,共享像素画布,每一笔都是一个 git push
  • 设计说明:
  • 灵感来源:OpenAI《Scaling Git at OpenAI》里的内部 Git 后端 Pando,以及 Cursor《Git at any scale》里的 Origin。Aspen 是独立实现,与 OpenAI / Cursor 无关联,不共享代码。

放弃的第一个保证:所有分支的同一时刻快照

普通 Git 服务器隐含一个语义:你在某一刻看仓库,所有分支处于同一时刻。Aspen 不要这个。

每个分支在 Postgres 里是一串版本,独立移动。唯一争用的写是该分支指针上的一次 compare-and-swap:「把这个分支从提交 A 移到 B,前提是它仍指向 A」。推到不同分支的操作从不互相等待。同一分支的两次推送在该行竞争,一个赢,另一个拿到 Git 常见的 "fetch first" 再 rebase。

代价很直接:同时看多个分支的读者,看到的分支可能各自处于不同时刻。对于「每个 agent 一条分支、CI 只关心自己那条」的负载,这个代价几乎为零;对于需要跨分支原子视图的场景,就得自己处理。

放弃的第二个保证:为每个客户端定制 pack

Aspen 从不解包 push。它检查 pack,补上服务器已有而客户端省略的对象(thin-pack completion),原样存入对象存储。

存储后端:

  • S3 / S3 兼容
  • GCS 走 S3 API + HMAC key
  • 本地目录
  • Azure Blob 暂不支持

fetch 时算出对方缺哪些已存 pack 直接流式发送,不重新压缩。也可以发签名 URL 让客户端从 bucket、pack cache 或 CDN 下载。启用 fetch.uriprotocols 的客户端发链接,其余客户端由 stitcher 把选中的 pack 拼成一个发出去。

代价是没有「为这个客户端优化过的 pack」,客户端拿到的是服务器上已存在的对象块。换来的是服务器不再因为客户端数量而重复做同样的事。

push 只能看到 trunk

普通服务器在 push 前会把所有分支列给客户端,好让客户端跳过服务器已有的对象。百万分支时这个列表本身就巨大,而且客户端打出来的 pack 取决于当时列了哪些分支,别人没法复用。

Aspen 只列 trunk 分支。push 因此带上自 trunk 以来的全部对象,拿到 trunk 的人用这一个 pack 就能 fetch 该分支。push 体积略大,但会话大小与分支数无关。

可见的差异是行为层面的:

  • git push --force 和 --delete 需要额外加 -o force,或使用 lease,或用分支级 URL。
  • 它不实现 OpenAI fork 里的 explicit-haves 扩展,所以裸 --force-with-lease 不行。

后台合并

小 pack 在后台按层合并,像 LSM tree。层的边界同时当版本跳表用,所以规划一次 fetch 的复杂度是客户端落后距离的对数级——落后几千次 push 的客户端拿到的是几十个 pack,不是几千个。

与 Pando / Continuity 的差异

  • 没有仓库级 WAL:Cursor Continuity 用一个 S3 上的 write-ahead log 给所有 ref 一个一致视图;Aspen 只给同一分支的 push 排序,因此没有单个日志的 CAS 延迟卡住整仓推送速率,也没有全分支快照。
  • 磁盘上没有 Git 仓库:Continuity 在本地 NVMe 保留正常 Git 仓库当缓存并避开关系库;Aspen 完全不落仓库,pack 在对象存储、元数据在 PostgreSQL(单机可用 SQLite),任何一台 server 都能回答任何请求。
  • 给原版客户端拼 pack,stock git 不改配置也能被服务。
  • 自包含 pack:thin pack 入库前补齐 base。
  • 精确 shallow pack:CI 常 --depth=1,对分支 tip 的 --depth=N(N 上限 64)fetch 会得到一个按版本与深度构建一次并存储的 shallow pack。
  • 服务端 merge queue:ASPEN_MERGE_QUEUE(默认关),把多个改动各自作为 commit 在一次 trunk 更新里落地。
  • 单进程开发模式:aspen dev 用 SQLite + 本地目录跑完整服务。

Quickstart

没有发布二进制,必须从源码构建。Rust 工具链版本锁在 rust-toolchain.toml,首次使用自动安装;需要 C 编译器(build-essential / Xcode CLT);小机器编译可能超十分钟。

git clone https://github.com/reason-machines/aspen
cd aspen
cargo install --locked --path crates/aspen-cli
aspen --version

团队用有 Docker Compose stack、Helm chart,以及 AWS / Google Cloud / Azure / Hetzner 的 Terraform。mirror mode 把 Aspen 放在 GitHub 或 GitLab 前面做代理,不用迁移就能试。

性能

在 8 核服务器 + S3 Standard + 4 核 Postgres 上,通过 Aspen 的分支 URL 往 git.git 推送,5 分钟内稳定 567 push/s 无错误。每次 push 在 Git 打印 "ok" 之前都已写入 S3 和 Postgres;推送延迟中位 208ms、p99 314ms;另一个客户端在推送确认后中位 30ms 看到它。作者明确说这是 toy model 不是 benchmark,数字是自己测的。

正确性靠差分测试:把随机 push、删除、竞争同时重放到 Aspen 和 Git 自带服务器,要求结果完全相同;每晚跑 Git 2.30 到最新版以及 JGit;另有 fuzz 和推送中途杀进程的混沌测试。项目自称主要由 agent 构建:四天、151 个已合并 PR,从首提交到公开。

Known limits

限制表现
partial clone不广告 --filter,git clone --filter=... 退化为完整克隆
shallow fetch除分支 tip 的 --depth=N(N≤64)外,返回所请求内容的完整历史(带正确 shallow 边界)
仓库类型无 SHA-256 仓库、无 Web UI、无 code review / issues(用 mirror mode 留在 GitHub/GitLab)
taggit fetch 不会自动跟随新 annotated tag;git fetch --tags 和 git clone 会
SSH只接受 Ed25519 与 ECDSA
限流每实例内存态;没有按仓库的存储配额
mirror mode只支持 HTTP(S) 上游(无 SSH,凭据只走 HTTPS 或 loopback),不支持 Git LFS
测量缺口大仓库上 compaction 与 multi-ref push 的存储放大尚未测量
状态pre-alpha,未过自己的发布门,没有 release,存储格式可能变

安全模型要点(docs/SECURITY.md)

  • 请求头必须在首字节后 30s 内到齐;HTTP/2 每 30s ping,单连接最多 100 条流。
  • fetch body 整体读,上限 9 MiB(gunzip 后同样计),v2 解析器上限 8 MiB;push body 流式,受每次 push 字节上限与每小时字节预算约束;body 60s 无字节失败,等待 10s 后平均速率低于 1 KiB/s 也失败。
  • 并发名额:per caller 默认 16/4/8(fetch/push/stitch),超出 429;每实例 ASPEN_MAX_CONCURRENT_* 默认 32/16/32,超出进有界队列(64,最多等 10s)然后 503,都带 Retry-After。
  • /healthz 不碰任何依赖;/readyz 用最多 1 秒旧的数据库检查结果。
  • 指标在私有监听 ASPEN_METRICS_LISTEN(默认 127.0.0.1:9464),只有 loopback 的 aspen dev 才和 Git 共用端口。
  • delta 深度上限 4095,重复对象拒收,对象数与 delta 大小在分配前按 header 校验。

在 pre-alpha、没有 release、存储格式可能变的前提下,不要把它放在存不能丢的数据的地方。

复制全文 生成海报 Git Rust PostgreSQL 自托管

推荐文章

程序员茄子在线接单