PostgreSQL WAL 排障实战(第 11 篇):主从延迟为零,闲置逻辑槽为什么仍能撑爆 pg_wal
物理副本监控显示延迟为零读流量也正常主库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_namedemo_idle_slot;消费者从未读取和确认数据库继续生成 WALcurrent_lsn - restart_lsn会增长。实际pg_wal目录大小受 checkpoint、段回收、归档和其他槽共同影响所以本实验用位点差证明保留责任不承诺文件尺寸与差值逐字节相等。测试结束先确认名称只属于本实验SELECTslot_name,slot_type,database,activeFROMpg_replication_slotsWHEREslot_namedemo_idle_slot;SELECTpg_drop_replication_slot(demo_idle_slot);DROPTABLEIFEXISTSwal_demo;该删除只因槽由本实验创建且没有业务消费者。生产槽绝不能照抄位点丢失后CDC 可能无法连续恢复必须重新快照并做全量对账。max_wal_size为什么挡不住max_wal_size是 checkpoint 频率相关的软阈值不是pg_wal硬容量上限。以下因素都可能要求保留更多 WALreplication slot 的restart_lsnWAL 归档持续失败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_REMOVEDwal_status lost、invalidation_reason wal_removed所需 WAL 已丢不能靠重新连接续传RS_INVAL_IDLE_TIMEOUTinvalidation_reason idle_timeoutcheckpoint 已使超时闲置槽失效本文不使用 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/restartrestart_lsn决定仍可能保留的最老 WAL。max_wal_size不是硬上限max_slot_wal_keep_size通过允许槽失效保护磁盘。每个槽都必须有负责人、容量上限、告警和重建对账方案。实验清理核对测试结束后应返回 0 行SELECTslot_nameFROMpg_replication_slotsWHEREslot_namedemo_idle_slot;若仍存在不要反复执行删除先确认是否有其他人复用了同名槽以及active_pid指向哪个会话。官方资料PostgreSQL 18Replication SlotsPostgreSQL 18pg_replication_slotsPostgreSQL 18Replication ConfigurationPostgreSQL 18WAL ConfigurationPostgreSQL 18System Administration FunctionsPostgreSQL 18pg_stat_archiverPostgreSQL 18.6 源码xlog.c / KeepLogSeg()PostgreSQL 18.6 源码slot.c / 槽失效判断PostgreSQL 18.6 源码标签 REL_18_6