这是本节的多页打印视图。 .
设计归档
- 1: 协同发布决策记录
- 2: SOW 设计演进:从路由图到仓库引擎
- 3: 发布与恢复
- 4: 系统模型
- 5: 设计原则
- 6: 为什么 SOW 每个仓库只保留一棵包体树
- 7: v0.2 重置:用 Plain 与 Managed 取代万能控制面
- 8: SOW v0.1 到底证明了什么
- 9: SOW v0.1 决策账本:哪些被保留,哪些已退役
- 10: SOW v0.1 原始计划:软件仓库的 Git
1 - 协同发布决策记录
SOW 0.4.0 保留了原生 filesystem 与 r2 发布后端。早期提案曾计划使用 rclone,并增加远端审计和预览命令。
最终选择原生后端,是因为 SOW 需要逐项记录条件写入及其结果。
当前命令语法见 sow publish。
为什么保留原生后端
发布计划为每个文件记录预期 SHA-256 和操作。后端需要创建不可变对象,避免覆盖已有内容,并按条件更新可变入口文件。 SOW 记录操作结果,用于中断后继续发布。
批量复制工具需要额外的身份与检查点模型才能保留这些规则。0.4 选择改进已有后端:
- 支持条件式分段上传;
- 分别设置响应头和响应体空闲超时;
- 对可重复执行的操作重试;
- 通过公共 URL 校验变化的文件。
这保留了已有发布状态,也省去了传输工具的迁移。
发布流程
原生后端先上传软件包与不可变元数据,再更新仓库索引。 随后检查公开文件,并为目标记录发布检查点。下次发布以该检查点为基准,计算需要更新的文件。
修改目标设置
目标名称、公共 URL 或缓存时间的变化记录在绑定修订中,原发布历史继续保留。 使用不同存储位置时,需要配置新目标。
继续中断的发布
SOW 为每次发布保存计划与进度。作出提交决定前,确认索引未变化后即可放弃该次发布。 作出提交决定后,恢复流程完成已保存的计划。 上传、校验和恢复因此可以使用同一份发布历史。
单独配置访问服务
运维人员配置 HTTP 访问、缓存与客户端信任。SOW 通过配置的公共端点检查发布结果。
完整执行顺序见发布与恢复,配置与命令见
sow publish。
2 - SOW 设计演进:从路由图到仓库引擎
SOW 在 v0.1 中探索了以 Git 为核心的软件分发控制面,在 v0.2 中转为仓库引擎。 本文记录这次变化,以及后续到 v0.5.0 的改进。
下文版本号指正式 Git 标签。部分归档计划曾用“v0.2”“v0.3”指代开发中的设计; 其中在 RPM 视图内创建硬链接的 C2 布局,已在 v0.2.0 发布前被替换。
早期工作的细节见四篇配套文章:
- v0.1 原始产品计划说明最初的范围,以及范围为何过大。
- v0.1 决策账本记录现存的 44 份 ADR 及其后续变化。
- v0.1 到底证明了什么汇总 112 份按日期记录的测试与评审报告。
- v0.2 重置说明 Plain/Managed 模式拆分,以及从 C2 改为单包体布局的过程。
简要历史
| 版本 | 主要变化 | 公共布局 |
|---|---|---|
| v0.1 | Git/CAS 状态、Route、边缘鉴权、云端工作流与 Pigsty 迁移 | 已退役 |
| v0.2 | Plain/Managed 模式、每仓库一个包池、纯元数据视图、保留 Generation 与目标发布 | 引入沿用至今的 pool/ + dists/ 布局 |
| v0.3 | 移除 V1、缓存包属性、简化 Plain 构建、批量提交包体 | 不变 |
| v0.4 | 单遍校验、独立 RPM 信任环、目标重新绑定、迁移与恢复修复 | 不变 |
| v0.5 | 包路径保留、移除与恢复修复、签名检查、可复现的 Plain 时间戳 | 不变 |
各版本的实现保存在源码标签中: v0.1.0、 v0.2.0、 v0.3.0、 v0.4.0 和 v0.5.0。
v0.1:问题很有价值,产品概念太多
第一版计划覆盖 Git 状态、内容寻址存储、Route 与 View、快照、远端存储服务、边缘鉴权和旧系统迁移。 现存 44 份 ADR(编号至 0045)记录了这些决策及其失败情形。
组合后的范围使所有权难以解释。多个对象都可能描述同一份包体与公开路径。 本地仓库生成也依赖云端、边缘服务和迁移概念。
v0.2 重置移除了 Git 权威数据库、独立 CAS 层、Route/View/Snapshot 命令模型和边缘访问控制, 把本地仓库构建作为核心任务。
v0.2:先建立小而明确的所有权模型
v0.2 将产品拆成两条执行路径:
- Plain 扫描调用者拥有的目录,就地重建 RPM 或 DEB 元数据。
- Managed 管理一个包含独立仓库的工作区。每个仓库都有自己的包池、Dist、SQLite 状态、锁和恢复记录。
这次重置引入了沿用至今的术语:包对象、成员关系、期望状态(Desired)、已构建状态(Built)、 Generation(构建快照)和变更集。稳定锁文件协调访问,协议指针在其他生成文件就绪后替换, Managed 操作日志记录中断后的恢复方式。
最初的 C2 实现仍会在每个 RPM 视图中创建包体硬链接。这一布局在发布前发生了变化。
v0.2.0:把 Repository Root 变成交付单元
单包体决策起草于 2026-08-05,并在 8 月 8 日随 v0.2.0 发布。
它移除了 RPM 视图中的包体别名。一个仓库只有一套 pool/ 与纯元数据 dists/,
完整仓库根目录是复制、托管、鉴权与发布的单位。
v0.2.0 也明确区分了本地与远端状态:
- 包对象、Desired/Built 状态、Generation、变更集与本地保留记录属于仓库。
- 发布尝试、已应用检查点、远端清单、缓存宽限期与删除记录属于一个仓库和一个发布目标前缀。
仓库之间不对包体去重。一个目标前缀只有一个所有者。 兼容性导出在原仓库之外创建独立副本。
v0.3:在同一模型上做减法与优化
v0.3 保持 v0.2.0 公共布局不变。它删除了 V1 运行时与迁移测试工具,引入包属性缓存, 并将 Plain 生成简化为可重建的单遍操作。成员关系改为批量展开,包体改为分组提交,减少文件系统操作。
v0.4:强化校验与恢复
v0.4 将 Managed 深度校验改为有界单遍读取。RPM 验证必须在一个信任环内完整通过, 不能拼接不同信任环的局部结果。显式重新绑定操作可以更新发布目标的可变设置。 迁移、发布恢复与公共交付检查也更严格。
早期协同发布提案曾考虑用 rclone 批量传输。最终设计保留原生存储适配器, 让 SOW 在同一状态机中检查对象身份、记录回执、控制提交顺序并恢复发布。
v0.5:保留包 URL 与已提交状态
v0.5.0 于 2026-09-29 发布,公共布局不变,托管状态升级为 schema 13。 新增的包池路径索引阻止不同字节占用已有 URL,也覆盖历史发布记录中的 URL。
该版本还修复了移除 Dist 后重新添加包的恢复问题,保留中断前已提交的更改,
并在修改期望状态前检查冲突的签名策略。Plain create --metadata-timestamp 为可复现 RPM 元数据
提供显式时间戳,默认值为零。完整变化见
v0.5.0 更新日志。
每次重构都保留下来的原则
v0.1 的部分规则继续用于更小的模型:
- 每个持久事实只有一个所有者。
- 包体与可重建元数据分开保存。
- 不可变包体与元数据就绪后,再替换协议指针。
- 提交意图持久化后,通过前向恢复完成操作。
- 删除前检查引用关系与精确身份;远端删除还要检查缓存宽限期与存储服务能力。
- 兼容性结果记录产生它的源码版本、客户端、存储服务和环境。
3 - 发布与恢复
构建生成本地快照(Generation)。发布把快照中的文件上传到目标,并记录进度,以便中断后继续。 每个目标分别保存检查点,记录上次完成发布的内容。
状态归属
| Repository 记录 | 目标记录 |
|---|---|
| 软件包、Desired 状态与 Built 状态 | 发布尝试与检查点 |
| Generation 与变更集 | 远端清单 |
| 保留的包和元数据引用 | 宽限期与删除记录 |
发布顺序
上传包体时,SOW 创建新对象或复用相同字节,不会用其他内容覆盖不可变路径。 修改第一个可变 APT 索引别名或协议入口文件前,SOW 会先记录提交意图。
每个视图都先上传引用内容,再更新入口文件:
更新期间,各视图可能暂时使用不同构建的索引。分离签名与索引也分两次写入。 因此,客户端可能需要重试索引刷新。公共校验完成后,SOW 才记录检查点。
公共访问校验
SOW 通过 public_endpoint 下载变化的文件和受影响的索引入口,并核对内容。
未变化的软件包无需重复下载。
公共端点可能仍返回缓存中的旧文件。SOW 会在 max_cache_ttl 内等待并重试。
校验失败时,发布不会记录新的完成检查点。
继续或放弃发布
重新运行原来的 sow publish TARGET 命令即可继续。恢复沿用保存的计划,不会改用更新的本地构建。
| 已保存的状态 | 下一步 |
|---|---|
| 尚未记录提交意图 | 重试发布,或用 --abort 核对状态并放弃该次发布 |
| 已记录提交意图 | 重试发布,完成保存的更新 |
| 已记录检查点 | 完成剩余维护;目标已完成更新时无需再次上传 |
| 文件身份冲突 | 检查报错中的文件和目标配置,再重试 |
--abort 清理符合条件的私有文件系统暂存目录,保留远端对象,并记录已放弃的上传。
后续发布可复用字节相同的对象。
目标身份
存储身份由后端、端点、区域和桶组成;目标身份还包括前缀。首次发布将这些值绑定到 Repository, 后续发布据此查找对应历史。
修改目标名称、public_endpoint 或 max_cache_ttl 后,运行
sow publish TARGET --rebind。
SOW 会追加绑定修订记录。Repository、存储身份或前缀变化时,需要配置新目标。
修改 max_cache_ttl 前,先完成该目标尚未结束的维护。
文件系统目标正在执行条件删除时,需等删除流程完成后才能修改 public_endpoint。
缩短 TTL 不会提前结束已记录的缓存宽限期。
移除已发布的视图
目标仍在配置中且已有发布检查点时,SOW 会阻止撤销其现有 Dist、架构或签名入口。
需要停用该目标时,从 sow.yml 的 targets 中删除对应条目,再运行 sow config check。
这会保留旧远端文件与发布历史,不会删除远端仓库。
随后可以修改本地 Dist 配置并重建。要发布删减后的视图集合,配置一个使用不同前缀的新目标,并让客户端使用新 URL。
--rebind 不能修改目标前缀。配置规则见 sow publish。
保留与回收软件包
保留的 Generation 保存元数据、清单和指向共享 Pool 的包引用。本地垃圾回收会检查以下引用:
- 当前 Desired 与 Built 成员关系;
- 保留的 Generation;
- 进行中的操作与恢复日志;
- 发布恢复和宽限期。
只有引用全部消失,且文件仍与记录的身份一致时,SOW 才删除包。 因此,保留 Generation 可以保留其软件包,无需再复制一棵包目录。
文件系统目标删除要求清单完整、目标由当前工作区独占且宽限期已过。 SOW 按记录的文件身份执行条件删除,再检查公共端点是否已返回文件不存在,最后记录结果。 R2 目标 GC 只报告候选并保留对象。
4 - 系统模型
SOW 用配置文件保存仓库设置,用 SQLite 记录托管状态,用公共目录向客户端提供文件。 模型分别记录选中的软件包、已完成的构建,以及已发布的内容。
对象
Workspace
工作区包含 sow.yml、私有 .sow/ 目录和稳定的锁文件。
它集中保存配置,让命令可以找到所选仓库。
Repository
一个仓库拥有一份 pool/、一棵 dists/ 目录、一个状态数据库和一条 Generation 序列。
包去重只在该仓库内进行。两个仓库即使输入相同,也各自保存一份包副本。
包对象
SOW 使用包签名完成后的 SHA-256 标识包对象。名称、版本、release 和架构描述软件包,摘要标识实际字节。 重复加入同一对象会复用已有文件。同一包池路径对应不同内容时,SOW 报告冲突。
Dist 与成员关系
一个 Dist 为一种格式(RPM 或 DEB)选择软件包和设置。
同一个包可以属于多个 Dist,但只保存一个 Pool 文件。
架构中性包(noarch 或 all)出现在每个适用架构的视图中,无需复制包体。
Desired、Built 与 Generation
Desired 状态记录所选软件包和配置,Built 状态记录上一次完成的构建。 Generation 是该次构建的不可变清单,变更集则比较两份清单。
add --skip 会修改 Desired 状态而不重建索引。构建中断也可能留下未完成的操作。
用 sow status 查看差异,用 sow build 应用待构建变更或完成恢复。
发布目标
目标将一个仓库连接到存储后端和前缀。每个目标分别记录发布尝试、检查点、清单和清理状态。
目标身份与配置变更规则见发布与恢复。
状态流
Managed 操作用事务和日志记录进度。恢复时,SOW 根据记录完成或回滚中断的步骤。
公共文件与私有文件
| 作为完整仓库提供访问 | 留在私有工作区中 |
|---|---|
<repo>/pool/ |
sow.yml |
<repo>/dists/ |
.sow/ 数据库与日志 |
| 生成的索引与签名 | 锁、暂存和恢复文件 |
公共目录副本可以提供软件下载。要继续管理仓库,还需保留匹配的私有工作区。 只有公共副本时,无法恢复成员关系、保留记录或发布历史。
锁与写入者
工作区和仓库各有自己的锁。多数读操作使用共享锁,写操作使用独占锁,避免多个进程同时修改同一份状态。
sow status 直接查询锁状态,便于查看正在进行的写入。
每个目标前缀必须由一个工作区负责写入。这些锁协调本地进程,不协调不同主机上的独立工作区。
5 - 设计原则
SOW 分别管理包存储、仓库构建和发布。以下原则说明各类状态由谁保存,以及操作如何使用这些状态。
每类记录由一个对象管理
| 记录 | 管理范围 |
|---|---|
| 包体与包身份 | Repository |
| Desired 成员关系与 Built 状态 | Repository |
| Generation 与变更集 | Repository |
| 目标绑定与修订记录 | Repository 与发布目标 |
| 发布尝试与检查点 | Repository 与目标前缀 |
| 远端清单与删除记录 | Repository 与目标前缀 |
每个 Repository 分别保存自己的包副本,每个发布目标也分别保存一份。 因此,各仓库和目标可以独立管理包的保留与删除。
保存包体,重建索引
Managed 模式将包体保存在 pool/ 中;Plain 模式使用输入目录中的包体。
索引和架构视图描述这些包,可以重新生成。
Managed 垃圾回收只删除已无当前引用、保留引用和恢复引用的包。Plain 模式直接根据当前目录重建索引。
提供完整公共目录
Managed Repository 根目录包含 pool/ 与 dists/。复制或提供整个根目录,客户端才能根据索引解析包路径。
如果消费者需要独立的 RPM 架构目录,使用 sow export rpm-leaf。
工作区的 sow.yml 和 .sow/ 放在 HTTP 根目录之外,其中保存配置、数据库、锁、日志和恢复文件。
先上传包,再更新索引入口
发布按以下顺序执行:
协议入口文件包括 repomd.xml、Release 和 InRelease。
SOW 先上传它们引用的文件,再更新入口文件。旧文件保留到宽限期结束,让缓存了旧索引的客户端仍可下载。
按已记录的状态恢复
Plain 构建中断后,重新运行 sow create,扫描包目录并重建索引。
Managed 发布会记录计划和进度。记录提交意图前,SOW 可以核对状态并放弃该次发布。
记录提交意图后,重新运行 sow publish TARGET 完成已记录的更新。
文件身份冲突时,恢复会停止并指出需要检查的对象。
用实际消费者检查布局
包管理器需要解析包 URL,镜像工具还可能限制输出目录结构。 完整 Repository 与导出的 RPM 叶目录应分别测试,并记录客户端版本与目录布局。 兼容性参考列出自动化覆盖范围。
在本地协调写入
每个目标前缀由一个工作区负责写入。SOW 使用锁协调本地进程,不协调多个独立工作区。
R2 发布支持增量上传。R2 垃圾回收只报告候选并保留远端文件,因为该后端未实现所需的条件删除操作。
6 - 为什么 SOW 每个仓库只保留一棵包体树
这项决策起草于 2026-08-05,随 2026-08-08 的 SOW v0.2.0 发布。SOW 0.5.0 仍使用这一布局: 每个仓库保存一棵包体树,各发行版视图引用同一棵树。
决策
每个 Repository 的公共目录如下:
上图展示 RPM 视图。APT 索引位于 dists/<dist>/main/binary-<architecture>/,
Release 文件位于 dists/<dist>/。
一个包对象在一个仓库内只有一个包体路径。将同一个包加入多个 Dist 或架构视图,只增加成员关系和元数据引用。
复制、静态托管、鉴权和发布都以完整的 pool/ + dists/ 根目录为单位。
单个架构目录是客户端视图,不是完整的软件仓库。
从 C2 开发线到 v0.2.0
早期 C2 开发线在根包池中保存一份包体,再在每个 RPM 视图内创建包体硬链接。
这让部分 reposync 客户端可以直接使用独立的架构目录,同时节省本地空间。
硬链接在其他环节带来几个问题:
- 对象存储把每条路径存为独立对象,重新产生包副本。
- 归档和跨文件系统复制可能丢失硬链接关系。
- 视图内的包路径看起来像独立文件,实际仍由仓库管理。
- 保留与回收容易依赖链接计数,而不是实际包引用。
- 仓库布局需要迁就特定镜像工具的输出目录要求。
v0.2.0 发布前,最终布局删除了 RPM 视图中的包体别名。归档中的 C2 文档描述的是该开发线。
元数据如何引用包池
APT 的 Filename 是相对仓库根目录的路径,因此可以直接引用包体:
RPM 的 <location href> 使用从视图目录到包池的相对路径:
渲染器和检查器使用同一个路径函数。函数计算相对路径、转义文件名,再检查解析结果是否仍指向仓库内的原包路径。
只要 pool/ 与 dists/ 的相对位置不变,就可以把整个公共目录复制到其他目录、HTTP 站点、桶或前缀,
无需改写元数据。
其他方案
| 方案 | 未采用的原因 |
|---|---|
| 每个 RPM 视图放包体硬链接或副本 | 产生路径别名和重复对象存储键 |
| 包体符号链接 | 对象存储不支持,静态服务与归档的处理也不一致 |
使用 /pool/... 绝对路径 |
客户端可能从站点根目录解析,破坏带前缀的仓库 URL |
绝对 URL 或 xml:base |
将元数据绑定到一个站点,搬迁时需要修改 |
| CDN 改写或重定向对象 | 仓库依赖额外的路由配置 |
| 工作区或桶级共享包池 | 需要跨仓库协调锁、保留与删除 |
为镜像工具导出独立目录
包管理器需要解析元数据引用并下载包体。镜像工具还可能要求所有软件包都位于指定视图目录之下。
需要独立 RPM 架构目录时,运行 sow export rpm-leaf。命令在仓库外生成包文件和指向本地文件的元数据。
默认复制包体;同一文件系统可使用 --hardlink,但导出包必须保持只读,因为写入硬链接也会修改包池文件。
导出目录不属于 Generation,不是发布源,也不参与成员关系或垃圾回收。
客户端与镜像工具测试见兼容性参考。
用 sow check 校验本地路径、摘要和文件引用,用对应客户端测试安装与镜像下载。
结果
- 包身份、成员关系、Generation、保留与本地 GC 都在仓库内管理。
- 发布尝试、检查点、远端清单、宽限期与删除记录按目标前缀管理。
- 每个包文件对应一个静态对象存储键。
- 删除包体前检查引用和文件身份,不依赖硬链接计数。
- 复制完整仓库根目录,或通过导出命令生成独立的 RPM 架构目录。
7 - v0.2 重置:用 Plain 与 Managed 取代万能控制面
2026-07-31,v0.1 源码基线封存后,SOW 开始了一次产品重置。 目标是构建一个小型仓库引擎,替代 Pigsty 日常软件包构建工作流。 重置保留了已有的完整性与恢复经验,同时移除了大部分控制面。
最终形成的分工沿用至今:Plain 重建调用者拥有的目录,Managed 管理仓库及其历史。
开发设计最初在 RPM 视图内使用 C2 硬链接,随后在 8 月 8 日发布 v0.2.0 前被替换。 部分归档计划曾把 C2 构建称为“v0.2.0”,但正式标签已经采用单一包池与纯元数据视图。
为什么不在 V1 上逐步删功能
v0.1 把五类任务耦合在一起:
- 构建有效的 APT/YUM/静态制品仓库。
- 维护包历史与快照。
- 迁移庞大的 Pigsty 旧拓扑。
- 发布到多个云存储与 CDN 目标。
- 执行商业边缘访问控制。
逐项删除功能仍会保留 Git/CAS/Route 所有权模型。 重置直接重新确定仓库引擎需要管理哪些事实。
第一阶段排除了:
- V1 Git 数据库、Route/Manifest/Quarantine 模型与全局内容寻址存储层;
- 上游同步、拉取、推送、复制、移动、晋级、边缘权限、CDN 初始化与商业拓扑;
- 远端发布与状态、对象存储、垃圾回收、快照与冻结、Managed 导出和托管服务;
- modulemd、源码包构建、Web 界面、跨仓库去重与多写入者操作。
V1 已经为上游同步和渠道晋级积累了流式处理、模糊测试与确定性验证结果,重置仍将它们移出产品。 包获取改为外部任务。运维者可以用专门的镜像或下载工具准备包集合, 再由 SOW 负责接纳、成员关系、构建与发布。 发布、保留、垃圾回收和外部导出后来在 v0.2.0 正式标签前回归,并遵循更小的仓库所有权模型。
两条隔离的执行路径
Plain:重建调用者拥有的目录
Plain 模式处理调用者拥有的 RPM 或 DEB 目录:
它扫描包、生成协议元数据、验证结果,最后替换协议指针。 Plain 负责生成的元数据,不维护工作区、仓库数据库、Generation(构建快照)历史或长期成员关系。
期望状态就是当前目录中的包,因此中断后重新运行构建即可恢复。 后续版本进一步简化了这条路径,移除了 Plain 日志与重复包体读取。
Managed:拥有生命周期与历史
Managed 模式引入包含多个独立仓库的工作区:
每个仓库管理自己的包池、包对象、成员关系、期望状态、已构建 Generation、变更集、锁、数据库与恢复状态。 仓库之间不自动去重,也不共享计数器。
Dist 是具名的 RPM 或 DEB 包成员集合。架构视图是成员集合的投影。
架构无关包(noarch / all)会进入每个适用架构,而逻辑成员关系只有一份。
哪些决定替换了 V1 概念
| V1 概念 | v0.2 替代项 |
|---|---|
| 作为权威状态的 Git 提交与引用 | 每仓库 SQLite,加显式文件系统记录与操作日志 |
| 全局 CAS 与 Route 物化 | 仓库自己的包池与协议视图 |
| View/Snapshot/Stable 引用图 | Dist 成员、期望状态版本、已构建 Generation 与保留状态 |
| 一套通用控制命令 | 分开的 Plain 与 Managed 命令接口 |
| 隐式旧拓扑 | 严格的 sow.yml、显式名称与选择器 |
| 跨产品发布流程 | 仓库构建与目标发布分离 |
| 直接复用 V1 模块 | 只移植解析、生成、版本比较和签名等独立组件 |
简化后,不借助 Git 或边缘路由器,也能解释私有控制状态与公共仓库内容。
Desired 与 Built
Managed 模式明确记录中断状态:
- 期望状态(Desired):SQLite 中当前的包与成员关系。
- 已构建 Generation(Built):最近一次完整生成并验证成功的公共树。
- 变更集(Changeset):两次已构建状态的精确差异。
新的 Built Generation 完成前,Desired 可以先变化。 操作日志记录未完成的工作,状态查询显示 Desired 与 Built 的差异。 恢复过程根据记录的操作阶段决定还需要完成哪些步骤。
P0 到 P3
实现按依赖顺序推进:
| 阶段 | 内容 |
|---|---|
| P0 | Plain create、真实 RPM/DEB 元数据、签名、Pigsty 过滤、最后替换协议指针 |
| P1 | 工作区、仓库与 Dist 生命周期,严格配置,包身份、成员关系与查询 |
| P2 | 添加、移除、策略与构建操作,日志、崩溃恢复与安全修改 |
| P3 | 检查、变更集、交付、规模、兼容性与发布验证 |
每个阶段都有 API 契约、实现规格、对抗评审与验收记录。
C2 Hardlink 绕路
最初的 Managed RPM 布局在根包池保存包文件原件,并在每个架构视图下创建硬链接。
元数据使用本地 pool/... 路径。
这套 C2 布局通过了固定版本的 AlmaLinux 9 reposync 测试。
别名共享 inode,因此不占用重复的本地包体空间。
部署时,这种布局产生了额外成本:
- 对象存储会将每条路径保存为独立的完整对象。
- 归档与跨文件系统复制可能丢失硬链接关系。
- 每个 Dist 与架构下的别名都表现为独立包文件。
- 保留与垃圾回收容易依赖 inode 和链接计数。
- 完整仓库无法一对一映射为静态对象键。
2026-08-05 的快照记录了这套 C2 实现与本地测试结果。
单包体契约起草于 8 月 5 日,8 月 7 日以“next”设计提交,8 月 8 日正式标签前完成实现。 最终发布的布局是:
默认 EL reposync 因而成为已知兼容性限制,独立 RPM 叶目录改为显式外部导出。
完整理由见单包体仓库。
重置复用了什么
重置保留了不依赖 V1 所有权模型的独立组件:
- RPM/DEB 解析与原生版本比较;
- 元数据生成、压缩、签名与验证;
- 安全文件系统操作;
- 部分客户端测试数据与故障测试;
- 精确包身份与来源检查。
Git 状态、Route 回执、边缘组件、旧迁移机制与云控制面只作为参考或待移除代码, 不再是新核心的依赖。
从验收到发布
开发线在 8 月 1 日记录 P0/P1 验收,8 月 2 日记录 P1–P3 验收,8 月 5 日运行本地发布检查。 单包体实现、打包和 CI 修正进入后,最终 v0.2.0 源码于 8 月 8 日打标签。
正式版本还交付了文件系统与 R2 发布、Generation 保留、外部 RPM 叶目录导出和本地垃圾回收。 R2 远端删除从一开始就被禁用,针对 R2 的 GC 只报告候选对象。
哪些东西仍在定义 SOW
- Plain 与 Managed 是隔离模式,恢复成本不同。
- 工作区负责配置与发现,仓库负责包与历史。
- 包对象身份不可变,成员关系是逻辑多对多关系。
- Desired 与 Built 状态分开记录。
- 写入使用稳定锁文件,最后替换协议指针。Managed 日志记录中断操作,Plain 通过重新构建恢复。
- 命令语法、JSON 输出格式与退出码提供明确的自动化接口。
- 协议、客户端、镜像工具和存储服务分别验证兼容性。
第一手设计快照
- v0.2 开发设计快照: 产品需求、API、C2 架构、评审与本地验收。
- 单包体“next”设计: 正式标签前采纳的设计。
- v0.2.0 发布注记与 v0.2.0 源码标签:正式发布内容。
后续 v0.3 到 v0.5 的改进见设计演进。
8 - SOW v0.1 到底证明了什么
v0.1 归档包含 112 份报告,日期从 2026-07-11 到 7 月 31 日。 它们记录了客户端测试、规模测量、故障恢复、迁移和云存储实验。 本文汇总主要结果,并链接到代表性报告。
每项结果对应报告中记录的源码版本、测试数据、客户端与环境。 当前支持情况见兼容性参考。
证据时间线
| 时间 | 报告数 | 主要工作 |
|---|---|---|
| 2026-07-11 | 3 | 真实 APT/DNF 基线、72,310 个包的清单扫描、5 万个上游包的流式处理 |
| 2026-07-12 | 35 | APT/YUM 协议、CAS/GC、发布顺序、恢复、MinIO、性能、迁移、构建与供应链检查、云测试工具 |
| 2026-07-13 | 2 | RPM 信任验证与选定包集合的物化恢复 |
| 2026-07-14–16 | 14 | 旧拓扑、仓库族迁移、物理所有权、完整副本纳管与包签名清单 |
| 2026-07-17–20 | 39 | Cloudflare/R2/COS/边缘服务安全、服务身份验证、切换回执、配置上限与检查点约束下的删除 |
| 2026-07-22 | 10 | 暂存、应用、清理、回滚与残留删除中的文件描述符和路径身份 |
| 2026-07-26–29 | 7 | 递归持久化、恶意并发写入、替换结果、保留核查副本与存储服务修正 |
| 2026-07-30–31 | 2 | 从空环境验证 APT/YUM/静态制品最小可用产品,核对最终旧版源码基线 |
哪些结论有较强证据
真实客户端与大仓库
最早的测试使用真实 APT 与 DNF 客户端,并测量处理大仓库的成本:
- 真实 APT/DNF 客户端兼容;
- 72,310 个包的清单扫描;
- 5 万个上游包的流式处理;
- 独立的 5 万包 APT、YUM、物化、发布计划与旧包纳管测试。
这些测量支持了大仓库采用流式处理与有界并发的要求。
协议完整性与异常情形
失败的实验找出了协议设计的限制:
- APT 固定别名反例 表明旧客户端行为限制了别名切换的原子性。
- YUM 原始别名与签名桥接反例 区分旧 BaseURL 兼容与固定到 Generation(构建快照)的元数据。
- YUM Generation 原子性 记录哪些协议指针操作可以原子化,哪些不能。
这些实验说明,协议格式与客户端事务需要分别测试。
故障与恢复
归档测试了同步、发布、物化、快照、归档和远端恢复被中断的情况。 后期报告进一步检查文件描述符与精确路径身份,并覆盖回滚、隔离和保留核查记录。
代表性记录包括:
这些测试促成了不同恢复路径:放弃未提交的工作,完成提交意图之后的工作,或在记录冲突时停止。
旧系统迁移
迁移报告枚举了旧 Make 目标、物理布局、仓库族、客户端配置、包签名清单和回滚方式。 修改前先使用只读副本,并核对精确回执。
这项工作验证了一条具体的 Pigsty 切换路径,也建立了一种方法: 把迁移假设记录为数据,在每一步前检查,切换结束后移除迁移代码。
云存储与边缘服务实验
归档包含真实和模拟的 R2、MinIO、Cloudflare、COS 与边缘服务测试。 代表性报告包括:
- 真实 R2 存储协议;
- R2 发布存储事务;
- 完整存储服务的检查点约束删除;
- Cloudflare 初始化、私有域名、服务身份验证和回滚记录。
部分测试使用指定的非生产资源,另一些离线运行或使用模拟服务,报告中分别注明。 在 R2、MinIO 或 COS 上测得的存储行为,对应具体服务与所测 API。
哪些做法进入了设计方法
这项工作形成了六条实践:
- 说明测了什么。 区分源码检查、模拟故障、客户端测试与真实服务测试。
- 保留失败结果。 记录设计在什么条件下无法工作。
- 随结果记录输入。 保存源码版本、容器与客户端版本、存储服务、路由和测试数据。
- 测量规模。 记录大仓库的内存、调用次数与并发行为。
- 重新测试新布局。 历史结果与产生它的模型一起保存。
- 检查每次发布构建。 测试支持的平台,扫描依赖并运行静态分析。检查能否重现构建结果。
第一手资料
v0.1.0 验证报告目录保留全部报告, 包括命令、结果和测试条件。
继续阅读 v0.2 重置,了解这些经验如何用于更小的产品模型。
9 - SOW v0.1 决策账本:哪些被保留,哪些已退役
v0.1.0 标签中有 44 份架构决策记录(ADR),编号为 0001 至 0045,缺少 0006。 每份 ADR 记录一个问题、当时的决策及实现必须满足的条件。 下表说明仓库模型变化后,各项决策的后续情况。
| 状态 | 含义 |
|---|---|
| 保留 | 原则以基本相同的范围继续用于当前 SOW。 |
| 演化 | 问题仍在,但所有权或实现已经改变。 |
| 迁移历史 | 决策用于 Pigsty/V1 切换。 |
| 退役 | 决策属于已移除的 Git/CAS/Route/Edge 模型。 |
决策日期从 2026-07-11 到 7 月 28 日,ADR-0035 另有 7 月 29 日与 31 日的修订。 多数早期 ADR 在实现开始后,于 7 月 19 日集中进入 Git。 ADR-0032 包含已废弃的基础设施细节,因此这里只提供摘要。
核心状态、发布与信任
| ADR | 原始决策 | 后续命运 |
|---|---|---|
| 0001 | Git 是权威状态,SHA-256 CAS 保存包体,引用表达视图与历史,发布按各目标的事务流程执行。 | 演化。 Git/CAS/Route 所有权已退役,单一所有者、协议指针最后提交、独立目标状态与执行前检查得以保留。 |
| 0002 | 分别验证公共路由、不可变 Generation(构建快照)和客户端、存储服务兼容性。 | 演化。 Route 机制已退役,协议、客户端和存储服务仍分别验证。 |
| 0003 | 快照需要经过验证的 Generation 副本,并按清单保留。 | 演化。 通过 CopyObject 复制快照树的方式已退役;保留的 Generation 只保存元数据与引用集合,不复制包体。 |
| 0004 | 边缘令牌验证与部署采用同一套跨服务商、带版本的接口。 | 退役。 边缘访问权限不再由 SOW 仓库引擎管理。 |
| 0005 | 逐项决定旧 Make 目标是由 SOW 接替、退役,还是因策略限制而拒绝。 | 迁移历史。 完成切换后退出产品。 |
| 0007 | 公共包与受访问控制的包共用一个仓库。 | 退役。 商业边缘服务拓扑退出产品,单一所有者原则保留。 |
| 0008 | 垃圾回收计算完整引用关系,删除前要求显式确认。 | 演化。 当前 GC 检查引用关系与精确身份,旧 --apply --confirm 接口已退役;远端删除还检查缓存宽限期与存储服务能力。 |
| 0009 | 远端 Generation 可见后,通过前向恢复完成发布。 | 保留。 提交意图仍是放弃或核对未提交操作与前向恢复之间的边界。 |
| 0010 | 元数据签名绑定精确公私钥身份,密钥轮换作为整个仓库的状态变化处理。 | 演化。 当前签名者记录与信任环仍检查精确身份,不再使用 V1 引用拓扑。 |
| 0011 | 远端删除需要目标所有权、清单、检查点、宽限期与条件删除能力。 | 保留。 R2 不满足所需的原子删除能力,因此只报告候选对象。 |
| 0012 | 明确规定私有源站、缓存拓扑与缓存清理映射。 | 退役。 CDN 与私有源站部署不属于当前 SOW。 |
| 0013 | 持久化计划是恢复输入,使用前要重新验证。 | 保留。 Managed 恢复仍会重新验证计划、文件、目标身份和公共交付结果。 |
| 0014 | RPM 来源记录保留精确签名包数据,并区分接纳策略与历史证明。 | 演化。 当前包认证与独立信任环替代了 V1 回执编码。 |
| 0015 | 配置负责命名目标,已发布引用与检查点属于持久状态。 | 保留。 Generation 与目标无关;发布尝试和检查点按目标前缀保存。 |
| 0016 | 云适配器使用具体、有界的 SDK/API 接口。 | 演化。 具体存储服务接口保留,V1 Edge/COS 细节退役。 |
| 0017 | 在稳定的仓库密钥环中验证 RPM 签名,并绑定已打开包体的身份。 | 演化。 v0.4 的独立多信任环验证强化了这一规则。 |
| 0018 | 选定包集合在一个有界本地事务中完成暂存、验证、提交与恢复。 | 演化。 Managed 操作使用恢复日志;Plain 暂存元数据并最后替换指针,中断后重新构建。 |
旧拓扑、兼容性与迁移
| ADR | 原始决策 | 后续命运 |
|---|---|---|
| 0019 | 为旧客户端固定 EL7 元数据格式与压缩行为。 | 迁移历史。 当前支持平台见兼容性文档。 |
| 0020 | 为每条旧物理路径与选择器分组指定所有者。 | 迁移历史。 用于消除切换歧义,未进入当前仓库模型。 |
| 0021 | 根据固定验证结果复现跨 EL 的 yum/infra/{arch} 投影。 |
迁移历史。 这项特殊投影已退出产品。 |
| 0022 | 物理所有权变化时保留包仓库历史。 | 演化。 Generation、保留引用与迁移日志继续保存历史,不再依赖 V1 Route。 |
| 0023 | 接纳或使用保存仓库状态的 Git 对象前,重新核对其内容。 | 演化。 Git 权威状态已退役,文件描述符、路径与摘要检查保留。 |
| 0024 | 物化路由回执只授予限定范围的读取与退役权限。 | 演化。 路由回执已退役,检查和删除仍限定对象与操作范围。 |
| 0025 | 锁绑定具体进程实例与稳定 inode,不因等待超时而夺锁。 | 演化。 稳定锁 inode 与不按时间夺锁的规则保留,V1 进程实例租约已退役。 |
| 0026 | 创建离线归档前持久化意图,并严格检查输入。 | 退役。 离线归档投影已退出产品范围。 |
| 0027 | 旧包体通过路径、包内容与来源检查后,才纳入仓库管理。 | 迁移历史。 旧包纳管计划已结束,包接纳检查以更小的范围保留。 |
| 0028 | 检查 DEB 时只打开元数据所需的 control 归档,拒绝有歧义的容器。 | 保留。 DEB 解析仍限定读取范围与输入大小。 |
| 0029 | 由所有者明确规定最低客户端版本与 EL8 固定版本。 | 迁移历史。 当前平台策略见兼容性参考与发布注记。 |
| 0030 | 仅依据已评审的问题集合摘要,修复旧 YUM 索引中缺失的包体引用。 | 迁移历史。 这项来源例外没有变成通用忽略选项。 |
| 0031 | 启用受访问控制的旧内容前,修复精确校验和并记录启用结果。 | 退役。 商业版本启用流程退出产品,无法验证时停止修复的原则保留。 |
云端与边缘控制面
| ADR | 原始决策 | 后续命运 |
|---|---|---|
| 0032 | 只允许一组由所有者指定的 Cloudflare 测试资源例外通过。 | 退役。 这是针对特定测试的历史例外。 |
| 0033 | 服务就绪检查使用独立注册表与所有权记录。 | 退役。 服务初始化注册表已退出 SOW。 |
| 0034 | Worker 初始化使用租约、两阶段恢复、精确资源身份与可逆状态。 | 退役。 Cloudflare 部署不再由仓库引擎负责。 |
| 0035 | 使用前验证服务身份、运行时绑定、日志接收端与租约所有权。 | 退役。 这套边缘控制面已退出产品。 |
| 0036 | R2 缺少条件 DeleteObject;能力探测与重复身份检查通过后,可显式启用 checkpoint-fenced 无条件删除。 |
演化;删除降级未继承。 能力探测和条件不足时停止的规则保留。当前 SOW 禁用 R2 远端删除,只报告 GC 候选对象。 |
| 0037 | 上传机密字节前,先证明边缘服务会拒绝未授权访问。 | 退役。 边缘访问控制现由外部部署负责。 |
| 0038 | YUM 切换需要有有效期的回执,绑定精确端点、Generation 与信任数据。 | 迁移历史。 用于核对客户端切换,随后退役。 |
| 0039 | 脱字符在 Go、边缘服务、日志与源站路由中采用唯一 URL 编码。 | 演化。 旧路由已退役,路径只编码一次的规范保留。 |
有界配置与派生状态恢复
| ADR | 原始决策 | 后续命运 |
|---|---|---|
| 0040 | 分配内存或执行前,限制配置条目数与展开后的拓扑大小。 | 保留。 当前解析器与状态格式仍有明确大小和数量上限。 |
| 0041 | 保留未知的最终投影暂存目录,以便核查。 | 演化。 当前恢复仍保留冲突记录,并在无法核实时停止。 |
| 0042 | 派生状态替换明确区分成功、回滚、保留与阻塞结果。 | 演化。 Managed 日志记录操作阶段;记录状态与文件不一致时停止恢复。 |
| 0043 | 文件修改检查描述符身份,并规定同一操作系统用户恶意并发写入的保护范围。 | 保留。 路径检查拒绝身份变化,操作系统权限仍负责隔离其他写入者。 |
| 0044 | 只有匹配精确操作权限与确认令牌,才可删除保留的核查副本。 | 演化。 V1 命令已退役,删除前限定对象与操作权限的规则保留。 |
| 0045 | 对无日志残留进行有界分类,不能静默接纳或删除。 | 保留。 当前恢复仍区分已知状态、安全残留与冲突记录。 |
账本背后的共同模式
延续下来的决策有几条共同规则:
- 每个事实只有一个所有者。
- 修改前检查精确身份。
- 不可变内容就绪后,再提交协议指针。
- 提交意图持久化后,通过前向恢复完成操作。
- 删除前检查引用关系与删除条件。
- 限制输入大小,明确产品范围。
- 记录实际验证过的客户端与存储服务。
不可变的 v0.1.0 ADR 目录仍是第一手 资料。下一篇文章继续汇总独立的 v0.1 证据记录。
10 - SOW v0.1 原始计划:软件仓库的 Git
v0.1 计划始于 2026-07-11,并于 7 月 31 日封存为 v0.1.0。 它计划用一个 Go 控制面替换 Pigsty 的 Makefile 与 rclone 工作流,统一管理 APT、YUM 和静态制品。 范围包括本地状态、包历史、上游同步、渠道、视图、快照、双云发布、CDN 行为、商业访问控制和迁移。
当时的核心说法是:“制品仓库的 Git”。 软件分发确实需要不可变内容、可变名称、历史、晋级、 发布与回滚。这个类比帮助描述了需求,也扩大了产品范围。 Git/CAS/Route/Edge 模型后来退役,当前模型见系统模型。
第一版设计要解决什么
旧系统已经发布了几十条稳定 URL,供 APT、DNF、脚本和安装入口使用。 替代系统既要保留这些 URL,又要补足 Makefile 工作流难以可靠实现的能力:
- 每个已接纳包体只有一个身份;
- 期望状态与各云实际发布的状态分开记录;
- 快照与历史记录明确指出哪些文件仍在使用;
- 增量上传与缓存清理成本随变更量增长;
- 元数据准备完成、协议指针尚未发布时发生崩溃,仍可恢复;
- 上游索引、包签名与迁移例外都有来源记录;
- 商业私有对象不会通过公共索引或缓存键泄漏;
- 使用真实 APT/DNF 客户端检验兼容性。
原始计划将这些需求纳入同一套所有权模型。
v0.1 选择的模型
Git 保存权威状态
.sow/state 是由内嵌 Go Git 库操作的普通工作树。
清单、引用、来源记录、配置摘要、目标检查点和安全标签都保存在 Git 中。
SQLite 只提供查询数据,可以从 Git 重建。
每次状态迁移都有持久历史,一次引用更新就是本地提交边界。 这也要求包管理、发布、迁移和操作意图都采用同一种 Git 状态模型。
CAS 拥有字节
包体进入以 SHA-256 命名的不可变包池,公开树通过硬链接指向包池。 能否回收包体取决于引用可达性。这样可以低成本地去重和保留历史, 但包池与公开树必须位于同一文件系统,路径、回执和 inode 身份也成为产品契约的一部分。
Ref 表达产品语义
Repository、View、Snapshot、Stable、History 和各目标的远端引用,描述内容的用途与保留范围。 移动可变 View 不会删除 Stable 或历史引用。 删除 View 只改变引用,垃圾回收仍是独立操作,执行前要检查完整引用关系。
每个目标分别记录发布历史
Cloudflare R2 与腾讯 COS 是独立目标。 每个目标分别准备不可变对象、持久化提交意图、移动协议指针、清理 CDN 缓存、验证公共可见性, 并推进自己的检查点。一朵云成功后,不会仅因另一朵云失败而回滚。
发布顺序是:
当前发布仍沿用这一顺序。缓存宽限期与存储服务的删除能力检查用于远端垃圾回收。
Route 与 Edge 属于仓库正确性
公共树保留旧 URL,Route 根据 Generation(构建快照)选择不可变元数据。 私有对象位于 Cloudflare Worker 与 EdgeOne 共享的边缘服务接口之后。 鉴权必须先于源站访问,令牌不能进入源站 URL、缓存键或日志。
这使 CDN 路由、令牌验证、服务部署、日志接收端和缓存拓扑都成为仓库模型的前置条件。
这套设计为什么有吸引力
模型为各部分规定了明确规则:
- 每个持久事实都有所有者;
- 不可变字节与可变名称分离;
- 本地与各远端目标的发布状态可以独立观察;
- 最后发布协议指针,使未完成的工作可以恢复;
- 垃圾回收计算完整引用关系;
- 使用前重新验证计划与回执;
- 分别验证客户端、存储服务和本地实现;
- 将旧系统迁移范围记录为数据。
后续版本以更少的机制保留了这些规则。
为什么后来必须重置
到七月底,SOW v0.1 已经包含:
- 充当内部数据库协议的 Git 提交与引用;
- 全局内容寻址存储与硬链接物化层;
- Route、View、Snapshot、Generation、Projection、Receipt 和 Lease 等概念;
- 内置 APT/YUM/静态制品同步与旧系统迁移;
- 云 SDK、边缘服务初始化、服务身份验证、缓存清理、令牌和私有源站逻辑;
- 44 份现存 ADR 和 112 份按日期记录的验证报告。
每一项都针对具体失败情形,但组合后产生了几个问题。
第一,同一个包与公共路径被过多对象描述。它同时是 CAS 对象、清单条目、View 成员、 物化 Route、Snapshot 引用和远端目标对象。
第二,本地仓库生成与云端、边缘服务生命周期耦合。只想构建 YUM 或 APT 仓库的用户, 也要理解商业访问控制和分布式发布。
第三,迁移计划变成了永久子系统。旧拓扑、Make 目标、CDN 行为和服务例外在切换时有用, 却不应长期留在仓库模型中。
最后,为了在恶意并发写入和崩溃后恢复每条派生路径,系统引入了越来越多的身份检查、日志、 隔离与退役机制,复杂度超过了仓库核心任务的需要。
上游同步与渠道晋级也被移出产品。V1 已经验证过包获取的流式处理、异常输入、URL 和校验顺序, 但下载流程会引入独立的策略、重试、来源与远端所有权模型。 重置将包获取交给外部工具,让 SOW 专注于包接纳、仓库管理和发布。
重置后保留下来的东西
| v0.1 教训 | 当前形态 |
|---|---|
| 每个持久事实只有一个所有者 | 系统模型中的仓库与目标前缀所有权 |
| 包原件与生成索引分开管理 | 一个 pool/ 加纯元数据 dists/ |
| 包体先准备,协议指针最后提交 | 发布与恢复 |
| 提交后只能前向恢复 | Managed 操作日志与发布日志 |
| 使用前验证计划与路径 | 精确身份、路径范围和重新绑定检查 |
| 删除前检查完整证据 | 引用关系与对象身份;远端删除还检查缓存宽限期与存储服务能力 |
| 生成的服务端配置是派生状态 | 完整仓库根目录是托管单位,服务配置由运维者管理 |
| 兼容性需要逐项验证 | 平台与集成 |
| 历史结果与版本绑定 | 按日期记录的设计与发布文章 |
被淘汰的东西
- Git 产品状态与“SQLite 只是可丢弃缓存”的模型;
- 跨产品 CAS 与硬链接物化层;
- Route/View/Snapshot 公共命令模型;
- 生成的 Nginx 配置片段、Route 回执与 V1 服务控制状态;
- 内置上游同步、晋级与旧 Make 目标迁移;
- 作为仓库前置条件的边缘令牌权限与私有源站拓扑;
- 多云发布控制面、服务初始化与身份验证系统;
- V1 专用投影回执、租约、隔离命令和恢复接口。
部分实现思想后来在更小的范围内回归。SOW 管理仓库及其发布,包获取与服务托管仍由外部工具负责。
时间线
| 日期 | 里程碑 |
|---|---|
| 2026-07-11 | 目标、构思、技术调研、客户端与规模基线 |
| 2026-07-12 | Git/CAS/发布核心契约与首批协议、存储服务验证 |
| 2026-07-13–16 | 包信任、事务、旧拓扑与迁移验证 |
| 2026-07-17–20 | 服务初始化、边缘机密性、删除能力与输入上限 |
| 2026-07-22–29 | 路径身份、恶意并发写入、隔离与残留恢复 |
| 2026-07-30–31 | 从空环境验证最小可用产品,封存 v0.1.0 源码 |
| 2026-08-01 起 | Plain/Managed 重置,随后移除 V1 运行时 |
第一手资料与后续记录
v0.1.0 docs/ 树保留了原始
架构契约、
需求追踪、
迁移材料、ADR 和历史验证报告。