文章目录
- PG 孤儿物理复制槽残留导致 pg_wal 膨胀 19G 故障分析
- 1. 故障现象
- 1.1 磁盘空间异常
- 1.2 复制槽查询发现异常
- 2. 排查过程
- 2.1 关键指标解读
- 2.2 排除其他可能
- 2.3 复制槽基础原理(排查依据)
- 3. 根因分析
- 3.1 事故链
- 3.2 缺陷定性
- 3.3 三个放大隐患
- 4. 解决方案
- 4.1 删除前安全检查(必做)
- 4.2 执行删除
- 4.3 验证结果
- 4.4 若需主动加速回收
- 5. 经验总结
- 5.1 排查思路(可复用)
- 5.2 常用运维命令
- 5.3 预防措施
- 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:
[root@openEuler ~]# du -sh /postgresql/pgdata/pg_wal19G /postgresql/pgdata/pg_wal[root@openEuler ~]# du -sh /postgresql/pgdata/1.7G /postgresql/pgdata/# 删除前 pgdata 总量被 pg_wal 占据1.2 复制槽查询发现异常
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(4rows)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 堆积?
槽存在 → 主库必须保留 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 事故链
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 膨胀至 19G3.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 删除前安全检查(必做)
-- 1) 确认目标槽无消费者SELECTslot_name,slot_type,active,active_pid,restart_lsn,pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(),restart_lsn))ASretained_walFROMpg_replication_slotsWHEREslot_nameIN('repl_replication_slot_192_168_0_96','repl_replication_slot_192_168_0_99');-- 2) 对照当前实际连接SELECTapplication_name,client_addr,state,sync_stateFROMpg_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 执行删除
SELECTpg_drop_replication_slot('repl_replication_slot_192_168_0_96');SELECTpg_drop_replication_slot('repl_replication_slot_192_168_0_99');4.3 验证结果
删除后仅剩 2 个在用的槽:
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(2rows)注意:172 槽的
restart_lsn由C/3C29A000推进到C/3C3C8000,确认活跃槽工作正常。
**pg_wal**空间快速回落:
[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 若需主动加速回收
CHECKPOINT;-- 手动触发回收SELECTcount(*)FROMpg_ls_waldir();-- 观察 WAL 文件数量若删槽 + checkpoint 后pg_wal仍不下降,需排查另一条链路:
SHOWarchive_mode;-- on 且归档积压时 WAL 也不会删SELECT*FROMpg_stat_archiver;5. 经验总结
5.1 排查思路(可复用)
**pg_wal**异常膨胀,先查复制槽:SELECT * FROM pg_replication_slots;——槽是 WAL 堆积的头号嫌疑看
**active**分组:active=f的是孤儿槽,active=t的是在用槽看
**wal_status**定性:extended= 已被迫超量保留 WAL,是膨胀的直接证据看
**restart_lsn**是否推进:停滞说明消费者已消失用
**pg_stat_replication**交叉验证:为空即无 walsender 接入,可放心清理删槽前必须确认无消费者,删除后 PG 会自动 checkpoint 回收
5.2 常用运维命令
-- 查看所有槽及关键状态SELECTslot_name,slot_type,active,active_pid,restart_lsn,wal_status,pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(),restart_lsn))ASretained_walFROMpg_replication_slotsORDERBYactive;-- 查看当前活跃复制连接SELECTapplication_name,client_addr,state,sync_state,sent_lsn,replay_lsnFROMpg_stat_replication;-- 查看 WAL 总量与文件数SELECTpg_size_pretty(sum(size))FROMpg_ls_waldir();SELECTcount(*)FROMpg_ls_waldir();-- 幂等清理(按前缀批量删,避免单个槽不存在导致流程中断)SELECTpg_drop_replication_slot(slot_name)FROMpg_replication_slotsWHEREslot_nameLIKE'repl_replication_slot_%'ANDactive=false;-- 触发回收CHECKPOINT;# 查看 pg_wal 占用du-sh$PGDATA/pg_wal5.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并最终撑满磁盘。 因此:凡是有槽的实例,必须建立孤儿槽巡检机制;凡是有槽创建逻辑的代码,必须有对应的删除逻辑。