早期发布是把构建目录 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、切回流量、确认数据库兼容、验证缓存与异步任务。发布平台记录谁在何时因为哪个指标触发回滚,以及回滚后是否恢复。
最重要的变化,是团队不再把回滚视为发布失败。渐进发布里,自动停止和回切恰恰说明保护机制在工作。真正失败的是系统已经看到异常,却因为回滚路径未知而继续扩大影响。