OpenShip 回滚子系统开放工作清单解读:三态恢复模型、保留策略与未完成路线图
2026/9/15 9:57:53 网站建设 项目流程

OpenShip 回滚子系统开放工作清单解读:三态恢复模型、保留策略与未完成路线图

【免费下载链接】openshipSelf-hosted deployment platform项目地址: https://gitcode.com/GitHub_Trending/ope/openship

OpenShip 的部署回滚(rollback)并非一次交付就完结的功能,而是由"已交付的核心模型"与"明确的开放工作清单"共同构成。本文基于仓库中apps/api/src/modules/deployments/rollback/PENDING.md(该模块的官方 open-work 清单,2026-08-20 对照工作树复核),系统梳理:已落地的三种恢复模式(restore modes)、保留窗口解析器(retention resolver)与"回滚永不走入死胡同"(never-dead-end)不变量;以及云(Oblien)运行时、保留策略与核心、UX 三个方向上的未完成项和它们背后的源码依据。读完本文,你将理解 OpenShip 回滚为何能做到"有可复现来源就永不失败",也能看清从"每部署一个 workspace"到"每项目一个 workspace"、从 Docker 重建式回滚到热切换式回滚的演进方向与阻塞点。

已交付的核心模型:三种恢复模式与"永不死胡同"不变量

PENDING.md 明确说明:已交付的模型不在该文件中复述,而是存在于代码与注释中——核心文件为:

  • rollback-orchestrator.ts:拥有保留策略(retention)并执行恢复(restore)的编排器;
  • restore-plan.ts:唯一决定"这个历史部署如何回到线上"的纯函数规划器;
  • release-retention.ts:回滚窗口解析器与磁盘容量测算。

备份/恢复的另一半(backup/restore)在 apps/api/src/modules/backups/PENDING.md 单独成文。

三种(实为四种)恢复模式

restore-plan.ts的头注释(apps/api/src/modules/deployments/rollback/restore-plan.ts#L1-L43)完整定义了恢复模式的语义:

模式适用运行时恢复方式代价
unit-swapbare / cloud(能力unitRestore工件是跨重部署存活的每部署单元(bare:supervisor 单元 + release 目录;cloud:已停止的 workspace + 磁盘),原地重启真正瞬时(instant)
redeploy-pinnedDocker容器是一次性的,重部署已移除旧容器;持久工件是镜像,由回滚窗口 keep set 保留。恢复 = 以目标冻结的配置快照 + 环境执行普通部署流程,镜像 PINNED(不构建、不 clone)秒级,依赖保留的镜像
reacquire-image单应用 release 镜像本地过期目标冻结快照记录了具体的 registry 引用(通常是不可变 repo digest),重放快照走预构建镜像路径重新拉取无需仓库/commit
rebuild无任何工件但有 commit同一部署调用,去掉 pinned 镜像:重新 clone 并构建该 commit慢,但永远正确

关键不变量(invariant)写在restore-plan.ts头部注释中:

回滚在拥有可复现来源时绝不走进死胡同(never dead-end)。冻结的 release-image 引用会被重新获取(reacquire);已知 commit 会被重建(rebuild)。因此"工件被清理"本身不是错误——它只是选择了一个更慢的分支。

规划器刻意保持"纯函数 + 同步"(planRestore不访问守护进程,镜像存在性由调用方传入),每个分支都可无 daemon 单测,与image-gccomputeKeepSet同构。

保留窗口解析器:explicit / auto / instance-default 三级

release-retention.tsresolveRollbackWindowDetailapps/api/src/modules/deployments/release-retention.ts#L39-L70)是项目"回滚窗口"的唯一权威答案,任何保留决策(prune、镜像 GC keep set、向导标签)都解析经过它:

  1. explicit:操作员手填project.rollbackWindow
  2. auto:未填写 → 使用上次部署时按磁盘容量测算的project.rollbackWindowComputed
  3. instance-default:从未测算 → 回落到instance_settings.default_rollback_window

设计上刻意做到"除一次 instance-settings 读取外零 I/O":磁盘探测与快照测算每次部署只做一次refreshRollbackCapacityapps/api/src/modules/deployments/release-retention.ts#L97-L130)并持久化,因此 prune 与每日 GC 扫描从不为做保留决策而 SSH 到主机。值得注意的实现细节(代码注释中记为 D9):失败的磁盘探测绝不能把 instance default 写进rollbackWindowComputed,否则"null = 从未测算"的语义被破坏,source: "auto"将永久覆盖 instance-default 分支。

编排器:保留、清理与恢复执行

rollback-orchestrator.ts的头部注释(apps/api/src/modules/deployments/rollback/rollback-orchestrator.ts#L1-L40)定义了完整生命周期:

  • 每次成功部署时:前一个 release 被标记artifact_retained_at;对工件是持久单元(bare/cloud,能力unitRestore)且项目选择保留工件的,通过runtime.archive停止但保留该单元;Docker 无物可停(旧容器已被重部署移除),镜像即工件,由 keep set 保留。
  • 溢出自动清理(prune)prunerollback-orchestrator.ts#L537-L615)按resolveRollbackWindow(project)丢弃窗口之外最旧的未 pin release;pin 的 release 既豁免又不消耗预算;活动 release 永不清理。实现上还注意了一个共享镜像陷阱:恢复会复用来源 release 的镜像 tag,因此按行 purge 前必须对照与镜像 GC 相同的 keep set,绝不移除仍在保留集合中的 tag。
  • 恢复执行rollback(id)先问planRestore,再执行。redeploy-pinned/reacquire-image/rebuild一次triggerDeployment调用,携带目标冻结的配置与环境(快照重放,而非从项目当前列取值);unit-swapruntime.makeActive,随后探活并提交指针,DB 写入失败时执行补偿性换回(revertUnitSwap)。恢复自身也可恢复:它把离开的 release 记录为commit_sha_before

Cloud(Oblien)方向:三个未完成项

内联 workspace 模型——每项目一个 workspace,而非每部署一个

当前实现仍是每部署一个 Oblien workspace:保留 5 个部署 = 5 个 workspace(1 运行 + 4 停止)加它们的归档,每个都计费一个 slot。源码印证(packages/adapters/src/runtime/cloud.ts):

  • CloudRuntime.deploy以 BUILD workspace 为单元,workspaceId = config.imageRef(cloud.ts:1776),随后ws.lifecycle.makePermanent()提升为永久(cloud.ts:1802),并返回containerId: workspaceId(cloud.ts:2067)——一个部署的 container id 就是它自己的 workspace id

提案模型是每项目一个 workspace,release 作为其内部目录,working_dir指向/app/current;回滚 =ln -sfn … current+workload.restart——瞬时完成,每项目只占一个 slot,与 bare 已采用的 Capistrano 形态一致(bare.tsln -sfn见 bare.ts):

/app/ releases/<depId-1>/ <depId-2>/ <depId-3>/ current → releases/<depId-3>/

该提案尚未交付,需要三件事:

  1. CloudRuntime.deploy的 provision-once 重构(cloud.ts 1625-1860 区间):当前 staging 是一次性原地mv /app/.staging /app/productionworkDir/app/production/app,从不指向 release 目录;
  2. 符号链接交换式makeActive,外加瘦身版archive/purge(cloud.ts 2124-2216 区间):三者目前仍是 workspace 生命周期调用(stop(from)start(to)createArchive + stopdeleteAllArchives + destroy);
  3. Cloud 对previousDeploymentId的消费:该字段位于共享DeployConfig上且每个运行时都会设置(build-pipeline.ts:2098),但只有 bare 读取它;types.ts 明确记载 "Docker/Cloud ignore the field"。

旧计划中的迁移路径已失效,这是一个陷阱project.cloudWorkspaceId现在已经存在,但它是 cloud LINK 标记而非内联钩子——每次 cloud 成功部署后从该部署刚创建的 workspace 盖戳,读取它仅用于推导目标。也就是说它对每个既有 cloud 项目都已是非 null,"若已设置则走内联路径"会把所有 legacy 每部署项目在下次部署时错误地路由进内联路径,必须另设一个内联/legacy 区分标记。还需注意BuildConfig.cloudWorkspaceId(cloud.ts 663-672 区间消费)是另一个值——浏览器文件夹上传会话的 workspace。

为何显而易见的替代方案不可行:对照 Oblien 官方文档核实,"杀死 workspace 再从归档重建"在当前 API 上无法实现——归档端点按 workspace 作用域(/workspace/{wsId}/archives/*),无账户级存储;workspaces.create没有restore_fromfrom_archiveseedclone_from等任何来源参数(image是只读 catalog id,是唯一来源标识);GET /workspace/images是唯一镜像端点,不存在 commit-workspace-to-custom-image 流程;POST /workspace/{wsId}/restore只能恢复到该 workspace 自己的最后快照。需要 Oblien 新端点(与对方团队悬而未决)或外部持久存储(已探索并作为 Openship Cloud 的错误方向回退)。内联模型完全绕开此需求,因而是演进路径。

过渡期代价:只要运行时声明unitRestore(cloud 确实声明,cloud.ts:499),保留逻辑就会归档上一 release(rollback-orchestrator.ts:96-105);archive按设计让 workspace 保持被占用(停止的 workspace 正是回滚生效的前提),只有 release 掉出窗口后purge才释放 slot。恢复侧依赖这个幸存者:restore-plan.tsunit-swap分支仅在目标仍有 container id 且工件被保留时返回(apps/api/src/modules/deployments/rollback/restore-plan.ts#L198-L200),否则 cloud 会抛出 "workspace is gone"。因此 cloud 项目的回滚窗口长度直接乘以 Oblien workspace slot 占用

cloud_archive_strategy: 'offload'被持久化后无人读取

该列接受'inplace' | 'offload',API 接收、持久化并回显(project-crud.service.ts多处),但零读取者:全库没有一处=== "offload"比较,唯一消费归档决策的地方(cloud.ts 2141-2179 区间)根本不接收 project 行,因此无法按策略变化。它预留给未来"自托管 → 外部 S3"的归档外移路径,但对 Openship Cloud 不可构建(需要上一节确认我们不具备的 Oblien 支持)。文档给出的行动建议是:要么接上它,要么停止接受该值

文档还纠正了旧引用清单的一处错误:0022_cloud_archive_offload.sql并不存在(0022 是0022_version_on_success_backfill.sql);该列实际随0000_init.sql发布。

purge 时归档删除失败仍只是警告

CloudRuntime.purge现在会传播 WORKSPACE 删除,因此"仍在付费的 slot"不再可能被记作已回收;但snapshots.deleteAllArchives保持 warn-only——因为从未归档的 workspace 本就无物可删,而 Oblien 对该场景的响应与真实失败无法区分,若将其设为致命错误会让每次 cloud purge 都误报一个可能不存在的泄漏。区分两者需要 Oblien 的确认答复(或删除前先列归档);在此之前,真正卡住的归档 blob 会持续计费存储,只出现在一行日志里。

保留策略与核心方向

每环境回滚窗口(per-environment rollback window)

rollbackWindow是项目级且解析器对环境无感知:RollbackWindowProject携带四个项目级字段(apps/api/src/modules/deployments/release-retention.ts#L7-L12),resolveRollbackWindowDetail的三个分支都没有环境参数;列是项目作用域,实例默认值是一个标量(schema/settings.ts),整个模型不存在每环境表——唯一的environment列是项目身份、环境变量作用域和deployment.environment(每部署属性)。强制环节印证了这一缺口:prune只解析一个窗口并遍历所有 ready 部署且无环境过滤(rollback-orchestrator.ts:541-557),而 release 的环境是每部署属性。

接手前值得知道的一个事实:独立创建的环境是独立的 project 行,因此会在创建时从父项目复制一份列值(project-crud.service.ts:1745)。真正未覆盖的场景比"每环境"字面意思更窄——住在同一个 project 行内的 preview 部署共享该行唯一的窗口,而这恰恰是生产要比 preview 保留更久的时候。方案二选一:把列移到每环境表,或运行时通过instance_settings解析并支持环境级覆盖。文档评估:在 preview 部署变重之前不紧急。

Docker 热回滚(无容器重建)

这是"主要剩余延迟收益"且完全未构建。当前每个 Docker 恢复都从保留镜像重建容器(rollback-orchestrator.ts:339-355 区间);唯一的原地路径是restoreViaUnitSwap(rollback-orchestrator.ts:378-481),即 bare/cloud 的makeActive+ 探活 + 指针翻转,且以完整路由重新同步syncProjectManagedEdge)收尾而非上游翻转。路由层没有端点交换原语:upstream-url.ts是纯解析器、每容器一个目标,routing-apply.service.ts只为项目的单一activeDeploymentId重新应用路由。

"第二个 loopback 端口"的前提在默认策略下不可能成立,这是必须先解决的问题:canOverlap要求运行时非 bare 且路由策略非 loopback-port——固定(pinned)的 loopback 端口无法双重绑定,所以 loopback-port 部署必须 stop-first(当前实现中由usesHostLoopback推导,见apps/api/src/modules/deployments/build-pipeline.ts#L1844-L1849)。今天唯一存在的重叠是高级container-ip策略下部署期"先跑新的再交换"(upstream-url.ts:13-15),且它仍会创建新容器。

完成它需要四件事:交换原语、双版本模型、为它准备的回滚调用方、以及"两个版本可共存多久"的策略。

UX 方向

部署列表从不显示 instant 还是 rebuild

廉价近似已交付一半:DeploymentCard.tsx:217-225!pinned && artifactRetainedAt && !isActive时渲染Snapshotted徽章,与Active(:199)和Pinned(:208)并排。缺失的是模式本身:instant/rebuild/mixed只存在于确认路径——DeploymentMenu.tsx:140-150从恢复计划构建modeLineRollbackConfirmDialog.tsx:57渲染它。没有行级徽章命名模式,没有行使用年龄,纯 rebuild 场景(无工件、有 commit)根本不渲染任何徽章——该状态只在菜单项 tooltip 里可见(DeploymentMenu.tsx:267-277)。真正的逐行解析成本是一次按行的存在性检查(SSH 往返),这正是当前改为按需的原因;artifact_retained_at+ 年龄是值得尝试的近似方案。

回滚 diff 预览——commit 区间与每服务镜像 diff

对话框其余部分都已完成,仍缺两项,且都因为恢复计划(restore-plan)负载不携带它们:

  • commit 区间RestorePlanUIdashboard/src/lib/api/deploy.ts:7-43)与服务端负载(deployment.service.ts:223-243,经deployment.controller.ts:200-206/deployment.routes.ts:140-151提供)携带 mode / needsRepository / rebuildServices / env / untouchedServices / code / reason,无任何 commit 字段;对话框不渲染对比链接。而编排器内部已解析prevSha——预览只是从不暴露它。
  • 每服务镜像 diffrebuildServices只是服务名称resolveEffectiveServiceImagesapps/api/src/modules/deployments/rollback/rollback-orchestrator.ts#L221-L240)内部已解析新旧镜像引用,预览却将其丢弃。

两者都需要新增负载字段,配套RestorePlanUI条目、对话框区块与 locale 键。

批量 pin 与批量删除

pin 和 delete 目前逐行、从上到下进行。DeploymentsList.tsx仅 38 行、只是映射行——无选择状态、无表头行、无复选框。动作是单行菜单项(DeploymentMenu.tsx:152-166:168-177),走每 id 路由(deployment.routes.ts:157POST /:id/pin:178DELETE /:id)与每 id 客户端(deploy.tsdeleteDeployment(id)pin(id, pinned))。任何一层都不存在批量能力。期望:行选择、批量动作栏、带权限标记的集合级路由——用例是保留清理(retention cleanup)。

不在近期路线图

内容寻址工件存储(content-addressable artifact store)

两个产生字节相同输出的构建要付 2 倍存储。以 SHA 为键、带引用计数的存储可在跨部署、跨项目去重。目前模型中的唯一 digest 是service_deployment.image_digest(migration 0050),其用途是可变 tag 上的更新扫描漂移检测;保留靠按行派生的 tag 集合(computeKeepSetimage-gc.ts:49-61)——是 keep set 而非引用计数;bare 的去重是文件系统硬链接(bare.ts:311),非内容寻址。规模化时是真收益,但属于大型新子系统。

bare 运行时完整 Capistrano(每项目 supervisor 单元)

目前仍是每部署一个单元:单元身份是部署 id(supervisor/systemd.ts:5:12:56-61),release 目录按部署划分(bare.ts:206-208),makeActivestop(from)start(to)bare.ts:835-853区间)。没有currentrelease 指针——唯一一处ln -sfnbare.ts:269)是把共享持久路径链进 release 目录。完整 Capistrano 会让每项目一个单元指向current,回滚变成符号链接交换 +systemctl reload——比现在的 stop/start 更快。这是更大规模的重构;当前模型可用。

bare 上文件系统原生快照(zfs/btrfs)

块级去重,超越 rsync 硬链接。完全未构建:无文件系统检测、无快照/克隆路径。bare release 去重目前是rsync -a --delete --link-destbare.ts:318-345,调用在 :345)配普通mv回退。它把项目绑定到特定文件系统,不可移植。

小结:如何继续跟踪这份清单

PENDING.md 是 OpenShip 回滚方向的权威开放工作清单,其价值在于:每一条都标注了工作树中的精确源码位置与验证日期(2026-08-20 复核,含未提交改动),并明确区分"已交付模型"(读代码与注释)与"未完成项"(读本文档)。若你计划贡献或评估该模块,建议按以下顺序阅读:

  1. 先读 restore-plan.ts 头部注释(恢复模式与 never-dead-end 不变量)与 rollback-orchestrator.ts 头部注释(保留与恢复生命周期);
  2. 再读 release-retention.ts 的窗口解析与容量测算;
  3. 对照本清单逐条验证:cloud 侧从 cloud.ts 的 deploy/archive/purge/makeActive 开始,保留侧从pruneresolveRollbackWindow开始,UX 侧从DeploymentCard.tsxDeploymentMenu.tsx开始;
  4. 配套测试位于 restore-plan.test.ts 与 rollback-orchestrator.test.ts,是理解各分支行为的最快途径。

一句话概括现状:回滚正确性已经交付(计划器 + 窗口 + 不变量),剩下的全是延迟优化(热切换、内联 workspace)、容量优化(每环境窗口、去重存储)与体验补全(模式徽章、diff 预览、批量操作)

【免费下载链接】openshipSelf-hosted deployment platform项目地址: https://gitcode.com/GitHub_Trending/ope/openship

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询