标签: Replication Slot

  • 深入 PostgreSQL 陷阱排查:僵尸复制槽引发的 xmin 冻结与 Autovacuum I/O 风暴实战

    排查过程中,我们接手了一个险些造成核心交易系统雪崩的 PostgreSQL 性能故障。表现非常直接:主库 Load Average 突然从 2 飙升到 80+,核心订单表查询的 99 线延迟从 5ms 暴涨至 3s 以上,底层 NVMe SSD 的 I/O 写入利用率打满 100%。

    直接抛出结论:下游大数据团队停用了一个 Flink CDC 抽取任务,但未在 PG 主库清理对应的逻辑复制槽(Logical Replication Slot)。这个处于离线(active = false)状态的僵尸复制槽死死拖住了全局的 catalog_xmin 推进,导致主库 MVCC 机制失效,几个核心大表的死元组(Dead Tuples)疯狂膨胀。当某位值班兄弟发现 WAL 目录快满,为了释放空间“急中生智”手动 Drop 掉该复制槽时,被压抑已久的 xmin 水位线瞬间释放,触发了史诗级的 Autovacuum 工作流。几个 Worker 进程以近乎全速的 I/O 扫描并清理上百 GB 的死元组,直接击穿了存储 I/O,引发全站查询阻塞。

    盲目 Drop 复制槽而不评估 MVCC 历史债务,是典型的只管表象不顾底层的操作。下面拆解底层的运作机理和防御性配置。

    案发现场与指标特征

    故障爆发时,首先看 iostat:

    iostat -dx 1
    Device:         rrqm/s   wrqm/s     r/s     w/s    rkB/s    wkB/s avgrq-sz avgqu-sz   await r_await w_await  svctm  %util
    nvme0n1           0.00     0.00 45200.00 1205.50 361600.00 15600.00    16.25    32.50    0.70    0.65    2.50   0.02 100.00
    

    磁盘 %util 顶在 100%,大量的随机读(r/s 极高),这通常不是正常的业务流量。

    切到数据库内部看 pg_stat_activity 的等待事件:

    SELECT pid, wait_event_type, wait_event, state, query 
    FROM pg_stat_activity 
    WHERE state = 'active' AND wait_event_type = 'IO';
    

    发现几十个业务连接卡在 DataFileRead,更致命的是,出现了几个底层的进程,query 字段显示: autovacuum: VACUUM public.orders

    回头翻看慢查询日志和监控,发现在 Autovacuum 启动前,有人执行了: SELECT pg_drop_replication_slot('flink_cdc_orders');

    这就是血案的导火索。

    底层原理解析:为什么 Drop 复制槽会击穿 I/O?

    PostgreSQL 的 MVCC 实现与 MySQL/Oracle 不同,它没有独立的 Undo Log 表空间,而是将旧版本的数据(Tuple)直接留在原表的 Heap 文件中。

    为了判断哪些旧版本 Tuple 可以被安全清理,PG 维护了一个全局的可见性底线——xmin horizon。如果系统中没有任何活跃事务、预备事务或复制槽需要看到某个旧版本,Autovacuum 就可以将其作为死元组(Dead Tuple)回收。

    逻辑复制槽(用于 CDC 日志解析)为了保证在解码 WAL 时能够读取到完整的系统表元数据,会持有一个 catalog_xmin。 只要下游 CDC 断开连接且不推进 LSN,这个复制槽就会变成“僵尸”,其 catalog_xmin 永远停留在断开的那一刻。

    -- 查看复制槽状态的致命视角
    SELECT slot_name, plugin, active, xmin, catalog_xmin, restart_lsn 
    FROM pg_replication_slots;
    

    连锁反应如下:

    1. 垃圾回收停滞:catalog_xmin 停滞,导致全局 xmin horizon 无法推进。

    2. 表极度膨胀:业务高频执行 UPDATE/DELETE,产生海量 Dead Tuples,但 Autovacuum 扫描后发现这些 Tuple 还处于 xmin horizon 之上,无法物理回收。表体积从 50GB 膨胀到 200GB。

    3. WAL 堆积:物理/逻辑复制槽不仅阻塞 xmin,还会通过 restart_lsn 阻塞 WAL 日志的清理。PG 的 pg_wal 目录开始告警。

    4. 引爆 I/O 风暴:运维强行 pg_drop_replication_slot(),释放了 catalog_xmin 和 WAL 限制。xmin horizon 瞬间跳跃到当前最新。Autovacuum 再次醒来时,发现有 150GB 的死元组“终于”可以清理了,开始执行极其沉重的索引和堆表扫描。如果没有做好 I/O 成本节流配置,Autovacuum 会消耗掉所有磁盘带宽。

    止血与防御性配置落地

    1. 现场紧急止血

    不能任由 Autovacuum 把主库 I/O 打死,必须先将其阻断,再限流恢复。

    第一步,干掉正在作恶的 autovacuum 进程(PG 内部机制会自动重启它,但我们要争取时间修改配置):

    -- 找出 autovacuum worker 并 cancel
    SELECT pg_cancel_backend(pid) 
    FROM pg_stat_activity 
    WHERE query LIKE 'autovacuum: %';
    

    第二步,动态调低 Autovacuum 的 I/O 侵略性:

    -- 增加每次清理达到成本限制后的睡眠时间(默认可能是2ms,调大到20ms)
    ALTER SYSTEM SET autovacuum_vacuum_cost_delay = '20ms';
    -- 减小单次清理的成本限制(让它更快休眠)
    ALTER SYSTEM SET autovacuum_vacuum_cost_limit = 200;
    -- 重新加载配置
    SELECT pg_reload_conf();
    

    2. 长期防御性调优(避坑指南)

    别指望所有研发都会记得下线 CDC 时通知 DBA,系统必须具备自我保护能力。

    防御手段 A:限制复制槽的 WAL 保留量 (PostgreSQL 13+ 特性) 在 PG 13 之前,一个失联的复制槽可以直接把磁盘撑爆。PG 13 引入了救命参数 max_slot_wal_keep_size。当复制槽保留的 WAL 超过此值时,PG 会强制使该槽失效(Invalidate),宁可让下游 CDC 报错重做,也不能让主库挂掉。

    # postgresql.conf
    # 允许单个复制槽最多保留 50GB 的 WAL
    max_slot_wal_keep_size = 50GB 
    

    防御手段 B:杀死长连接与长事务 除了复制槽,长时间 Idle in transaction 的连接同样会死锁 xmin horizon。

    # postgresql.conf
    # 强制断开闲置超过 10 分钟的事务连接,防止 xmin 冻结
    idle_in_transaction_session_timeout = 600000 
    

    防御手段 C:精细化 Autovacuum I/O 控制 默认的 Autovacuum 参数在 SSD 时代显得过于保守,但如果是极度膨胀后的突发清理,又会过于激进。建议基于磁盘 I/O 能力计算 cost_limit,不要使用一刀切的默认值。基于 NVMe 盘的基线建议:

    autovacuum_max_workers = 5
    autovacuum_vacuum_cost_limit = 2000
    autovacuum_vacuum_cost_delay = 2ms
    

    保证日常能快读清理完毕,不至于把债务堆积到爆发。

    排查清单:PostgreSQL 存储膨胀与 I/O 突增速查

    为了便于后续快速定位同类问题,请重点检查以下几项:

    1. 检查全局 xmin 阻塞源: 执行 SELECT * FROM pg_replication_slots WHERE active = false; 查找僵尸复制槽。

    2. 排查长事务/预备事务: 查询 pg_stat_activity 中 state = 'idle in transaction' 且 backend_xid / backend_xmin 极老的会话;排查 pg_prepared_xacts 中未提交的 2PC 事务。

    3. 确认表膨胀率 (Dead Tuples): 查询 pg_stat_user_tables,对比 n_live_tup 与 n_dead_tup 的比例,若 n_dead_tup 极高且持续不降,说明 MVCC 清理链已断裂。

    4. 监控 Autovacuum 行为: 观察系统视图 pg_stat_progress_vacuum,检查当前是否有表正在经历阶段耗时极长的 vacuum_index 或 vacuum_heap。

    5. WAL 堆积安全阈值验证: 检查当前 PG 版本是否配置了 max_slot_wal_keep_size 作为底线防御,否则将随时面临全盘占满的宕机风险。