早期发布是把构建目录 rsync 到服务器。回滚文档写着“重新发布上一版本”,真正出问题时才发现上一版本依赖已经变了、数据库迁移也执行过了,所谓回滚只是重新赌一次。2021 年我们开始把“能否回滚”当成发布设计的一部分,而不是事故发生后的命令搜索。

新版本部署、健康检查、渐进切流量和异常回切的发布时序

图 1:新版本先存在但不承载全部流量,证明健康后再逐步接管。

产物一旦构建就不再改变

CI 对提交 SHA 构建一次,生成 artifactId,记录依赖锁、Node 版本、环境无关配置和校验和。测试、预发、生产部署同一份产物,只注入环境配置。

{
  "artifactId": "web-20210522-a91f3c2",
  "gitSha": "a91f3c2",
  "node": "14.17.0",
  "sha256": "...",
  "builtAt": "2021-05-22T08:12:03Z"
}

回滚只是选择旧 artifactId,不重新安装依赖。制品库按保留策略保存最近稳定版本,发布记录指明当前每个环境使用哪份产物。

健康检查要覆盖真实依赖

进程能监听端口不等于可接流量。我们分两类:liveness 只判断进程是否失去响应;readiness 检查必要配置、数据库连接和关键依赖是否足以服务请求。依赖短暂波动时,不应让所有实例一起被重启。

新实例先 readiness 通过,再跑冒烟用例:登录、读取核心列表、创建一条可清理测试数据。只有结果和延迟都满足条件,才进入小流量。

自动停止比自动发布更重要

每个发布阶段都有观察窗口和停止条件:

阶段 流量 观察指标 异常动作
warm-up 0% 健康、依赖、启动日志 终止新版本
canary 1% 5xx、P95、核心业务失败 自动回切
ramp 10% → 50% 资源、队列积压、错误差异 停止扩量
full 100% 全局 SLO 与旧版本连接耗尽 保留旧实例一段时间

阈值按新旧版本对照,而不是只看绝对值。业务高峰本来就有波动,单一固定阈值容易误判。

数据库变更必须向前兼容

应用可以回滚,破坏性数据库迁移不能。我们采用 expand/contract:先增加新字段和双读兼容;新版本逐步写新格式;回填历史数据;确认旧版本不再运行后,才移除旧字段。

发布 N: 新增 nullable 字段,应用仍读旧字段
发布 N+1: 双写新旧字段,读新失败回退旧
回填: 分批迁移并核对数量
发布 N+2: 只读新字段,仍保留旧字段
观察期后: 删除旧字段

如果一次发布必须依赖不可逆迁移,它就不具备简单回滚,需要准备前向修复和业务停机方案,不能在界面上仍展示一个绿色“回滚”按钮。

回滚也需要演练和审计

我们定期在预发执行:选定旧 artifactId、切回流量、确认数据库兼容、验证缓存与异步任务。发布平台记录谁在何时因为哪个指标触发回滚,以及回滚后是否恢复。

最重要的变化,是团队不再把回滚视为发布失败。渐进发布里,自动停止和回切恰恰说明保护机制在工作。真正失败的是系统已经看到异常,却因为回滚路径未知而继续扩大影响。