线上数据库迁移卡在 ALTER TABLE 一动不动

ALTER TABLE 迁移在生产卡住。用 pg_stat_activity 找出阻塞者、终止它、加 lock_timeout 重跑。

部署到一半,迁移卡在 ALTER TABLE orders ADD COLUMN ... 不动了。CPU 平的、没有报错。Postgres 的大多数 DDL 要 ACCESS EXCLUSIVE 锁,只要还有别的事务摸过这张表——哪怕是分析师一条懒得提交的 SELECT,或者一个 pg_dump——就得排队。更糟的是,迁移在等锁的时候,后续所有查询都排在它后面卡住。修法是先定位是谁挡着、kill 掉,然后加 lock_timeout 重跑,让以后的迁移失败得干脆。

常见原因

按踩坑频率排序。

1. 长事务持有竞争锁

一个报表查询、忘关的 psql 会话、ORM 没收尾的事务,正持有目标表的 ROW EXCLUSIVEACCESS SHARE

怎么判断pg_stat_activity 里能看到一条对同一张表跑了好久的语句。

2. pg_dump 正在跑

pg_dump 整个 dump 期间对每张表都拿 ACCESS SHARE。ALTER TABLE 只能等。

怎么判断pg_stat_activity 里有 application_name = 'pg_dump' 的会话。

3. 同一张表的 autovacuum

普通 autovacuum 持的锁不阻塞查询,但阻塞 DDL。更糟的是 VACUUM FULLCLUSTER 自己就要 ACCESS EXCLUSIVE

怎么判断pg_stat_activity.query 显示 autovacuum: VACUUM table

4. 应用层 idle in transaction

BEGIN 了再没关——常见于调试器、或连接断开但没正常 close。

怎么判断state = 'idle in transaction' 已经好几分钟了。

5. DDL 不是被挡,是真在干活

ADD COLUMN ... DEFAULT non_constant(老版本 Postgres)或者 ADD CONSTRAINT ... NOT VALID 然后 VALIDATE CONSTRAINT,都要全表扫描。它没卡,它在跑。

怎么判断:会话状态是 active 不是 waiting;wait_event_type IS NULL

最短修复路径

Step 1: 定位阻塞者

SELECT
  blocked.pid       AS blocked_pid,
  blocked.query     AS blocked_query,
  blocking.pid      AS blocking_pid,
  blocking.usename  AS blocking_user,
  blocking.application_name,
  blocking.state,
  age(now(), blocking.xact_start) AS xact_age,
  blocking.query    AS blocking_query
FROM pg_stat_activity blocked
JOIN pg_stat_activity blocking
  ON blocking.pid = ANY(pg_blocking_pids(blocked.pid))
WHERE blocked.query ILIKE 'ALTER TABLE%';

直接告诉你是谁在挡迁移。pg_blocking_pids() 比老式连 pg_locks 的写法准、也轻。

Step 2: 终止阻塞者(小心点)

大多数应用会话可以放心取消:

-- 先 cancel(温柔点)
SELECT pg_cancel_backend(12345);

-- 不放就 terminate
SELECT pg_terminate_backend(12345);

pg_dump 是备份链路的就别瞎 kill——要么暂停迁移让 dump 跑完,要么把迁移排到 dump 之后。

Step 3: 加 lock_timeout 重跑

让以后的迁移失败得干脆、可以重试。

SET lock_timeout = '5s';
SET statement_timeout = '0';  -- DDL 本身可能就要跑很久
ALTER TABLE orders ADD COLUMN shipping_method text;

迁移工具配置(Flyway、Alembic、Prisma):

# alembic.ini 片段——每个迁移前生效
sqlalchemy.connect_args = { "options": "-c lock_timeout=5s" }
-- Flyway:在迁移脚本顶上设
SET lock_timeout TO '5s';

拿不到锁就报 canceling statement due to lock timeout,重试。小步重试比一次长卡好得多。

Step 4: 选更安全的 DDL 写法

大表加列:

-- Postgres 11+:加可空列是纯元数据操作
ALTER TABLE orders ADD COLUMN shipping_method text;

-- 加 NOT NULL 拆两步
ALTER TABLE orders ADD COLUMN shipping_method text;
UPDATE orders SET shipping_method = 'standard' WHERE shipping_method IS NULL;  -- 分批
ALTER TABLE orders ALTER COLUMN shipping_method SET NOT NULL;

新索引用 CREATE INDEX CONCURRENTLY(不要 ACCESS EXCLUSIVE):

CREATE INDEX CONCURRENTLY orders_shipping_method_idx
ON orders (shipping_method);

注意:不能在事务里跑;有些迁移工具需要 opt-out(Alembic 用 op.create_index(..., postgresql_concurrently=True) 配合 op.execute("COMMIT"))。

Step 5: 确认迁移落地了

\d+ orders                                   -- 列在
SELECT relname, n_live_tup FROM pg_stat_user_tables WHERE relname = 'orders';
SELECT * FROM pg_locks WHERE relation = 'orders'::regclass;  -- 没有遗留锁

预防

  • 每个迁移都设 lock_timeout(2-30 秒)且可重试。
  • 把迁移排在备份和重报表窗口之外。
  • CREATE INDEX CONCURRENTLY;大表别用 ADD COLUMN ... DEFAULT non_constant
  • idle in transaction 超过 60 秒告警——这是常见嫌疑人。
  • 高风险迁移先在生产数据量级的影子库上跑一次(pg_dump + pg_restore 到 staging)。

标签: #后端 #排查 #迁移