今天老冯来和大家聊一聊《母猪的产后护理》。俺做的新开源项目 —— SOW,翻译成中文,就是老母猪的意思。

做一个 PostgreSQL 发行版,最折磨人的往往不是把软件编译出来。而是收拾编译出来的东西。

Pigsty 要为多个 Linux 发行版、多个 CPU 架构、多个 PostgreSQL 大版本维护成百上千个组件。不同组合一路展开,最终落到仓库里的制品超过十万个:RPM、DEB、索引、签名、校验和、快照,还有一堆为了兼容包管理器而存在的元数据。

用户看到的只是 apt installdnf install。维护者看到的却是另一幅画面:你只更新了一个包,却必须保证另外九万九千九百九十九个对象没被误删;你只改了一份索引,却必须保证全球用户不会在切换瞬间读到一半新、一半旧的仓库。当仓库小的时候,这些事都像脚本题。仓库大到十万个制品之后,它突然变成了一道数据库题、分布式系统题,还是一道供应链安全题。

所以我写了 SOW[1] —— 一个用 Go 编写的自包含 APT/YUM 软件仓库管理器。

打开网易新闻 查看精彩图片

如果你只是想把一目录 RPM / DEB 变成可用仓库,一条命令就够了:

sow create /www/pigsty

如果你要长期维护仓库,SOW 还提供 Managed 模式:一份包体投影成多个发行视图,记录期望状态与已构建状态,生成不可变快照,计算精确变更集,再增量发布到文件系统或对象存储。

一句话概括:SOW 把“生成软件仓库索引”这件小事,和“治理一个长期运行的软件仓库”这件大事,装进了同一个单文件工具里。

这样的工具纯粹是为了解决老冯自己的问题。但如果你也维护着大型跨 Linux 发行版仓库,它应该也能帮到你。虽然有这样需求的用户也不会很多就是了。

为什么叫 SOW?

这个名字值得单独讲讲。在之前,我们做的另一个配套开源项目叫 ,这是 PostgreSQL 生态的一个包管理器。那么既然有小猪来装包,那么制作这些包,负责承载、组织和分发这些软件制品的仓库工具,自然就是“母猪” SOW 了。

打开网易新闻 查看精彩图片

它还可以展开成 Software Object Warehouse —— 软件对象仓库。一段工业史里的术语,正好严丝合缝地落到了软件供应链里。而且更妙的是,它还有另外一层双关 —— 在传统铸铁场里,铁水先流入一条主槽,再分流到两侧的小槽里,冷却成一块块铁锭。那些铁锭叫 Pig,承载和分配铁水的主槽叫 Sow。从上方看,一条大槽带着一排小铁锭,正像一头母猪带着一窝小猪。

打开网易新闻 查看精彩图片

为什么要再造一个仓库工具?

最直接的诱因,来自 Pigsty 的离线安装。Pigsty 会先把安装所需的 RPM / DEB 下载到本地,再生成一个离线软件仓库。过去,RPM 系统依赖 createrepo_c,Debian / Ubuntu 依赖 dpkg-dev。真正要生成的不过是几份 XML、Packages 与压缩索引,准备工具链却要装进几百 MB 的依赖。

这在 Linux 上已经够啰嗦;到了 macOS 上更难看。你得启动不同的 Linux 容器,挂载同一份目录,分别跑 RPM 与 DEB 工具,再把结果搬回来。为了生成几 MB 元数据,先请来几百 MB 工具链和一支容器车队,怎么看都不优雅。

打开网易新闻 查看精彩图片

到了 Pigsty 4.5,我把这套冗余清掉了。SOW 是一个几 MB 级的自包含二进制,在 Linux 和 macOS 上都能直接运行,同时理解 RPM 与 DEB,APT / DNF 仓库的规范。没有守护进程,也不需要额外语言运行时。

但“少装几个工具”只是表面问题。真正让我决定把 SOW 做下去的,是仓库规模扩大后暴露出的四个痛点。

第一,硬链接救不了对象存储

同一个 noarch RPM 可能同时出现在多个架构仓库里,同一份包也可能进入 beta、latest、stable 等多个视图。放在本地磁盘上,可以用硬链接让许多路径共用一份 inode;上传到 Cloudflare R2、OSS 或其他对象存储后,每个 object key 都会变成一份实打实的存储与上传成本。文件内容相同,不代表云端知道它们应该共享所有权。

第二,十万个文件让“比较一下”都变得昂贵

仓库更新通常只变动几十个包,但传统同步工具为了确认这一点,往往要把十几万个文件重新遍历、比较、校验一遍。全量检查一次花十几分钟并不稀奇;真正的数据传输可能只有几秒,时间全耗在“什么都没变”的证明上。

打开网易新闻 查看精彩图片

第三,在线仓库不应该露出半成品

一个 RPM 仓库不只是 .rpm 文件;客户端先读 repomd.xml,再沿着它找到 primary、filelists 和包体。APT 同理:ReleaseInReleasePackages 与 by-hash 文件之间有严格引用关系。

如果更新顺序错了,客户端就可能先看到新指针,却找不到新指针引用的对象。对维护者来说只是几秒钟的上传窗口,对全球随机到访的用户来说,就是一次无法复现的 404、校验失败或安装中断。

打开网易新闻 查看精彩图片

第四,目录没有版本,发行版需要版本

最核心的问题是,当老冯想要进一步的改进仓库,提供 Channel 的能力,之前的维护模型就会遇到这些问题:如何同时维护 beta、latest、stable 的仓库?如何每月保存一个快照?如何回答“上周三到底加了哪些包”?如何安全回退?哪些旧对象已经没有任何快照引用,可以删除?

这些需求单独看都能用脚本拼出来;组合到一起,脚本就会长成一套没有事务、没有模式、没有审计的影子数据库。市面上当然有 createrepo_cdpkg-scanpackagesrepreproaptly,也有通用同步与对象存储工具。但我没有找到一个足够轻、同时把 RPM 与 DEB、单份包池、不可变快照、原子发布和增量交付放进同一套清晰模型里的开源工具。

幸运的是,自己做工具的成本从来没有像现在这样低过。

两种复杂度,两种模式

SOW 没有假装所有仓库都需要同样的治理强度。它把问题切成 Plain 与 Managed 两层:小问题保持小,大问题才使用完整状态机。

Plain:包目录就是事实

Plain 模式只有一个核心命令:

目录里的 RPM / DEB 是唯一权威事实,repodata/PackagesPackages.gz 都是可以随时丢弃重建的投影。SOW 会并行扫描顶层软件包,每个包在默认路径上只打开一次,在同一遍里完成 SHA-256、解析和渲染所需事实的提取,再生成两种仓库元数据。

生成结果先进入同文件系统的私有 staging 区,经过 SOW 自己的解析器校验后再替换公开文件。最后,它只重新比较文件集合与 stat 快照,确认构建期间没有包被新增、删除或替换;不会为了“再放心一次”把所有大包重新哈希一遍。

打开网易新闻 查看精彩图片

Plain 不保存操作日志,也不做沉重的事务恢复。进程中断了,就重新执行同一条 sow create:包目录还在,索引只是派生状态,重建比恢复更便宜。

--pigsty 模式还会最后写入 repo_complete 完成标记。只要 marker 不存在,消费方就知道这份仓库还没准备好。这是一个很小、但非常实用的提交协议。

这套模式解决了 Pigsty 最初的痛点:用一个小二进制替代两套工具链与多个容器,快速得到能被真实 APT、DNF/YUM 客户端消费的仓库。

Managed:仓库不是目录,而是状态机

长期运行的仓库不能只看“目录里现在有什么”。它还必须知道你想要什么、上一次成功发布了什么,以及两者为什么不同。

SOW 的 Managed 模型分成四层:

打开网易新闻 查看精彩图片

Workspace 是配置与发现边界;Repository 是所有权边界;Dist 是一个具名的 RPM 或 DEB 成员集合;Architecture View 只是渲染结果,不再拥有一份软件包。这里最重要的不变式是:在一个 Repository 内,每个软件包只有一份正典包体,不留副本。

repo/dists/trixie/main/binary-amd64/...    APT 元数据视图

noarch RPM 或 all DEB 可以被投影进多个架构索引,却不会复制包体。beta 与 stable 也可以引用同一个软件包对象,而不制造第二份云端 object key。而且更妙的是,APT 和 DNF 仓库可以在同一套目录体系下管理。

打开网易新闻 查看精彩图片

“只存一份”的边界必须说准确:它是一个 Repository 或一个发布前缀,不是整个 Workspace、bucket 或全世界。不同 Repository 之间不做隐式去重,因为去重不能以破坏所有权为代价。删掉一个仓库,绝不能顺手删掉另一个仓库依赖的共享对象。

Desired、Built 与 Generation

Managed 模式把仓库状态拆成三个概念:

状态

含义

Desired

配置、加包、删包操作想要得到的成员集合

Built

上一次完整渲染、校验并提交成功的公共视图

Generation

对某个 Built 状态的不可变清单

这个区分看似学院派,实际上专门解决失败场景。

假设你一次加入五千个包,Desired 已经改变,但构建在中途被 SIGKILL。没有这层区分,系统只能面对一棵“不知道改到哪儿”的目录;有了它,SOW 可以诚实地说:意图已经更新,上一个 Built Generation 仍完整对外服务,新操作处于待恢复状态。

Generation 不是把整个仓库再复制一遍。它保存的是不可变 manifest、元数据与包体引用集合;多个快照可以引用同一份 Pool 对象。两代 Generation 之间的精确差异就是 Changeset:新增哪些包体、替换哪些元数据、切换哪些指针、哪些旧对象在保留期后可以删除,一目了然。

所以增量同步不再从“重新扫描十万个文件”开始,而是从“比较两个已知 Generation”开始。

打开网易新闻 查看精彩图片

原子切换的秘密:最后才动指针

软件仓库没有一个跨文件、跨对象的全局事务。SOW 的做法不是假装它存在,而是把发布顺序设计成可证明的协议:

 包体          元数据          指针          删除旧对象

先放入不可变包体;再写 checksum 命名的元数据与 by-hash 索引;全部就位之后,最后才切换 repomd.xmlRelease / InRelease 这些客户端入口。只有旧指针已经不再引用旧文件,并且保留期与证据门禁都满足,旧对象才允许删除。

因此,客户端沿着一个生效指针往下走时,它引用的内容一定已经存在。对单个协议视图来说,读者看到的要么是完整旧代,要么是完整新代,不会看到一棵被撕裂的树。

在本地 POSIX 文件系统上,这套过程依赖同盘 staging、fsync、原子 rename、稳定路径锁和持久操作日志。每条 Managed 写命令都会先检查并恢复上一次未完成操作,再开始自己的工作。恢复只根据已经落盘的证据判断该回滚还是前滚;证据矛盾时宁可失败关闭,也不提供一个可能猜错的 repair --force

对象存储没有多 key 原子提交,SOW 就先持久化 commit intent,再按确定顺序前滚协议指针,并为每个 target 单独保存 Applied Checkpoint。当前文件系统与 R2 发布各有自己的证据,前者成功绝不会被误认为后者也成功。R2 端如果缺少足够安全的条件删除证据,垃圾回收就只报告候选,不冒险远程删对象。

这也是 SOW 与一条 rclone sync 命令最本质的区别:传输文件不难,困难的是知道哪些能传、何时算提交、失败后往哪边恢复,以及哪些真的可以删

十万个对象,性能不能靠信仰

SOW 0.3 的主要工作,不是继续堆功能,而是把已经成立的模型推到真实仓库规模。

Plain 路径现在每个包只做一遍内容读取、哈希与解析,并通过 --jobs 使用有界并发。输入相同,元数据逐字节确定;无需更新时返回 no-op,也不会为了“更新一下时间戳”替换公开 inode。

Managed 路径则把解析后的“软件包事实”按不可变 SHA-256 缓存在 SQLite 中。新包入库时完整认证并解析一次;后续构建批量载入事实,在内存里完成成员投影。暖构建仍会遍历公开命名空间,但对未变化的 Pool 文件只检查 device、inode、size、mtime、ctime 指纹,不再把所有包体重新读一遍。指纹漂移时才回退到一次权威 SHA-256,并自动修复缓存;需要全量密码学审计时,显式运行 sow check

这类优化的价值,必须落在数字上。

在项目基准中,一个包含 5000 个对象的 Dist,成员展开从约 4.1 秒降到 33 毫秒;50000 个对象原先十分钟仍跑不完,现在约 300 毫秒完成。载荷提升也改成了有界单写入者组提交,每批最多 512 个对象或 1 GiB,既减少 fsync 风暴,也保证文件描述符和恢复状态不会随着仓库规模无限增长。

这些数字不是为了做一张跑分海报。它们只是说明:当仓库真的有十万个制品时,“状态模型正确”只是及格线,“日常小改动仍然足够便宜”才决定工具能不能被长期使用。

推倒第一版,再从最小闭环长回来

SOW 的开发时间不短,中间还经历过一次相当彻底的推倒重来。

最初那一版后来以 v0.1.0 留档。它野心很大:用 Git ref 管理仓库视图,用 SHA-256 CAS 保存制品,同时处理上游同步、多目标云发布、校验、修复、垃圾回收、Cloudflare Worker、CDN purge、边缘验证和生产迁移。

这些功能很多都已经做出来了,部分路径也跑过真实 APT/DNF 与非生产 R2 验收。但它的问题同样明显:仓库模型、云厂商、CDN、边缘运行时与迁移流程耦合得太紧。一项功能的正确性,要靠另外半套系统才能证明;任何小改动都会拖着一长串验收矩阵一起移动。

我最后决定把它封存。

推倒的不是目标,而是抵达目标的方式。一个基础设施工具最怕“什么都有一点,但没有哪一层能单独说清楚”。所以第二版先把最小闭环重新切出来:

P0 / Plain Create: 一个目录进去,一个可用仓库出来;•P1 / Managed Control Plane: Workspace、Repository、Dist、Membership、Build、Generation、Check、Changes 与 Operation Log;•更复杂的同步、远端发布、CDN 与供应商控制面,逐项回到独立验收队列。

v0.2.0 建立了今天的 Plain + Managed 主体:单份包池、元数据视图、确定性构建、锁、日志、崩溃恢复、Generation、文件系统与 R2 发布。

v0.3.0 没有再造一层概念,而是集中清理旧 V1 运行时,收敛云端传输边界,并解决 Plain 与 Managed 在大仓库上的重复读取、逐对象查询、载荷提交和可观察性问题。当前正式二进制只依赖新的 V2 核心,旧实现留在 Git 历史与 v0.1.0 / v0.2.0 标签里,作为经验,而不是第二套事实来源。

这条路看上去比“一次憋个大的”慢,其实更快。每一层都有独立契约、失败语义和真实客户端验收,下一层建立在已经站稳的地基上,而不是建立在一份越来越难读的愿望清单上。

打开网易新闻 查看精彩图片

接下来的 Roadmap

SOW 0.3 已经能创建仓库、管理成员与快照、计算变更集,并发布到文件系统和 R2;但它离我脑子里完整的软件制品控制面还有距离。

接下来主要有四条线:

1.上游仓库同步。 直接消费 APT/YUM 上游索引,验证签名与摘要,只拉取缺失制品,并把镜像结果纳入同一套 Package Object、Membership 与 Generation 模型。2.更完整的增量交付。 现在 changes 与 target checkpoint 已经能描述、复用并只发布差异;下一步是扩展对象存储与同步供应商覆盖,把大规模远端 inventory、断点恢复、条件写入和安全删除证据做成稳定闭环。3.CDN 与缓存控制。 CDN purge 不是“调一下 API”这么简单,还要绑定精确 Generation、缓存 TTL、回执与失败恢复。V1 已经证明这条路可行,但它会以独立、可验收的模块重新进入,而不是重新和仓库核心焊死。4.版本与保留策略。 beta、latest、stable、月度快照这些需求,现在已经可以用 Dist、Generation、retain 与 target 组合表达;后续还会补上更高层的策略编排,让常见发布节奏不必由外部脚本手工串联。

这些能力在第一版里大多已经有过实现。我不打算把旧代码整块搬回来,而会像 0.2、0.3 一样,一次只拿回一个边界清楚、能独立验收的能力。

不再憋大招,持续交付小而完整的闭环。

猪猪家族还在继续长大

SOW 是 Pigsty “猪圈宇宙”的一部分,而且这个命名体系已经越来越离谱,也越来越完整:

Pigsty:•SOW:母猪,负责组织、构建和发布软件仓库;•Boar:公猪,开发中的 PIGSTY GUI 管控平台;•Silo:•Oink:•Snort:猪拱,负责收集日志与监控指标

这当然首先是一套命名梗,但它背后也慢慢长出了一条完整链路:SOW 整理制品,Silo 存放制品,Pigsty 把它们安装成系统,Snort 观察系统,Oink 把一切讲清楚。而 SOW 填上的,正是过去最容易被忽视的那一段。

软件仓库看起来只是一个能被 Nginx 托管的目录,但当它承载十万个对象、多个操作系统、多个架构与无数用户之后,它其实是一台没有界面的数据库:有对象、有关系、有版本、有事务、有日志、有垃圾回收,还有绝不能写错的提交指针。

SOW 做的事情,就是把这些隐含规则变成显式模型,把一堆“祖传脚本应该没问题”的侥幸,变成可以检查、恢复和审计的工程契约。如果你只想建一个离线仓库,从一条命令就可以开始。

如果你也在维护一套长期运行的软件发行版,欢迎来看看它更深的一面。SOW 采用 Apache-2.0 许可证。当前 0.3.0 版提供 Linux / macOS 的 amd64、arm64 归档,以及 Linux RPM / DEB 安装包。

•项目主页:sow.pgsty.com/zh[2]•使用文档:sow.pgsty.com/zh/docs[3]•下载:sow.pgsty.com/zh/download[4]•源代码:github.com/pgsty/sow[5]

References

[1] SOW: https://sow.pgsty.com/zh/
[2] sow.pgsty.com/zh: https://sow.pgsty.com/zh/
[3] sow.pgsty.com/zh/docs: https://sow.pgsty.com/zh/docs/
[4] sow.pgsty.com/zh/download: https://sow.pgsty.com/zh/download/
[5] github.com/pgsty/sow: https://github.com/pgsty/sow

打开网易新闻 查看精彩图片

数据库老司机

点一个关注 ⭐️,精彩不迷路

跟数据库老司机学习母猪的产后护理