PG 孤儿物理复制槽残留导致 pg_wal 膨胀 19G 故障分析

文章目录

  • [PG 孤儿物理复制槽残留导致 pg_wal 膨胀 19G 故障分析](#PG 孤儿物理复制槽残留导致 pg_wal 膨胀 19G 故障分析)
    • [1. 故障现象](#1. 故障现象)
      • [1.1 磁盘空间异常](#1.1 磁盘空间异常)
      • [1.2 复制槽查询发现异常](#1.2 复制槽查询发现异常)
    • [2. 排查过程](#2. 排查过程)
      • [2.1 关键指标解读](#2.1 关键指标解读)
      • [2.2 排除其他可能](#2.2 排除其他可能)
      • [2.3 复制槽基础原理(排查依据)](#2.3 复制槽基础原理(排查依据))
    • [3. 根因分析](#3. 根因分析)
      • [3.1 事故链](#3.1 事故链)
      • [3.2 缺陷定性](#3.2 缺陷定性)
      • [3.3 三个放大隐患](#3.3 三个放大隐患)
    • [4. 解决方案](#4. 解决方案)
      • [4.1 删除前安全检查(必做)](#4.1 删除前安全检查(必做))
      • [4.2 执行删除](#4.2 执行删除)
      • [4.3 验证结果](#4.3 验证结果)
      • [4.4 若需主动加速回收](#4.4 若需主动加速回收)
    • [5. 经验总结](#5. 经验总结)
      • [5.1 排查思路(可复用)](#5.1 排查思路(可复用))
      • [5.2 常用运维命令](#5.2 常用运维命令)
      • [5.3 预防措施](#5.3 预防措施)
      • [5.4 关键结论](#5.4 关键结论)

PG 孤儿物理复制槽残留导致 pg_wal 膨胀 19G 故障分析

项目 内容
故障日期 2026-09-17
故障现象 服务器磁盘被 pg_wal 占用 19G(正常应为几十 MB),存在撑满磁盘、拖死主库的风险
影响范围 停车场环境主库所在服务器;pg_wal 持续膨胀会最终写满磁盘导致数据库不可用
根因 配置 2 个从机时底层在主库创建了 2 个物理复制槽(命名含从机 IP);解除主从关系时未删除槽,叠加环境从 192 网段切换到 172 网段,旧槽再无消费者,成为孤儿槽并强制保留 WAL
解决方案 确认无消费者后,用 pg_drop_replication_slot() 删除 2 个孤儿槽,pg_wal 由 19G 降至 17M
环境信息 openEuler + PostgreSQL 15.18(PolarDB 15.18.5.0 内核);PGDATA:/postgresql/pgdata

1. 故障现象

1.1 磁盘空间异常

pg_wal 目录占用异常,达 19G:

bash 复制代码
[root@openEuler ~]# du -sh /postgresql/pgdata/pg_wal
19G     /postgresql/pgdata/pg_wal

[root@openEuler ~]# du -sh /postgresql/pgdata/
1.7G    /postgresql/pgdata/     # 删除前 pgdata 总量被 pg_wal 占据

1.2 复制槽查询发现异常

sql 复制代码
postgres=# select * from pg_replication_slots;
             slot_name              | plugin | slot_type | datoid | database | temporary | active | active_pid | xmin | catalog_xmin | restart_lsn | confirmed_flush_lsn | wal_status | safe_wal_size | two_phase
------------------------------------+--------+-----------+--------+----------+-----------+--------+------------+------+--------------+-------------+---------------------+------------+---------------+-----------
 repl_replication_slot_192_168_0_96 |        | physical  |        |          | f         | f      |            |      |              | 0/C37EFE8   |                     | extended   |               | f
 repl_replication_slot_192_168_0_99 |        | physical  |        |          | f         | f      |            |      |              | 0/C350168   |                     | extended   |               | f
 repl_replication_slot_172_168_0_99 |        | physical  |        |          | f         | t      |    1009698 |      |              | C/3C29A000  |                     | reserved   |               | f
 repl_replication_slot_172_168_0_96 |        | physical  |        |          | f         | t      |    1009542 |      |              | C/3C29A000  |                     | reserved   |               | f
(4 rows)

4 个物理复制槽分成泾渭分明的两组:

槽名 active restart_lsn wal_status 判定
repl_replication_slot_192_168_0_96 f 0/C37EFE8 extended 孤儿槽(192 旧网段)
repl_replication_slot_192_168_0_99 f 0/C350168 extended 孤儿槽(192 旧网段)
repl_replication_slot_172_168_0_99 t C/3C29A000 reserved 正常在用(172 现网段)
repl_replication_slot_172_168_0_96 t C/3C29A000 reserved 正常在用(172 现网段)

2. 排查过程

2.1 关键指标解读

(1) **active** 字段------判断槽有没有消费者

active 表示该槽当前是否正被 walsender 进程占用(即 active_pid IS NOT NULL)。192 的两个槽 active=f 且 active_pid 为空,说明没有任何复制连接在消费它们。

(2) **restart_lsn** 字段------判断槽是否"冻结"

  • 192 槽的 restart_lsn 停留在 0/...(逻辑段号 0)

  • 172 槽的 restart_lsn 已在 C/...(逻辑段号 12)

  • 1 个 WAL 逻辑段 = 256 个 16MB 段文件 ≈ 4GB

两者相差多个逻辑段,说明 192 这两个槽从极早的位置起就再没被消费过,完全对应"环境从 192 网段切到 172 网段后就没人认领了"。

(3) **wal_status** 字段------WAL 膨胀的直接铁证

PG 13+ 引入该字段,取值含义:

取值 含义
reserved WAL 保留量在 max_wal_size 范围内(正常)
**extended** 保留量已超出 **max_wal_size**,但因槽存在被迫继续保留
unreserved 不再保留 WAL,restart_lsn 之前的 WAL 可能已被删除
lost 槽已失效,需要重建

192 槽是 extended------这就是 **pg_wal** 被撑到 19G 的直接原因 ;而 172 槽是 reserved,属正常范围。

2.2 排除其他可能

排查项 结果 结论
是否有 walsender 连接 192 槽 active=f、active_pid 为空 无消费者,可安全删除
172 槽是否正常 active=t,restart_lsn 持续推进 现网段从机在用,不可动
项目打包目录是否有槽管理代码 product/Linux/x86_64/{bin,script} 搜索 repl_replication_slot / physical_replication_slot / pg_drop_replication_slot 均无命中 槽由底层业务代码运行时动态创建,不经 postgresql.conf 模板

2.3 复制槽基础原理(排查依据)

复制槽是什么?

复制槽(replication slot)是主库上的持久化对象,作用是"向主库承诺保留从该位置起的 WAL,直到消费者确认已接收"。

  • 存储位置:$PGDATA/pg_replslot/<slot_name>/state

  • 生命周期:显式创建(pg_create_physical_replication_slot() 或 pg_basebackup -S ... -C),只有显式 **pg_drop_replication_slot()** 才会删除

  • PG 没有任何"备库不连了就自动删槽"的机制

为什么槽会导致 WAL 堆积?

plaintext 复制代码
槽存在 → 主库必须保留 restart_lsn 之后的所有 WAL
       ↓
备库不再消费 → restart_lsn 不再推进
       ↓
WAL 无限累积 → pg_wal 持续膨胀 → 磁盘写满 → 主库挂掉

两个易混淆视图的区别:

pg_stat_replication pg_replication_slots
反映什么 当前活跃的 walsender 连接(会话级) 已创建的复制槽(持久化对象)
何时有记录 有人连上来做流复制就有 只有显式创建过槽才有
何时消失 备库断开即消失 只有 drop 才消失
无槽流复制 有记录 空

注:无槽流复制(未配 primary_slot_name)虽然也能跑通,但没有 WAL 保留保障 ,仅靠 wal_keep_size(默认 0)兜底,主库 checkpoint 后可能回收掉备库未接收的 WAL,导致备库报 requested WAL segment ... has already been removed 而断流。


3. 根因分析

3.1 事故链

plaintext 复制代码
1. 停车场环境配置 2 个从机
2. 底层业务代码在主库创建 2 个物理复制槽
   命名规则:repl_replication_slot_<从机IP下划线形式>
   → repl_replication_slot_192_168_0_96 / repl_replication_slot_192_168_0_99
3. 环境从 192 网段切换到 172 网段,新建 2 个槽
   → repl_replication_slot_172_168_0_96 / repl_replication_slot_172_168_0_99
4. 解除原主从关系时,未执行 pg_drop_replication_slot()
5. 192 两个槽失去消费者 → active=f,restart_lsn 永久停滞
6. PG 仍按承诺强制保留 WAL → wal_status=extended(超出 max_wal_size)
7. WAL 持续累积 → pg_wal 膨胀至 19G

3.2 缺陷定性

资源创建与释放不对称 :底层创建了持久化对象(复制槽),解除主从关系时没有对应的销毁动作。这是代码缺陷,而非运维误操作。

3.3 三个放大隐患

隐患 说明 后果
槽名内嵌 IP 命名规则 repl_replication_slot_<IP> 把网络地址编进了对象名 换网段/改 IP 后旧槽名无法被自动识别清理,必然残留
备库侧 **primary_slot_name** 未清理 若只删了主库的槽、没清备库 postgresql.auto.conf 中的 primary_slot_name 该实例下次作为备库启动会报 replication slot "xxx" does not exist,挂不上复制
无监控告警 孤儿槽从产生到磁盘被撑到 19G 期间零告警 只能被动发现,等到磁盘告警才暴露

4. 解决方案

4.1 删除前安全检查(必做)

sql 复制代码
-- 1) 确认目标槽无消费者
SELECT slot_name, slot_type, active, active_pid, restart_lsn,
       pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS retained_wal
FROM pg_replication_slots
WHERE slot_name IN ('repl_replication_slot_192_168_0_96',
                    'repl_replication_slot_192_168_0_99');

-- 2) 对照当前实际连接
SELECT application_name, client_addr, state, sync_state FROM pg_stat_replication;

判定标准 :active = f 且 active_pid 为空,pg_stat_replication 中无对应连接 → 可安全删除。

若槽为 active = t,pg_drop_replication_slot() 会直接报错 replication slot "xxx" is active for PID nnn,不会误删。

4.2 执行删除

sql 复制代码
SELECT pg_drop_replication_slot('repl_replication_slot_192_168_0_96');
SELECT pg_drop_replication_slot('repl_replication_slot_192_168_0_99');

4.3 验证结果

删除后仅剩 2 个在用的槽:

sql 复制代码
postgres=# select * from pg_replication_slots;
             slot_name              | plugin | slot_type | datoid | database | temporary | active | active_pid | restart_lsn | wal_status | two_phase
------------------------------------+--------+-----------+--------+----------+-----------+--------+------------+-------------+------------+-----------
 repl_replication_slot_172_168_0_99 |        | physical  |        |          | f         | t      |    1009698 | C/3C3C8000  | reserved   | f
 repl_replication_slot_172_168_0_96 |        | physical  |        |          | f         | t      |    1009542 | C/3C3C8000  | reserved   | f
(2 rows)

注意:172 槽的 restart_lsn 由 C/3C29A000 推进到 C/3C3C8000,确认活跃槽工作正常。

**pg_wal** 空间快速回落:

bash 复制代码
[root@openEuler ~]# du -sm /postgresql/pgdata/pg_wal/
17265   → 15873 → 15457 → 6529 → 6465 → 6177 → 3921 → 3217 → 2673 → 1377 → 17
# 数十秒内由 17GB 降至 17MB

[root@openEuler ~]# du -sh /postgresql/pgdata/pg_wal/
17M     /postgresql/pgdata/pg_wal/

[root@openEuler ~]# du -sh /postgresql/pgdata/
1.7G    /postgresql/pgdata/

回收为何这么快?

PG 在 pg_drop_replication_slot() 之后会主动触发一次 checkpoint (CHECKPOINT_IMMEDIATE | FORCE | WAIT),因此 WAL 回收立即启动,无需等待常规 checkpoint_timeout 周期。

执行 du 期间出现 du: cannot access '.../0000000100000008000000CA': No such file or directory 属正常现象------是 du 扫描目录时 WAL 文件正被 unlink。

4.4 若需主动加速回收

sql 复制代码
CHECKPOINT;                                    -- 手动触发回收
SELECT count(*) FROM pg_ls_waldir();           -- 观察 WAL 文件数量

若删槽 + checkpoint 后 pg_wal 仍不下降,需排查另一条链路:

sql 复制代码
SHOW archive_mode;                             -- on 且归档积压时 WAL 也不会删
SELECT * FROM pg_stat_archiver;

5. 经验总结

5.1 排查思路(可复用)

  1. **pg_wal** 异常膨胀,先查复制槽 :SELECT * FROM pg_replication_slots;------槽是 WAL 堆积的头号嫌疑

  2. 看 **active** 分组 :active=f 的是孤儿槽,active=t 的是在用槽

  3. 看 **wal_status** 定性 :extended = 已被迫超量保留 WAL,是膨胀的直接证据

  4. 看 **restart_lsn** 是否推进:停滞说明消费者已消失

  5. 用 **pg_stat_replication** 交叉验证:为空即无 walsender 接入,可放心清理

  6. 删槽前必须确认无消费者,删除后 PG 会自动 checkpoint 回收

5.2 常用运维命令

sql 复制代码
-- 查看所有槽及关键状态
SELECT slot_name, slot_type, active, active_pid, restart_lsn, wal_status,
       pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS retained_wal
FROM pg_replication_slots ORDER BY active;

-- 查看当前活跃复制连接
SELECT application_name, client_addr, state, sync_state, sent_lsn, replay_lsn
FROM pg_stat_replication;

-- 查看 WAL 总量与文件数
SELECT pg_size_pretty(sum(size)) FROM pg_ls_waldir();
SELECT count(*) FROM pg_ls_waldir();

-- 幂等清理(按前缀批量删,避免单个槽不存在导致流程中断)
SELECT pg_drop_replication_slot(slot_name)
FROM pg_replication_slots
WHERE slot_name LIKE 'repl_replication_slot_%' AND active = false;

-- 触发回收
CHECKPOINT;
bash 复制代码
# 查看 pg_wal 占用
du -sh $PGDATA/pg_wal

5.3 预防措施

层次 措施 说明
代码修复(根本) 解除主从关系时同步 pg_drop_replication_slot() 资源创建/释放必须对称,建议提问题单推动底层修复
命名优化 槽名不要内嵌 IP,改用稳定标识(从机序号/UUID) 换网段/改 IP 后不会产生"无法识别的孤儿槽"
清理幂等 清理逻辑按条件删(WHERE slot_name LIKE ... AND active = false) pg_drop_replication_slot() 对不存在的槽会报错,需容错
同步清理备库配置 解除主从时一并清理备库 primary_slot_name 否则该实例下次作备库会报 replication slot ... does not exist
监控告警 ① 孤儿槽:active=f 且 restart_lsn 停滞超 N 小时 ② wal_status = extended 即告警 ③ pg_wal 目录容量 本次从产生到 19G 全程零告警
防御性配置 评估设置 max_slot_wal_keep_size(默认 -1 不限制) 给单个槽的 WAL 保留量设上限,防止单个孤儿槽撑爆磁盘
流程固化 解除主从标准清单:删槽 → 清备库 primary_slot_name → 清 standby.signal → CHECKPOINT 回收 避免遗漏

5.4 关键结论

复制槽是"向主库承诺保留 WAL"的持久化对象,PG 不会自动清理。 只要槽失去消费者又不删除,它就会持续拽住 WAL 不放,wal_status 会变成 extended 并最终撑满磁盘。 因此:凡是有槽的实例,必须建立孤儿槽巡检机制;凡是有槽创建逻辑的代码,必须有对应的删除逻辑。

相关推荐
haerapi11 小时前
把 PostgreSQL 复制巡检做成可审计闭环:确定性采集、阈值判定与受控模型归纳
数据库·postgresql
如果'\'真能转义说19 小时前
Docker Desktop | 本地化挂载 Postgresql
docker·postgresql·容器
IvorySQL2 天前
PostgreSQL 日报 |RLS 机密性绕过漏洞(9 月 27 日)
数据库·postgresql
IvorySQL2 天前
去 IOE 的最后一公里:IvorySQL 5.4 × RISC-V 实测
数据库·人工智能·ai·postgresql·risc-v
jeffwang3 天前
不装分词插件,用 PostgreSQL 做中文商品搜索:bigram + 向量 + RRF,以及「单字搜不到」这个坑
搜索引擎·postgresql·go
柒和远方3 天前
NestJS + TypeORM 学习笔记:语法和类型不绕了
postgresql·orm·nestjs
Y003 天前
PostgreSQL 的锁:为什么你的 ALTER TABLE 会卡住,以及怎么查
数据库·postgresql
geovindu9 天前
sql: Transaction & Concurrency Patterns using postgresql 18
postgresql·数据库开发·数据库架构