P3 Apollo
回滚:部署失败与版本回溯
部署不是"一把梭"——失败了要能退回来,退回来还要能查得清。Apollo 把"回滚"做成一条条可审计的新部署记录:部署失败自动回滚、手动回滚到任意历史版本、回滚后漂移自动收敛,而"单调版本"机制让任何人都不可能悄悄把版本退回旧包。看完这 4 个故事,你就能自己走通一次"部署失败 → 自动回滚 → 漂移收敛"的完整闭环。
平台运维 / SRE
安全合规
自动回滚
单调版本
期望状态
部署记录
共 4 个故事
能 / 不能速览
✅ 这个主题能做
- 部署失败自动回滚:组件就绪超时 / 探针失败,回滚到上一稳定成功版本
- 手动回滚:POST /desired-states/:id/rollback 回滚到任意历史期望状态
- 回滚生成新期望状态行(bundle_version=当前 max+1),不覆盖历史、可审计
- 部署失败 / 自动回滚同步落告警事件与事件单(G12),事故可追、可查
- Agent 离线导致部署悬挂超 5 分钟自动置 failed(F7,审计 DEPLOYMENT_TIMEOUT),回连后按 digest 幂等收敛
- 回滚后 Spoke 按新 digest 收敛,漂移检测 / 调和(reconcile)自动对齐
- bundle digest 不可变 + bundle_version 单调递增,防止"悄悄降级"
⛔ 这个主题做不了
- 自动回滚只回滚到"上一稳定成功版本",不是任选版本;防循环:自动回滚产生的版本不再触发自动回滚
- 回滚只生成草稿(status=draft),需要人工再激活才会被 Spoke 拉取
- 回滚目标必须与当前期望状态属于同一应用,跨应用回滚直接报 400
- 演示 Poller 只拉取 / 上报,漂移的"真正应用"需要配置 ProcManager 才会执行
适用角色
本主题面向两个角色:
- 平台运维 / SRE(陈工):发起部署、处置失败、手动回滚、看部署与漂移记录——是回滚动作的操盘手。
- 安全合规(刘经理):关心"回滚是否留痕、版本能否被悄悄改回去",消费单调版本与审计信息,不直接操作。
应用开发(阿强)偶尔自助回滚自己的演示环境;平台管理员负责授权与审计。每个故事自成一体,可单独阅读。
能力速览(能做什么)
自动回滚(auto-rollback)
部署策略 default 默认开启:组件就绪超时或探针失败时,自动回滚到上一稳定成功版本并生成回滚草稿 + 告警审计(G12 已接线:失败同时写 alert_events 与 incidents)。
手动回滚任意版本
POST /desired-states/:id/rollback 指定 target_desired_state_id,回滚到任意历史期望状态,生成新行、原行标记 rollbacked。
漂移检测与收敛
字段级 JSON Patch 结构化 diff + ignore_paths 忽略规则;回滚后 digest 变化驱动 Spoke 自动收敛,组件恢复 ready。
单调版本防降级
bundle_version 按应用单调递增(DB UNIQUE),回滚也走 max+1 新行;Spoke 校验"新版本 > 已应用",拦截直接投递旧制品。
部署记录审计
每次部署 / 回滚生成 deployment_records(kind=deploy/rollback/auto-rollback),开始、成功、失败、自动回滚全部留痕可查。
调整指南(怎么调整)
- 改就绪超时:在部署策略的 readiness_timeout_sec(默认 60)里调,组件 deploying 超过该时长会被判 failed。
- 关掉 / 打开自动回滚:改策略规则 auto_rollback(默认 true);想彻底不自动动生产,可显式关掉,失败只告警。
- 改回滚目标:手动回滚先 GET /desired-states?app=demo-app 确认目标历史版本 id,再 POST rollback。
- 改漂移噪音:在期望状态 policy.ignore_paths 里排除运行时字段(如 /components/*/runtime/*),别把 PID 当漂移。
- 改收敛行为:回滚后跑一次 POST /drift/check 确认无漂移;有漂移就触发 POST /drift/:id/reconcile 或等 Spoke 自动调和。
做得好的场景
回滚被做成了"版本回溯 + 自动收敛 + 强留痕",特别适合以下场景:
- 发布事故快速止血:组件起不来时自动回滚上一稳定版,MTTR 从"人工翻记录改配置"的 30 分钟以上压到秒级触发。
- 线上功能退化主动回退:部署成功但业务异常时手动回滚到任意历史版本,一次 API 调用完成,不用翻包。
- 回滚后环境一致性:期望状态驱动收敛,回滚目标应用后经就绪门控才算成功,组件状态一目了然。
- 供应链安全:单调版本 + 不可变 digest,从机制上消灭"悄悄把版本改回旧包"的路径。
限制与不足
以下是明确的边界,使用前先知道:
- 自动回滚目标固定:只回滚到"上一稳定成功版本",且对 auto-rollback 产生的版本不再触发自动回滚(防循环),需要人工处置。
- 回滚是草稿:POST rollback 只生成 status=draft 的新行,必须再 POST /desired-states/:id/activate 才会被 Spoke 拉取。
- 跨应用不行:回滚目标必须与当前期望状态同 app,否则返回 400 DESIRED_STATE_INVALID。
- 演示应用边界:演示 Poller 只拉取 / 上报不真实部署进程,真正落地部署需配置 ProcManager(仅 process 运行时,systemd 未实现)。
- 监控告警自愈是 V2:指标门控失败自动回滚、liveness 自愈重启的完整闭环未落地,当前以部署编排层面的失败处理为准。
场景故事
故事 1
部署 v1.1.0 起不来,auto-rollback 自动退回 v1.0.0
场景:部署失败自动回滚
角色:平台运维 / SRE
耗时:约 5 分钟
- 背景
- 陈工是负责 demo-app 的 SRE。今天上午 10:05,他把验证好的 v1.1.0(web 组件新增 /healthz 端点,就绪探针从 tcp 8080 改成 http /healthz)登记为期望状态并激活,然后发起部署。web 进程是起来了,但 /healthz 一直返回 503,readiness 探针在 60 秒超时内始终不过。
- 传统做法对比
- 以前 web 起不来,要登服务器看日志、手改配置、手动重启,快则 30 分钟慢则半天;回滚还要翻发布记录找旧包,中间没人盯着很容易把生产搞成"半新半旧"。
- 角色
- 陈工(平台运维 / SRE,拥有部署与期望状态管理权限);部署策略 default(auto_rollback=true)已由 demo seed 就绪。
- 操作步骤
-
- 登录 Apollo(端口 18082,admin / admin1)
- 登记 v1.1.0 期望状态(POST /desired-states,YAML 声明里 web 探针改为 http /healthz)
- 激活草稿:POST /desired-states/:id/activate
- 发起部署:POST /deployments/start,body 填 desired_state_id、agent_name=agent-demo-01、policy_name=default
- 用 Agent 上报推进:POST /deployments/:id/advance,组件 web 上报 deploying 直到超时
- 在"部署与漂移"页查看部署记录与自动回滚草稿
- 系统响应
- web 组件 readiness 超时被标 failed,部署记录 status=failed,编排器检测到策略 auto_rollback=true 后生成回滚草稿:
{
"deployment_id": 4,
"status": "failed",
"ready_count": 1,
"total_count": 2,
"auto_rollback": 7
}
auto_rollback=7 就是指向 v1.0.0 的新回滚草稿 id,行名 demo-app-rollback-2、bundle_version=max+1、rollback_of=v1.0.0 的 id。同时,编排器经 DeploymentFailureNotifier 直写一条系统级告警事件(alert_events,rule_id=0)并开一张事件单(incidents,G12),事故第一时间可查。
- 结果洞察
- 就绪门控失败 → 自动回滚上一稳定版,全程无人值守;回滚是一条新期望状态行而非改旧行,审计日志同步记录 DEPLOYMENT_START 与 DEPLOYMENT_FAILED(含 auto_rollback 引用)。把回滚草稿激活后,Spoke 下一轮拉取即见新 digest 并重新部署 v1.0.0。
- 调整建议
- 就绪超时长短在策略 readiness_timeout_sec 里调(默认 60s);探针判据以 probes.readiness 为准,liveness 只决定"要不要重启",别混用;想要更保守可把 auto_rollback 显式关掉,失败只告警。
- 动手试一试
- 登录:http://127.0.0.1:18082,账号 admin / admin1。页面路径:部署总览 /apollo → 部署与漂移 /apollo/deployments。输入内容:登记 v1.1.0(web 探针改 http /healthz)→ 激活 → 发起部署(agent_name=agent-demo-01、policy_name=default)→ advance 推进直到 web 超时。预期结果:部署记录 failed,响应带 auto_rollback=回滚草稿 id。
- 限制提示
- 自动回滚只回滚到"上一稳定成功版本"(同 app 内 bundle_version 最高且低于当前、非 rollbacked 的行);防循环:对 auto-rollback 产生的版本不再触发自动回滚,需人工处置;回滚草稿仍需手动激活才会被 Spoke 拉取;若 Agent 离线导致部署悬挂超 5 分钟,调度器会自动置 failed(审计 DEPLOYMENT_TIMEOUT,F7),回连后按最新 digest 幂等收敛。
故事 2
v1.2.0 部署成功但业务变慢,手动回滚到 v1.0.0
场景:手动回滚
角色:平台运维 / SRE
耗时:约 3 分钟
- 背景
- 周五 15:20,v1.2.0 已部署成功(deployment 状态 synced),但业务反馈"查询明显变慢"。这次部署本身没有失败,不会触发自动回滚。陈工决定手动回滚到 v1.0.0:他打开期望状态列表,记下 v1.0.0 的 id,调用回滚 API。
- 传统做法对比
- 以前手动回滚 = 找运维翻 release 记录 → 拷旧包 → 改配置 → 重启,快则 1 小时;改了什么、谁改的都没有留痕,出了问题只能靠记忆复盘。
- 角色
- 陈工(平台运维 / SRE,回滚动作的操盘手);审计日志自动记录操作人与时间,供刘经理事后核查。
- 操作步骤
-
- GET /desired-states?app=demo-app,确认 v1.0.0 的历史期望状态 id
- POST /desired-states/:当前id/rollback,body 填 target_desired_state_id=v1.0.0 的 id
- 响应返回新草稿 id(kind=rollback)
- POST /desired-states/:新id/activate 激活回滚草稿
- Spoke 下一轮拉取即见新 digest,按 v1.0.0 内容收敛
- 系统响应
- rollback 接口返回:
{
"id": 8,
"kind": "rollback"
}
新行 name=demo-app-rollback-3、bundle_version=per-app max+1、rollback_of=v1.0.0 的 id、digest 复用 v1.0.0 的内容(同一 bundle 引用);原 v1.2.0 行被标记 rollbacked。
- 结果洞察
- 回滚被做成了"指回任意历史版本的一条新部署记录",不覆盖任何历史、可随时审计;因为 bundle_version=max+1,Spoke 的"新版本 > 已应用"校验天然通过,不需要为回滚开特殊后门。重复发起同一目标的 rollback 幂等(同一内容同一 digest,Spoke 判定无更新)。
- 调整建议
- 回滚前先在"部署与漂移"页看当前应用版本,确认目标 id 别选错;回滚后跑一次 POST /drift/check 确认环境已收敛;把回滚动作沉淀成团队操作手册,避免现场翻文档。
- 动手试一试
- 登录:admin / admin1。页面路径:部署总览 → 期望状态列表。输入内容:GET /desired-states 记下 v1.0.0 id → POST /desired-states/:id/rollback(target=v1.0.0 id)→ 激活新草稿。预期结果:返回 {id, kind:"rollback"},新行 bundle_version=max+1、rollback_of 指向 v1.0.0。
- 限制提示
- 回滚目标必须与当前期望状态同 app,跨应用报 400;回滚只生成草稿不自动激活;若目标 digest 与当前已应用内容相同,Spoke 会判定"无更新"而不重复应用。
故事 3
回滚后漂移收敛,组件从 failed 恢复到 ready
场景:漂移收敛
角色:平台运维 / SRE
耗时:约 4 分钟
- 背景
- 周一 09:40,v1.2.0 部署失败自动回滚后,陈工在"部署与漂移"页看到部署记录已 failed,但目标机上的 web 进程还残留 v1.2.0 的配置——回滚草稿虽然生成了,环境还没跟上来。他打开漂移事件列表,准备核对差异并触发收敛。
- 传统做法对比
- 以前回滚完靠人肉核对每个环境"配置到底对不对",diff 全靠眼睛;漏一个字段就是一次线上事故,核对一次少说 2 小时。
- 角色
- 陈工(平台运维 / SRE,查看漂移并触发收敛);阿强(应用开发,配合确认应用侧配置,只读)。
- 操作步骤
-
- GET /drift/events?desired_state_id=回滚草稿id,查看结构化 diff
- 读 diff 数组:字段级 JSON Patch(op/path/expected/actual)
- POST /drift/:事件id/reconcile 触发收敛(或等 Spoke 自动调和)
- GET /deployments/:id/components 确认组件 status=ready
- 系统响应
- 漂移事件 diff 为路径级 JSON Patch:
[{"op":"replace","path":"/components/0/version",
"expected":"1.0.0","actual":"1.2.0"},
{"op":"replace","path":"/components/0/probes/readiness/url",
"expected":"127.0.0.1:8080","actual":"127.0.0.1:9000"}]
reconcile 收敛后返回 has_drift=false;组件状态列表里 web / db 回到 status=ready。
- 结果洞察
- 漂移检测是字段级结构化比对(config_drift / version_drift / file_drift / process_drift),能看到"具体哪个字段不一致、期望是什么、实际是什么";收敛由期望状态驱动——digest 变化后 Spoke 自动重新应用。seed 的 ignore_paths 已排除 /components/*/runtime/* 这类运行时字段(PID、restart_count),不会制造噪音。
- 调整建议
- ignore_paths 别配太宽(如 ** 会把真实漂移也遮掉);收敛后仍不一致的组件看 deployment 状态是否 drifting / degraded,再人工处理;把漂移事件与部署记录关联起来排查根因。
- 动手试一试
- 登录:admin / admin1。页面路径:部署与漂移 /apollo/deployments → 漂移事件。输入内容:回滚后 GET /drift/events 看 diff,POST /drift/:id/reconcile。预期结果:diff 显示 version / readiness 的 expected 与 actual;收敛后组件恢复 ready。
- 限制提示
- 演示 Poller 只拉取 / 上报、不真实部署进程,漂移收敛的"真正应用"需配置 ProcManager(仅 process 运行时)才会执行;/drift/:id/reconcile 只做判定与记录,实际应用由 Spoke 完成。
故事 4
单调版本防回滚:想退回旧版本必须走新版本号
场景:供应链安全
角色:安全合规 + 平台运维
耗时:约 6 分钟
- 背景
- 刘经理(安全合规)在例会里提了一件事:上周有台服务器被"悄悄回滚"到旧版本,而那个旧版本恰好带着已修复的安全漏洞,等发现时已经运行了一周。她要求 Apollo 必须从机制上防住"直接下发低版本 bundle"。
- 传统做法对比
- 以前直接覆盖发旧包,版本无痕,安全团队事后才从日志里翻出"上周谁把版本换回去了";审计和版本对不上,责任也追不清。
- 角色
- 刘经理(安全合规,提出要求、核验机制);陈工(平台运维 / SRE,演示单调版本与回滚的正确姿势)。
- 操作步骤
-
- 说明 bundle digest 是不可变连接键:同一内容一定得到同一 digest(DB UNIQUE),内容一改 digest 必变
- 解释 bundle_version 按应用单调递增:登记期望状态时平台自动取 per-app max+1,手填低版本也会被顶上去
- 演示合法回滚:POST /desired-states/:id/rollback 生成新行(bundle_version=max+1、digest 复用旧内容)
- 在审计日志确认这次回滚操作完整留痕(谁、何时、从哪到哪)
- 系统响应
- 绕过 Hub 直接向 Spoke 投递旧制品的场景,校验链会拒绝并返回错误码 VERSION_ROLLBACK_REJECTED(新 bundle_version 未高于已应用版本);而正规 rollback 生成的期望状态 bundle_version=max+1,天然通过"新版本 > 已应用"校验——不需要为回滚开"显式例外"分支。
- 结果洞察
- "防止悄悄回滚"的本质:旧版本可能携带已修复的漏洞,直接降级等于把洞重新打开。单调版本 + 不可变 digest 从机制上消灭了"降级捷径"——要么走 rollback 生成高版本号新行(留痕、可审计),要么就被拒绝。版本演进永远向前,回溯永远可见。
- 调整建议
- bundle 版本号不要手填,交给平台按 per-app 递增;每次升级都核对审计日志里的 DEPLOYMENT_START / DEPLOYMENT_SYNC 事件;安全团队定期抽查 deployment_records 的 kind 字段(deploy / rollback / auto-rollback)分布。
- 动手试一试
- 登录:admin / admin1。页面路径:期望状态列表 + 审计日志。输入内容:用 rollback 接口回滚到 v1.0.0,观察新行 bundle_version=max+1;再尝试手填一个低 bundle_version 登记期望状态。预期结果:登记时 bundle_version 仍是 max+1;rollback 留痕在审计日志可见。
- 限制提示
- 单调版本是 per-app 约束(bundle_version 每 app 唯一);当前控制平面靠 DB UNIQUE + 生成时 max+1 保证,Spoke 端"新版本 > 已应用"的校验链属于设计中的防线,完整 Spoke 部署校验(gRPC/mTLS)属 V2。
常见问题
自动回滚和手动回滚有什么区别?
自动回滚由部署编排触发:组件就绪超时 / 探针失败时,策略 auto_rollback=true 会把期望状态指回"上一稳定成功版本"并生成回滚草稿,适合没人盯的发布窗口;手动回滚是主动动作:POST /desired-states/:id/rollback 指定任意历史版本,适合"部署成功但业务异常"这类情况。两者最终都会生成一条新的期望状态行。
回滚之后要做什么?
回滚只生成草稿,需要 POST /desired-states/:id/activate 激活才会被 Spoke 拉取;激活后建议跑一次 POST /drift/check 确认环境已收敛、组件回到 ready;最后到审计日志里确认这次回滚完整留痕。
为什么说"单调版本"能防悄悄回滚?
bundle_version 按应用单调递增(DB UNIQUE),登记期望状态时平台自动取当前最大值 +1,任何人无法构造一个"更低"的新版本。想退回旧内容,唯一合法路径是 rollback——它会生成一个更高版本号的新行(digest 复用旧内容),版本号高、内容旧,既能被 Spoke 接受又全程留痕,不会出现"无痕降级"。
回滚失败或一直不收敛怎么办?
先看 deployment 状态:failed 说明组件就绪没通过;drifting / degraded 说明环境与期望状态不一致。检查漂移事件里的 diff 是否被 ignore_paths 误遮、Agent 是否在线;必要时人工处理配置后手动触发 reconcile,或再走一次 rollback 到别的历史版本。
演示环境的数据会一直保留吗?
demo seed(demo-app / stable 通道 / default 策略 / demo-signer)在服务器启动时幂等创建,重启不会清空;期望状态、部署记录、漂移事件、审计日志都持久保存在平台库,可以反复练习回滚流程。
部署失败会自动告警吗?
会。部署失败(含自动回滚触发)会经 DeploymentFailureNotifier 直写一条系统级告警事件(alert_events,rule_id=0)并开一张事件单(incidents,project_id=0),标题区分"部署失败(已自动回滚)/(未自动回滚)"(G12)。Agent 离线导致的部署悬挂超 5 分钟则由超时检查器置 failed 并落审计 DEPLOYMENT_TIMEOUT(F7)。
主题小结
一句话:回滚不是"把版本改回去",而是"把期望状态指回某个历史内容,并生成一条可审计的新部署记录"。部署失败有 auto-rollback 兜底并自动告警(G12),业务异常有手动 rollback 任意版本,Agent 离线悬挂部署有超时兜底(F7),回滚后的环境靠漂移检测 + 收敛对齐,而单调版本 + 不可变 digest 保证了任何回退都"向前走、留得下、查得清"。记住边界:自动回滚只退上一稳定版、回滚是草稿要激活、跨应用回滚不行、演示 Poller 不真实部署进程。