← 返回信息流
AI 资讯Hacker News·1 天前

我教水桶学会了Git

原标题:I taught a bucket to speak Git

速览

这是一篇充满极客幽默的技术创意文章。作者通过巧妙的硬件改造和软件集成,成功让一个普通水桶能够响应Git命令。该实验虽无实际工程价值,但生动展示了物联网、命令行工具与物理世界交互的可能性,体现了开发者对技术边界的趣味探索。

AI 深度解读

我教一个存储桶“说” Git:将 Git 服务器运行在对象存储之上

背景

在将 Agent 沙箱移植到 Go 语言的过程中,作者构建了一套基于 billy 的抽象文件系统层。billy 原本是为 go-git(一个纯 Go 实现的 Git 协议和数据格式库)设计的,旨在让 Tigris 对象存储桶表现得足够像一个标准文件系统,从而欺骗 Shell 解释器及其工具,使其无法分辨差异。

然而,作者发现自己在正常使用场景之外大量使用了 billy。既然 go-git 的原生语言就是“文件系统”,而作者已经拥有一个能像文件系统一样运作的存储桶,一个大胆的想法由此诞生:如果直接将 Git 服务器指向对象存储桶(Object Storage Bucket),会发生什么?

传统的 Git 服务器通常严重依赖本地文件系统,这导致了单点故障风险,并使得 Git 仓库成为云原生无状态环境中最“有状态”的服务之一。作者希望探索是否可以通过纯软件层面的抽象,打破这一限制。

核心内容

Git 的本质:一个对象存储系统

如果剥离 Git 的表层操作(porcelain),Git 仓库本质上只包含四种基本事物:

  1. Objects(对象):压缩后的数据块(blobs)。在大多数仓库中,这些对象就是文件。
  2. Trees(树):映射到其他对象的对象。简而言之,树就是文件夹。
  3. Commits(提交):指向一个树及其父提交的对象。这使得我们可以确定哪些文件属于一个逻辑变更集。
  4. Refs(引用):分支和标签,它们是指向对象堆栈中的微小可变指针。

作者指出,许多人误以为 Git 只存储对空文件夹的补丁(patches),并通过补丁重建历史。事实并非如此,Git 实际上跟踪的是整个文件。这也解释了为什么大型二进制文件会让工具链变得笨重。虽然“差异(diff)”的心智模型适合日常使用,但在存储层它是错误的。

例如,在一个包含 README.md 的新仓库中,.git 目录结构如下:

.git
├── COMMIT_EDITMSG
├── config
├── HEAD
├── index
├── objects
│   ├── 5e
│   │   └── b8151eb669aa4467b6dea2c4bce19183cd0b41
│   ├── 6a
│   │   └── 6a8ecfcae2632152486aca3d9150ef83dedd66
│   ├── f4
│   │   └── d2487a1c6d742c8037c0296ddf80625190bd80
│   ├── info
│   └── pack
└── refs
    ├── heads
    │   └── main
    └── tags

这里包含三个对象:一个 commit、一个 tree 和一个 README 文件。main 分支指向该 commit。关键在于,一半的内容是内容寻址(content-addressed)的。一旦提交,内容寻址的部分永远不会改变。这与 Tigris 的追加写入(append-only)存储模型完美契合。经常变化的部分是 refs,但它们只是微小的文件,对象存储可以轻松处理。

传统 Git 服务器的痛点

当我们在服务器上托管 Git 仓库时,通常会将 Git 对象与文件系统对象进行 1:1 关联。即使是 GitHub 这样的巨头,也依赖于挂载的大型文件系统,因为 Git 的工具链(如 /usr/bin/git 二进制文件)没有提供其他选项。这使得 Git 仓库成为单点故障的源头,且难以在去中心化的云环境中扩展。

构建无本地文件系统 Git 服务器的挑战

若要构建一个不依赖本地文件系统的 Git 服务器,传统的替代方案并不理想:

  • 调用 git 二进制文件:这意味着库变成了进程参数(argv),错误处理依赖于屏幕抓取输出。Git 内部通过大量子命令实现功能,而非暴露为库,代码库中充满了 die() 调用,这会直接杀死进程。
  • 链接 libgit:继承“出错即崩溃”的行为,导致应用随机崩溃,严重影响可用性。
  • 使用 libgit2:虽然它是真正的库,但受 GPL 许可证困扰(尽管有链接例外,但法律解释复杂),需要频繁处理 C 语言接口,开发停滞,Go 绑定已归档,且仍假设存在本地文件系统。

解决方案:go-git 与对象存储的结合

go-git 是一个从头开始的纯 Go 实现的 Git 协议和内部结构库。它不依赖 cgo/usr/bin/git,也不假设仓库存储在本地文件系统。其存储接口基于 billy,这正是作者已经适配 Tigris 的接口。

作者开发了 objgit,一个由对象存储支持的 Git 服务器。为了使其启动,作者只需添加一个文件系统调用 MkdirAll。他将 transport 包连接到套接字以明文实现 Git 协议,并将其挂钩到存储桶,然后推送了当前正在工作的仓库。

结果令人惊讶:它工作了。

Git 的推送、拉取、日志、 blame、标记等所有功能均正常运行。作者并没有重新实现 Git,而是通过大量的适配工作,将方形 peg 强行塞入圆形 hole 中。从逻辑上讲,裸仓库(bare repo)就是文件系统中的那四种事物;将文件系统替换为对象存储,其余部分“正常工作”是完全合理的。Git 的磁盘格式即其数据库模式,只要足够逼真地模拟 open/stat/rename 等操作,整个伪装就能维持。

功能特性

经过大量开发,objgit 具备以下特性:

  • 支持通过三种传输协议进行推送和拉取:HTTP、经典 git:// 和 SSH。
  • 首次推送时自动创建(upsert)仓库。
  • 未投入精力实现身份验证(因为这是一个实验,且认证复杂)。
  • 集成 Prometheus 指标,以便优化文件系统层。

所有功能都打包在一个 Go 二进制文件中,无本地状态。即使生成的 SSH 密钥也存储在存储桶中。可以在 Kubernetes 集群中运行,唯一的可变存储需求是智能 Git 克隆时的临时文件(用于乐观缓存)。

关键要点

  • Git 的本质是对象存储:Git 存储的是完整文件及其元数据(树、提交、引用),而非仅仅是差异。这种内容寻址、追加写入的特性使其天然适合对象存储。
  • go-git 是关键使能技术:作为一个纯 Go 实现的 Git 库,go-git 不依赖本地二进制文件或 C 库,且其存储接口抽象(billy)允许轻松适配非文件系统后端。
  • 文件系统抽象是核心:通过 billy 抽象层,对象存储桶可以被模拟为符合 POSIX 标准的文件系统,从而欺骗 Git 工具链,使其认为自己在操作本地磁盘。
  • 去状态化 Git 服务objgit 证明了 Git 服务器可以完全无状态运行,所有数据(包括密钥)存储在对象存储中,消除了单点故障,提高了云原生环境下的可扩展性和可靠性。
  • 实验性质:作者强调这是一个实验性项目,未经过充分测试或验证,不建议在生产环境中直接迁移核心单体仓库。

意义与影响

这项实验揭示了 Git 架构中一个常被忽视的事实:Git 的磁盘格式本质上就是一个数据库模式。只要能够模拟必要的文件系统接口(如 open, stat, rename),Git 就可以运行在任何支持这些抽象的存储后端上。

对于云原生架构而言,这意味着 Git 服务可以从沉重的、有状态的本地文件系统依赖中解放出来,转变为真正的无状态服务。这带来了以下潜在优势:

  1. 高可用性与可扩展性:对象存储通常具备极高的持久性和可用性,消除了单点故障。
  2. 简化的运维:无需管理复杂的本地磁盘挂载和权限,所有状态集中在对象存储中。
查看原文 →tigrisdata.com