PostgreSQL 与 Prisma 零停机迁移:兼容发布、分批回填与安全收缩
本文目录
线上迁移的难点不是生成一段 DDL,而是新旧应用版本会并存、数据量和锁等待不可预测、发布可能中断。常用策略是 expand-and-contract:先增加与旧代码兼容的结构,再部署能双读/双写的新代码,分批回填和验证,最后经过观察窗口再删除旧结构。数据库、应用和后台 worker 必须能在每个中间态安全共存。
零停机是目标,不是 DDL 的保证。先评估 PostgreSQL 版本、表大小、写入速率、复制延迟、长事务、锁模式、磁盘空间、索引构建成本和回滚方案;在生产同量级数据上排练并监控。
1. 把一项改动拆成兼容阶段
假设要把 users.display_name 拆成 given_name 和 family_name:
- Expand:添加新列,保持可空,不立刻删/改旧列;部署读旧写旧与兼容新列的数据库结构。
- Dual-write:发布代码,同时维护旧字段和新字段;读取时优先新列,旧数据为空时安全回退。保证事务内双写,避免两列短暂不一致。
- Backfill:按稳定主键范围分小批回填;可重复运行、限速、记录 checkpoint,并监控锁、WAL、复制延迟和错误。
- Verify:比较空值/不一致计数、抽样业务结果与写入日志。验证程序和回填完成标记不能互相替代。
- Switch reads:部署只读新列的版本,观察完整回滚窗口;仍不要删旧列,因为回滚版本可能还会读它。
- Contract:确认所有旧进程、worker、脚本和只读副本不再依赖旧列后,单独发布删除旧列的迁移。
每一步都要支持重试。版本 N+1 必须能与旧 schema 的剩余部分协作;不能把“迁移成功后立即删列”塞进同一个发布,让回滚只剩恢复整库。
2. PostgreSQL DDL 的锁与约束策略
ALTER TABLE 的子命令会取得各自要求的锁;组合多个操作时使用其中最严格的锁。即使某个 DDL 很快,等待长事务取得锁也可能让请求排队。先设置有界 lock_timeout,让迁移在锁竞争时快速失败并重试,而不是无限阻塞线上查询;statement_timeout 也需按操作和环境明确设置。
新增列通常先保持 nullable。带常量 DEFAULT 的加列在较新的 PostgreSQL 可避免逐行重写,但仍需短暂强锁,具体行为依赖版本、默认值表达式与表状态;不能把它当成永远零成本。对大表更稳妥的方案是先加 nullable 列、分批填充,再设置默认值/约束。
大表约束可分阶段验证:先增加 NOT VALID 的 CHECK 或外键,使新写入接受约束,再通过 VALIDATE CONSTRAINT 扫描已有数据;验证失败时先定位坏数据,不要盲目重跑。NOT NULL 前先检查并回填所有空值,再在可控维护窗口应用。
BEGIN;
SET LOCAL lock_timeout = '2s';
SET LOCAL statement_timeout = '30s';
ALTER TABLE users ADD COLUMN display_name_v2 text;
COMMIT;
ALTER TABLE users
ADD CONSTRAINT users_display_name_v2_nonempty
CHECK (display_name_v2 IS NULL OR length(display_name_v2) > 0) NOT VALID;
ALTER TABLE users VALIDATE CONSTRAINT users_display_name_v2_nonempty;
这是示意语句,不是可直接用于任意版本/生产数据的迁移文件。SET LOCAL 只在事务中有效;锁等待超时只是避免长时间排队,失败后仍要调查阻塞者。
3. 大表索引:CONCURRENTLY 有自己的运维代价
普通 CREATE INDEX 构建期间会阻止该表的写入。CREATE INDEX CONCURRENTLY 可让常规 INSERT/UPDATE/DELETE 继续,但要进行多轮扫描、等待旧事务,耗时更长并消耗 I/O;它不能放进事务块,也不是“完全不锁”。构建失败可能留下 INVALID 索引,必须检查并显式清理/重建。唯一索引还需要处理已有重复值,并发写入也可能令构建失败。
CREATE INDEX CONCURRENTLY users_display_name_v2_idx
ON users (display_name_v2);
Prisma 的 migration SQL 可按实际版本/数据库配置关闭迁移事务包裹,再在 migration 中放置并发索引语句;先检查生成的 SQL 和迁移执行器行为。禁止在一个包含 CREATE INDEX CONCURRENTLY 的事务里执行,也不要因为 IF NOT EXISTS 就跳过索引有效状态检查。
4. 可恢复的批量回填
不要在单个迁移事务里对数千万行执行 UPDATE:它会持有长事务、产生大量 WAL、延迟 vacuum/replica,并让回滚窗口变大。把回填做成可重启的运维 job:主键游标或 checkpoint、有上限的小批量、只更新仍为空的行、每批独立提交、速率可调、失败记录和最终校验。回填逻辑应能重复运行,不覆盖新代码已写入的值。
WITH batch AS (
SELECT id
FROM users
WHERE display_name_v2 IS NULL AND id > $1
ORDER BY id
LIMIT 500
FOR UPDATE SKIP LOCKED
)
UPDATE users AS u
SET display_name_v2 = COALESCE(NULLIF(u.display_name, ''), 'Reader')
FROM batch
WHERE u.id = batch.id AND u.display_name_v2 IS NULL
RETURNING u.id;
SKIP LOCKED 适合多个 worker 领取互不冲突的行,但单靠递增 ID 游标可能跳过被锁定而暂时未处理的记录。生产回填应以更新返回值/持久化队列状态确认进度,并在最终阶段全量查询残留空值与不一致数据。按实际主键类型、分区、触发器和写入竞争改写 SQL;批大小不是通用最佳值。
5. Prisma Migrate 发布与故障处理
- 把
prisma/schema.prisma与迁移目录一起纳入版本控制;在开发库生成迁移后审阅 SQL,检查锁、默认值、数据丢失和不可逆操作。生产只运行已审阅的迁移,不用db push代替迁移台账。 - CI 对空库与上一生产 schema 分别应用 migration;再跑应用测试和关键查询计划。部署通过
prisma migrate deploy应用待执行迁移,运行前备份并确认目标连接与迁移清单。 - 数据变更很大或需要节流时,把分批回填作为带进度/限速/告警的独立 job;Prisma migration 负责必要的结构扩展,不要隐藏不可控长任务。
- Prisma 出现 failed migration 时先读
_prisma_migrations与数据库真实 schema,确认哪些语句已成功、部分数据是否写入;修复数据库或迁移后依照官方migrate resolve流程标记。不能仅因命令报错就把迁移标成 applied。 - 数据库已经通过其他可信路径完成迁移、且每条 DDL/数据效果被核对时,才可能用 baseline/resolve 补充台账;补台账不执行 SQL,也不证明数据库结构正确。
应用兼容期要考虑滚动发布期间旧、新进程同时运行;迁移完成并不等于所有进程都已更新。发布前确认 worker/cron、人工脚本、只读副本与管理工具都不会读已弃用列。
6. 回滚不是迁移的反义词
代码回滚通常不会自动逆转 DDL,反向 SQL 也可能丢数据或无法重建写入期间的新值。优先使用向前修复:保持扩展 schema,部署修复代码,再清理坏数据。contract 阶段删除旧列后,老版本代码无法安全启动;必须有明确的备份恢复、保留旧列窗口或向前兼容策略。
迁移前备份要验证可恢复性,备份本身不是回滚按钮;至少在隔离库做恢复演练。生产操作应写下起止版本、迁移 ID、锁/复制指标、应用兼容版本和停止条件。预先定义何时中止、如何恢复服务、如何补偿数据,而非故障时临时决定。
7. 发布检查清单
- 迁移在真实规模副本排练;预计时长、锁模式、WAL/磁盘增量与复制延迟有测量。
- 已备份并验证恢复,migration SQL 审阅过;
lock_timeout、statement_timeout和停止条件明确。 - Expand 结构对新旧应用均兼容;代码采用 feature flag/双写时有一致性和回滚测试。
- Backfill 可暂停/重启/限速,checkpoint 可信,不覆盖新数据;最终不一致/残留记录检查为零或有书面处置。
- 并发索引检查
indisvalid等状态;约束验证、查询计划、写入延迟和错误率符合阈值。 - Contract 与 expand 分开发布,等待旧代码/worker/脚本退出并度过回滚观察期;删除列前确认数据备份和旧应用依赖已解除。
官方资料
- PostgreSQL ALTER TABLE:锁与约束验证语义。
- PostgreSQL CREATE INDEX:
CONCURRENTLY的扫描、失败索引和事务边界。 - PostgreSQL Explicit Locking:表锁、行锁和冲突矩阵。
- Prisma Migrate production deployment:生产应用迁移的流程与边界。
- Prisma expand-and-contract data migration:新旧字段兼容和数据迁移思路。
版本快照:官方链接按 2026-10-14 核对。PostgreSQL DDL 行为与 Prisma 迁移执行细节会随版本和连接配置变化;尤其确认数据库版本、迁移事务策略和生成 SQL,再在生产规模副本上演练。
Live Notes
留言提交后会先进入审核,审核通过后对其他读者公开。