--- id: devops.cicd.release-rollback title: 发布与回滚:镜像标签、环境、滚动 / 蓝绿 / 金丝雀、数据库迁移 type: lesson domain: devops stack: [cicd] tags: [发布策略, 回滚, 蓝绿发布, 金丝雀, 数据库迁移, 特性开关] level: intermediate prerequisites: [devops.cicd.pipeline] est_hours: 3 related: [devops.cicd.scenario.multi-lang-pipeline, devops.nginx.https-stream-limit, devops.cicd.interview] week: [14] order: 2 --- # 发布与回滚:镜像标签、环境、滚动 / 蓝绿 / 金丝雀、数据库迁移 ## 解释说明 **不可变产物**:一次构建出镜像(标签 = 提交哈希或版本号),测试、预发、生产用**同一个镜像**,只是配置不同。这样测试过的东西就是上线的东西,回滚就是切回上一个标签。 **环境**:开发 → 测试 → 预发(和生产配置尽量一致)→ 生产。配置通过环境变量注入,不打进镜像。 **发布策略** | 策略 | 做法 | 优点 | 缺点 | | --- | --- | --- | --- | | 重建 | 停掉旧版本再启动新版本 | 最简单 | 有停机 | | 滚动 | 逐台替换实例 | 无停机、资源占用少 | 新旧版本同时在线一段时间;回滚也要逐台 | | 蓝绿 | 准备一整套新环境,验证后切流量 | 切换和回滚都是瞬间 | 需要双倍资源 | | 金丝雀 | 先给小部分流量,观察指标后逐步放大 | 风险最小 | 需要流量控制能力和监控 | **回滚**:应用回滚 = 部署上一个镜像标签。难点在**数据库**:新版本已经改了表结构,旧版本可能跑不起来。 **数据库迁移的原则**:迁移脚本放在仓库里、有版本号、由工具执行(Go 的 golang-migrate / goose,Python 的 Alembic);**每次迁移都要向后兼容**——新旧代码都能在新表结构上运行。删除或重命名字段要分多次发布(“扩展 → 迁移 → 收缩”): 1. 加新字段(旧代码不受影响),新代码同时写新旧字段; 2. 回填历史数据,新代码改为读新字段; 3. 确认不再回滚到旧版本后,再删除旧字段。 **特性开关**:代码先上线但功能默认关闭,通过配置打开;出问题直接关开关,比回滚代码更快。 ## 案例 一次发布要把 `users.name` 改名为 `display_name`,迁移脚本直接执行了 `RENAME COLUMN`。发布后发现新版本有 bug,回滚到旧版本——旧代码找不到 `name` 字段,全站报错,故障时间翻倍。按“扩展 → 迁移 → 收缩”分三次发布就能避免。 ## 代码示例 单机部署 + 健康检查 + 失败自动回滚(完整脚本 [examples/pipeline/deploy.sh](examples/pipeline/deploy.sh)): ```bash previous="$(cat "$STATE_FILE" 2>/dev/null || true)" # 上一次成功部署的标签 deploy "$IMAGE_TAG" if healthy; then echo "$IMAGE_TAG" > "$STATE_FILE" else echo "健康检查失败,回滚到 $previous" >&2 [[ -n "$previous" ]] && deploy "$previous" exit 1 fi ``` “扩展 → 迁移 → 收缩”的迁移脚本(PostgreSQL): ```sql -- 第 1 次发布:扩展(旧代码不受影响) ALTER TABLE users ADD COLUMN display_name TEXT; -- 第 2 次发布前:回填(大表要分批执行) UPDATE users SET display_name = name WHERE display_name IS NULL; -- 第 3 次发布:确认不再回滚到读 name 的版本后,收缩 ALTER TABLE users DROP COLUMN name; ``` ## 易错点 - **每个环境重新构建镜像**:测试过的和上线的不是同一个东西。 - **迁移不向后兼容**:应用无法回滚。 - **应用启动时自动执行破坏性迁移**:多实例同时启动会并发迁移;迁移失败导致所有实例起不来。迁移作为单独的发布步骤执行。 - **只看“部署成功”不看业务指标**:容器起来了不代表功能正常。发布后观察错误率、延迟、关键业务指标。 - **回滚流程从来没有演练过**:真出事时脚本跑不通。 - **特性开关从不清理**:代码里分支越来越多。功能稳定后删除开关。 ## 面试常问 见 [CI/CD 面试题](interview.md):蓝绿和金丝雀的区别、怎么回滚、数据库变更怎么做到可回滚。