物理副本监控显示延迟为零,读流量也正常,主库pg_wal却每天增长数百 GB。直接答案是:物理副本追平只覆盖一条物理复制链;三周前停用的 CDC 逻辑槽虽然没有连接,仍可能用旧restart_lsn抬高 WAL 回收边界。
PostgreSQL 没有一个“复制延迟”数字能覆盖所有消费者。物理副本追平,只能证明那条复制链;逻辑槽是否确认消费,要看自己的
confirmed_flush_lsn与restart_lsn。
一次看全物理链与所有槽
物理/流复制连接:
SELECTapplication_name,client_addr,state,sync_state,sent_lsn,write_lsn,flush_lsn,replay_lsn,pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(),replay_lsn))ASreplay_gapFROMpg_stat_replication;全部复制槽:
SELECTslot_name,slot_type,database,active,active_pid,restart_lsn,confirmed_flush_lsn,inactive_since,wal_status,pg_size_pretty(safe_wal_size)ASsafe_wal_size,invalidation_reason,pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(),restart_lsn))ASretained_gapFROMpg_replication_slotsORDERBYrestart_lsn NULLSLAST;这些位点回答不同问题:
sent_lsn:主库 WAL sender 已发送到哪里;write_lsn:接收端写入操作系统的位置;flush_lsn:接收端确认持久化的位置;replay_lsn:物理副本已经重放的位置;confirmed_flush_lsn:逻辑消费者已确认接收的位置;restart_lsn:该槽仍可能需要的最老 WAL,因此是保留边界。
active = false只表示当前没有流式消费者,不表示槽不保留 WAL。replay_lag = 0也可能在副本空闲、统计语义或采样时点下被误读,应同时看 LSN 字节差和时间序列。
再把逻辑位点与实际 WAL 目录、归档状态交叉验证:
SELECTcount(*)ASwal_files,pg_size_pretty(sum(size))ASwal_dir_size,min(modification)ASoldest_file_time,max(modification)ASnewest_file_timeFROMpg_ls_waldir();SELECTarchived_count,failed_count,last_archived_wal,last_archived_time,last_failed_wal,last_failed_timeFROMpg_stat_archiver;pg_ls_waldir()读取服务器目录清单,需要受控的高权限,不能向普通业务账号开放;它反映当前目录实际占用,却不能说明每个文件由哪个槽保留。pg_stat_archiver能发现归档失败这条替代假设,但累计计数可能跨越很长时间,判断时要看最近失败时间和监控增量。
LSN retained gap 上升 + pg_wal 同步增长 → 支持 slot 保留假设 slot gap 稳定 + last_failed_time 持续刷新 → 优先排查归档失败 两者同时异常 → 不能只处理其中一条容量链测试槽复现 WAL 保留
仅在隔离测试实例执行。前提是wal_level = logical、存在足够槽位,并允许使用 PostgreSQL 自带test_decoding输出插件。
SHOWwal_level;SHOWmax_replication_slots;SELECT*FROMpg_create_logical_replication_slot('demo_idle_slot','test_decoding');CREATETABLEwal_demo(idbigint,payloadtext);INSERTINTOwal_demoSELECTg,repeat(md5(g::text),32)FROMgenerate_series(1,200000)ASg;SELECTslot_name,active,restart_lsn,confirmed_flush_lsn,pg_wal_lsn_diff(pg_current_wal_lsn(),restart_lsn)ASretained_bytes,wal_status,safe_wal_sizeFROMpg_replication_slotsWHEREslot_name='demo_idle_slot';消费者从未读取和确认,数据库继续生成 WAL,current_lsn - restart_lsn会增长。实际pg_wal目录大小受 checkpoint、段回收、归档和其他槽共同影响,所以本实验用位点差证明保留责任,不承诺文件尺寸与差值逐字节相等。
测试结束,先确认名称只属于本实验:
SELECTslot_name,slot_type,database,activeFROMpg_replication_slotsWHEREslot_name='demo_idle_slot';SELECTpg_drop_replication_slot('demo_idle_slot');DROPTABLEIFEXISTSwal_demo;该删除只因槽由本实验创建且没有业务消费者。生产槽绝不能照抄:位点丢失后,CDC 可能无法连续恢复,必须重新快照并做全量对账。
max_wal_size为什么挡不住
max_wal_size是 checkpoint 频率相关的软阈值,不是pg_wal硬容量上限。以下因素都可能要求保留更多 WAL:
- replication slot 的
restart_lsn; - WAL 归档持续失败;
wal_keep_size保留下限;- checkpoint 周期和高峰生成量;
- 备份或恢复链上的其他需要。
PostgreSQL 18 的max_slot_wal_keep_size可以限制槽在 checkpoint 时允许保留的 WAL。默认-1表示无限制。超过有限上限后,槽所需 WAL 可能被移除,最终wal_status = lost,消费者不能继续。
这不是无损容量保护,而是明确取舍:保护主库磁盘,必要时牺牲消费者连续性。
safe_wal_size表示在槽有丢失危险前还能写多少 WAL;无限制或槽已 lost 时为 NULL。应按其下降速度告警,而不是等磁盘 95% 才处理。
源码证明:旧restart_lsn怎样挡住 WAL 回收
PostgreSQL 18.6 的保留链跨两个模块,不是pg_replication_slots视图自己“保存文件”:
各 slot.restart_lsn → ReplicationSlotsComputeRequiredLSN() → XLogCtl.replicationSlotMinLSN → xlog.c / KeepLogSeg() → checkpoint 计算最早可删除的 WAL segmentsrc/backend/access/transam/xlog.c的KeepLogSeg()先通过XLogGetReplicationSlotMinimumLSN()取得所有槽要求的最老 LSN,并据此向前移动保留边界;配置了非负max_slot_wal_keep_size时,它又把槽允许保留的距离限制在该值内。随后它还会合并 WAL summarization 与wal_keep_size的要求,所以“某个槽的 retained gap”与最终目录尺寸不是一一相等关系。
当 checkpoint 推进到需要移除槽所需 WAL 时,src/backend/replication/slot.c的DetermineSlotInvalidationCause()会比较restart_lsn与最老可用 LSN:前者更旧时返回RS_INVAL_WAL_REMOVED。InvalidateObsoleteReplicationSlots()标记槽失效并重新计算资源下限,视图最终表现为invalidation_reason = 'wal_removed',槽可能进入lost。
| 源码状态/分支 | SQL 证据 | 运维含义 |
|---|---|---|
replicationSlotMinLSN长期不前进 | 最老restart_lsn与 retained gap 持续增长 | 某个槽正在抬高回收边界 |
max_slot_wal_keep_size >= 0限制保留距离 | safe_wal_size下降,wal_status由 reserved/extended 变化 | 主库容量保护正在逼近消费者连续性代价 |
RS_INVAL_WAL_REMOVED | wal_status = 'lost'、invalidation_reason = 'wal_removed' | 所需 WAL 已丢,不能靠重新连接续传 |
RS_INVAL_IDLE_TIMEOUT | invalidation_reason = 'idle_timeout' | checkpoint 已使超时闲置槽失效 |
本文不使用 Java 客户端作为核心证据,因为 WAL 文件回收和槽失效由服务端 checkpoint/slot 状态机决定;只读系统视图、固定版本源码和隔离 SQL 实验比任意 CDC SDK 更直接。
复制槽不只可能保留 WAL,逻辑槽的catalog_xmin还可能影响系统目录版本回收;这条清理链及其与只读长事务的区别见第 8 篇:没有锁等待,只读事务为什么仍能拖胖整库。
闲置槽超时也会让消费者失效
idle_replication_slot_timeout可使长期 inactive 的槽在 checkpoint 时失效。超时到点不一定立即发生,因为失效在 checkpoint 处理;强制 checkpoint 只为立刻失效槽会引入 I/O,不应当作日常操作。
槽因超时失效后,invalidation_reason = idle_timeout。它仍需要消费者重建方案、负责人和告警,不能把超时当自动垃圾桶。
三类“正常”指标为何会误导
| 正常指标 | 它实际证明 | 它漏掉什么 |
|---|---|---|
物理replay_gap = 0 | 某条物理副本已重放到当前附近 | 逻辑槽、归档和其他副本 |
slotactive = true | 有进程正在使用槽 | 消费速度是否追上 WAL 生成 |
| CDC 作业 RUNNING | 进程/任务状态正常 | 是否确认位点、下游是否落库正确 |
max_wal_size未改 | checkpoint 目标没变 | 槽可无限保留 WAL |
技术位点还必须与业务结果闭环:目标端最大业务版本、端到端延迟、重复/丢失和重建能力。confirmed_flush_lsn前进不等于下游业务表一定正确。
生产处置:容量告警时先保护证据
只读确认
- 记录文件系统剩余量、WAL 每分钟增长与预计耗尽时间。
- 列出所有槽的负责人、active、restart/confirmed、status 和 retained gap。
- 检查物理复制、归档状态、备份与当前 WAL 生成速率。
- 向 CDC 团队确认最后成功业务位点及从哪里可重新快照。
最小止血
先暂停非必要高 WAL 批作业、扩容或释放已确认无关的文件,争取决策时间。不要手工删除pg_wal文件;这会破坏恢复与复制,可能使实例无法启动。
根因修复
- 每个 slot 绑定负责人、用途、消费 SLA、容量预算和失效策略;
- 对停用 CDC 走“停止生产 → 记录最终位点 → 下游验收 → 删除槽”流程;
- 根据磁盘和重建 RTO 评估
max_slot_wal_keep_size; - 只有具备重建能力的槽才启用合理 idle timeout;
- 同时监控 retained bytes、safe WAL、磁盘耗尽时间和业务端到端延迟。
删除生产槽的护栏
删除前必须有:槽的精确名称与数据库、消费者书面确认、最后业务位点、可用源端快照/备份、重建步骤和对账口径。先处理一个已确认废弃槽,观察磁盘、checkpoint、归档和其他消费者。
若消费者身份不明、仍可能恢复、快照能力未验证或业务对账失败,立即停止。删除槽不可通过重新创建同名槽恢复原 LSN;回滚本质是重新建立一致性,而不是撤销 SQL。
证据边界
| 证据 | 能证明 | 不能证明 |
|---|---|---|
restart_lsn很旧 | 槽保留较老 WAL | 消费者为何停滞 |
confirmed_flush_lsn前进 | 消费者确认了逻辑流 | 下游业务写入正确 |
wal_status = lost | 槽已不可继续使用所需 WAL | 自动完成消费者重建 |
| 删除槽后磁盘下降 | 该槽是重要保留因素 | 所有 WAL 风险已消失 |
| 物理副本追平 | 该物理链路正常 | 所有逻辑消费者正常 |
面试表达主线
物理复制看 sent/write/flush/replay,逻辑槽看 confirmed/restart;restart_lsn决定仍可能保留的最老 WAL。max_wal_size不是硬上限,max_slot_wal_keep_size通过允许槽失效保护磁盘。每个槽都必须有负责人、容量上限、告警和重建对账方案。
实验清理核对
测试结束后应返回 0 行:
SELECTslot_nameFROMpg_replication_slotsWHEREslot_name='demo_idle_slot';若仍存在,不要反复执行删除;先确认是否有其他人复用了同名槽,以及active_pid指向哪个会话。
官方资料
- PostgreSQL 18:Replication Slots
- PostgreSQL 18:pg_replication_slots
- PostgreSQL 18:Replication Configuration
- PostgreSQL 18:WAL Configuration
- PostgreSQL 18:System Administration Functions
- PostgreSQL 18:pg_stat_archiver
- PostgreSQL 18.6 源码:xlog.c / KeepLogSeg()
- PostgreSQL 18.6 源码:slot.c / 槽失效判断
- PostgreSQL 18.6 源码标签 REL_18_6