文章目录
- [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 排查思路(可复用)
-
**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 常用运维命令
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并最终撑满磁盘。 因此:凡是有槽的实例,必须建立孤儿槽巡检机制;凡是有槽创建逻辑的代码,必须有对应的删除逻辑。