这不是一次功能发布。它记录的是 WEOUR OS 为什么先停下新功能,去补一层生产安全网:备份、调度、发布、回滚。
- 本轮范围
- 生产基础设施(这轮建设的记录):备份 · 调度 · 发布 · 回滚
- 备份节奏
- 每 6 小时一次(launchd 定时)
- 备份成功契约
- 四道门禁:stream / decrypt / structure / durable
- 备份健康分级
- HEALTHY ≤ 7 小时 · DEGRADED ≤ 12 小时 · STALE > 12 小时(FAILED 表示单次尝试失败,不是档位)
- 调度 stagger 预算
- 默认 30 秒(下限 0 / 上限 60),与 tick 间隔解耦
- 调度 tick P50(4 店)
- 119.5 s → 31.9 s
- 调度 tick P95(4 店)
- 369.9 s → 55.3 s
- 调度占空比
- 83.7% → 17.1%
- 调度验收样本
- 11 个 tick(4 店)
- 首次 canonical 部署 Trial
- 2026-09-23 失败,由流水线执行了回滚
- 该轮生产验证
- release 20260923-rpg13-7f685ca(CANONICAL,validation / smoke PASS)
- 发布治理用例
- 123 → 157
- 该轮观测点保留状态
- release 19 · 删除候选 1 · 来源未知 6 · 受保护 5(retention dry-run)
- 调度修正提交
- Anxin360/weour-os@e92b059
- readiness / iowait 修正提交
- Anxin360/weour-os@d4cbe01
- Foundation V1.1 trial 分支顶端
- Anxin360/weour-os@b8cc817
- 治理 V1.3 提交
- Anxin360/weour-os@7f685ca
为什么先停下新功能
WEOUR OS 已经在生产里跑真实数据:真实的店铺、真实的调度循环、真实的发布记录。功能越多,出错的代价越高;但在这之前,有几件事并没有真正闭环:
- 不可变 release 只会不断累积,磁盘不会自己变干净;
- 备份是有的,但"这次备份是否真的成功"没有自动核对,也没有一条可以被自动消费的健康信号;
- 调度器看起来很忙,但忙的主要不是业务工作。
这些都不是功能问题,而是运行方式的问题。先把它们修好,后面每加一个功能才会更便宜。
改了什么
1. Release 保留:不可变 release 需要受治理的清理
发布以不可变的整份构建为单位:切换用符号链接指向新的 release 目录,回滚就是切回旧目录。这个设计让回滚变得便宜,代价是磁盘会一直增长——审计时,release 目录占了该主机已用磁盘的约 74%(44.12 GB / 59.44 GB 已用),而同一块磁盘还要留出构建所需的余量。
于是清理被写成了分类优先级,而不是"删最旧的":
CURRENT > IMMEDIATE_ROLLBACK > DIAGNOSTIC > FAILED_TRIAL > VALIDATED_ANCHOR
> MIGRATION_BOUNDARY > MANUAL_PROTECT > UNKNOWN_PROVENANCE > ROUTINE
只有 ROUTINE 才进入日常配额清理,其余类别都不会被顺手删掉。其中一条规则尤其重要:一次失败 Trial 的 release,不会因为它的 commit 后来进了 main 就被降级成 ROUTINE。判定只依据生命周期证据,不依据目录名、时间戳或聊天记录。
2. 备份:从"有备份"到"可核对的备份"
备份的形态没有推倒重来:备份主机主动拉取,生产只提供一条受限的强制命令流,用于解密的私钥从不进入生产主机。改动发生在拉取这一侧:
- 节奏与 RPO:每 6 小时一次(launchd 定时),目标 RPO 为 6 小时。节奏是可解释的,不依赖漂移的间隔计时器。
- 四道门禁:一次运行只有在 stream、decrypt、structure、durable 四道门禁全部通过后才算成功。
pg_dump退出码为 0 本身从来不是成功。 - 失败即失败:任何一道门禁失败,运行就是
FAILED,last-success不被更新,清理也不会发生。 - 健康分级:
HEALTHY(≤ 7 小时)/DEGRADED(≤ 12 小时)/STALE(> 12 小时)由"距上次成功的小时数"推导,并可以作为退出码被监控直接消费;FAILED描述的是单次尝试,不是档位。 - 失败注入测试:覆盖远端失败、过小产物、解密失败、结构不合法、叠加运行、陈旧锁、保留策略与损坏的健康状态。
最后一步是把健康信号接回生产:备份主机在四道门禁通过之后,通过一个受限身份把快照推给生产,该身份只能写一个固定文件,既不能切换 release,也不能重启服务,更不能读任何密钥。
3. 调度器:把 tick 和 stagger 的语义分开
诊断先给出了一个不舒服的结论:在 4 家店的运行区间里,tick 墙钟时间的 96.8% 花在确定性的 stagger 睡眠上——2610 秒里有 84 秒在做真实工作。按每个 action 计算,睡眠本身就要 4150 秒/小时,接近墙钟的 115%。
根因是语义混在一起:stagger 窗口被绑定到 tick 间隔(180 秒),而同一个串行 runner 上多个 action 各自的固定睡眠被串行累加。
修正把两件事分开:tick 间隔只用于报告 cadence,不再参与 stagger 计算;stagger 有自己的预算,默认 30 秒,可选 0 / 30 / 60,非法值回落到默认值。部署后的测量结果:
- tick P50 119.5 秒 → 31.9 秒,P95 369.9 秒 → 55.3 秒;
- 占空比 83.7% → 17.1%;
- 计划睡眠与实际睡眠一致,其余时间是可解释的 API 与 DB 工作;
- 未改 timer、cadence、店铺开关与并发预算。
4. 一条 canonical 发布流水线
发布不再是一串手工命令,而是一条有顺序、可复现的流水线:
provenance → preflight → release lock → scheduler quiet window → source
→ build backend → monitored build → immutable release + manifest
→ candidate validation → rollback readiness → atomic switch → restart
→ readiness → functional smoke → state write → history → retention dry-run
其中几处是刻意设计的:构建只在调度器安静窗口内进行;切换是原子的;回滚目标在切换之前和之后各校验一次;systemctl restart 的退出码被明确记录为"单元已启动",而不是"应用已就绪"。
5. 第一次 Trial 失败,而且被完整保留
第一次 canonical 部署 Trial 跑完了自己的成功路径,然后在 smoke 阶段失败,并由流水线执行了自己的 canonical 回滚。
原因不是应用有问题:systemctl restart 返回时只代表单元启动了,不代表端口已经绑定。当时的 smoke 是一次立即的 HTTP 检查,没有任何 readiness 等待,于是它观察到了连接被拒绝与网关错误。
处理方式才是重点:没有人绕过流水线去手工修好这一步。流水线按自己的失败契约停下并执行了回滚,该 release 被保留用于诊断,清理阶段一次删除也没有执行。生产继续在切换前的 release 上正常服务。
随后发布的修正把 readiness 变成有界轮询,并让构建监控真实测量 iowait。重新试跑时,此前那个失败模式被新逻辑正确地吸收为瞬时未就绪,而不是一次部署失败。
6. 治理:让"谁可以发布"和"发布了什么"都留下证据
这一轮补的规则是给未来的自己用的:
- canonical deploy only:日常发布只走流水线;例外必须显式标记。
deploymentMethod:每条新的生命周期记录都带方法枚举,历史记录追加而不改写;无法判断的历史标记为UNKNOWN_LEGACY,绝不猜成CANONICAL。- 失败 Trial 分类:
FAILED_TRIAL由生命周期证据判定,不占日常清理配额。 - 诊断证据优先:删除前必须先写回 KB 级、不含密钥的诊断记录并校验;写不下去就拒绝删除。
在该轮观测点(生产 release 20260923-rpg13-7f685ca)上,保留策略以 dry-run 呈现:记录显示 19 个 release,其中 1 个删除候选、6 个来源未知、5 个受保护。清理只在证据齐全(目标已确认不被引用、诊断记录已写回并校验)时才会发生。
怎么验证的
每个环节的验证方式都尽量落在可复查的证据上,而不是"看起来正常":
- 备份由失败注入测试与真实四门禁运行验证,健康信号由监控消费;
- 调度修正先在隔离环境里用实测 offset 模拟,再在部署后用 11 个真实 tick 前后对比;
- 发布流水线的验证方式是让它自己跑完一次真实发布:readiness 通过、functional smoke 六条路由通过、状态由流水线写入,然后才做 retention 的 dry-run;
- 治理规则由用例覆盖,从"失败 Trial 不被降级"到"发布器拒绝异常载荷"。
它不意味着什么
它不是一次功能发布,也不意味着基础设施已经完成。
在该轮记录里,备份的局限是明确写下来的:解密密钥只有一份且与密文在同一台机器上;没有第二个异地副本;没有 PITR,RPO 就是 6 小时这个节奏;媒体备份没有保留策略;角色与全局对象不在 dump 里。这些是已知的取舍,不是被忽略的细节。
调度修正只解决"时间花在哪里",不是并发或性能优化。
治理也没有覆盖全部历史:在该轮观测点上仍然存在来源无法判定的旧 release,它们被保守地保留。
这篇记录属于 2026-09-22 至 2026-09-23 这一轮建设。文中的生产状态都是在该轮观测点上的事实,不是此后任何时刻的现状;后续的生产变化不在本文范围内。
最后,它不是对未来版本、时间表或能力的承诺。这里记录的是一次已经发生的基础设施升级,以及支撑它的证据。
证据在哪里
这篇记录里的每条事实性陈述都指向一个可以反查的对象:提交、部署 release 标识与用例数量的对照关系登记在上方的事实表里,也登记在这篇记录的 evidence 条目中。本文的正文只解释含义,不重复罗列标识符。