零停机迁移
概述
一种模式变更规范,确保在滚动部署期间运行的系统保持在线。在发布过程中,旧 Pod 和新 Pod 共享同一个数据库——因此模式必须同时兼容两个应用版本。 将破坏性迁移与依赖该迁移的部署捆绑在一起是导致自产停机的经典错误。
编写规则(所有项目)
- 两种迁移类型,保持区分: 幂等的
current/集合(开发迭代——每次循环重新运行,使用CREATE OR REPLACE/IF NOT EXISTS,对象处于最终形式,创建后无ALTER)和已提交的迁移(不可变、有序,实际发布的版本)。ALTER DEFAULT PRIVILEGES是唯一允许的内联ALTER。 - 所有权源自迁移运行的方式,而非零散的
ALTER ... OWNER。 - 单一引导源,由本地/Docker 初始化路径和影子/测试数据库共享——每个环境以相同方式构建。
扩展/收缩(并行变更)
适用场景: 数据库包含在部署中必须存活的数据(阶段性数据或生产数据)。例外: 本地/可重置项目——但请按照此原则适用的方式编写已提交迁移,以便升级时无需重写。
模式变更分为三个发布阶段,而非一次完成:
- 扩展 —— 以增量方式添加新结构,保持向后兼容:新可空列、新表、新函数。应用版本 N 继续正常运行。
- 迁移 —— 在有限批次中进行回填(绝不用一个表锁
UPDATE);如果需要,从应用 N+1 进行双写;将读取切换到新结构。此时才添加NOT NULL(先添加NOT VALID约束,然后单独执行VALIDATE)。 - 收缩 —— 仅在应用 N 完全退役后,在后续发布中移除旧列/约束。
Postgres 常见陷阱
| 陷阱 | 安全方法 |
|---|---|
带有易变/大型回填默认值的 NOT NULL 列 | 添加可空列 → 分批回填 → 添加 NOT VALID 约束 → 执行 VALIDATE |
| 添加约束 | 先添加 NOT VALID,然后 VALIDATE CONSTRAINT(锁较弱) |
| 构建索引 | CREATE INDEX CONCURRENTLY — 不能在事务中运行;隔离迁移工具的事务包装 |
| 阻塞的迁移冻结生产环境 | 设置 lock_timeout / statement_timeout 以便快速失败 |
| 重命名列 | 采用扩展/收缩模式:跨发布阶段添加新列 → 回填 → 切换读取 → 删除旧列 |
为什么“最终形式 CREATE OR REPLACE”在生产环境中不够用
该规则是开发人员的最佳实践,在开发环境中正确。但在生产环境中(有真实用户和滚动部署),两个应用版本在发布期间共享模式——因此扩展/收缩才是确保安全的方法。不要混淆两者。
完整原理:参见随附的 engineering-constitution/references/engineering-constitution.md 文档第 XVII 条。部署管道:cloud-delivery-aks。