用户登录

技术文章

外部链接将在新标签页打开。

Postgres 逻辑复制槽撑爆 WAL:定位、回收与阈值

一句话回答

逻辑复制槽撑爆 WAL 的根因是不再推进的 restart_lsn,用 pg_replication_slots 算出保留字节数即可定位;回收按 drop、advance、max_slot_wal_keep_size 三选一,并给 20 GiB 警告、50 GiB 严重的监控阈值。

核心要点 TL;DR
  • 复制槽保留 WAL 的量等于写入速率乘以位点停滞时长,与槽大小无关。
  • 先查 pg_replication_slots 的 retained 字节数,再决定 drop 还是 advance。
  • inactive 超过 15 分钟就该告警,20 GiB 警告、50 GiB 严重是可用起点。

结论先行:复制槽是 Postgres 给消费者记的一张书签,书签之后的预写日志(WAL,Write-Ahead Logging)一律不许删。撑爆磁盘的从来不是槽本身,是不推进的 restart_lsn。

先讲一句大白话:Postgres 每写一笔数据,先往 WAL 里追加一条记录,再异步刷到数据文件。正常情况这些 WAL 归档完、备库收完就能删。一旦有个复制槽(Replication Slot)说自己「只读到 3F/8A000000」,那么从那个位置往后的所有 WAL 都得留着,哪怕消费者早就跑路了。磁盘就是这么被撑满的。

为什么一个没人消费的逻辑复制槽能撑爆 WAL?#

逻辑复制槽(Logical Replication Slot)和物理复制槽共用同一套保留机制,区别只是前者按逻辑解码插件(pgoutput、wal2json、decoderbufs)输出行级变更,后者输出原始 WAL 块。保留判定只看一个值:pg_replication_slots.restart_lsn。所有槽里最旧的那个 restart_lsn,就是 WAL 能够回收的低水位线(Low Watermark)。

算一笔账。业务写入 50 MB/s 属于中等偏上的 OLTP 负载,一小时产生约 176 GiB WAL。一个做变更数据捕获(CDC,Change Data Capture)的 Debezium 连接器因为证书过期挂了 6 小时没人管,磁盘上就多出 1 TiB 的 pg_wal。这个量级用 du -sh 看一眼就明白,但很多人是在监控告警「磁盘使用率 95%」时才第一次打开这个目录。

对比表:四类常见的 WAL 膨胀模式与识别特征

模式典型成因识别特征处置优先级
僵尸槽临时排查建的槽没删,或 CDC 任务下线未清理active = false,restart_lsn 长期不动最高,直接 drop
消费者假活客户端连着但不再 ack,位点停在几个月前active = true 但 confirmed_flush_lsn 不变高,需停消费者后处置
订阅过滤过宽发布端只订了两张表,槽却按整库解码保留量随全库写入线性增长中,改发布定义
长事务叠加槽正常,但库里有跑了一天的长事务压着 catalog_xmincatalog_xmin 远小于当前 xid中,先杀长事务

第三、四种模式容易被误判成「槽的问题」。判断方法很简单:如果 retained_wal 涨得比业务写入速率还快,多半不是槽;如果严格等于「写入速率 × 停滞时长」,那就是槽。

如何定位是哪把刀在割?15 分钟排查路径#

我不喜欢一上来就翻日志。pg_replication_slots 这一个视图基本能定性,剩下的只是确认责任方。

编号步骤:从告警到定位

  1. 看 pg_wal 目录总量:du -sh $PGDATA/pg_wal,确认是不是 WAL 撑的而不是数据文件。
  2. 查槽的保留字节数,按从大到小排序,取第一名。
  3. 看这个槽的 active、active_pid、wal_status。
  4. 在客户端侧找 active_pid 对应的应用,确认它的消费位点。
  5. 对照业务写入速率,估算「再撑多久会写满磁盘」,决定是止血还是慢慢处理。

可执行脚本:诊断查询(PostgreSQL 13 及以上,psql 14/15/16/17 通用)

-- 示例仅用于逻辑验证,生产环境需补充日志、监控与降级
-- 环境:PostgreSQL 13+,需要 pg_monitor 或超级用户权限
SELECT
  slot_name,
  plugin,
  slot_type,
  active,
  active_pid,
  wal_status,
  pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn))       AS retained_wal,
  pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), confirmed_flush_lsn)) AS consumer_lag,
  restart_lsn,
  confirmed_flush_lsn
FROM pg_replication_slots
ORDER BY pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn) DESC NULLS LAST;

pg_wal_lsn_diff(lsn_a, lsn_b) 返回两个 LSN 之间的字节数,pg_current_wal_lsn() 是当前写入位置,两者相减就是「这个槽压住了多少 WAL」。物理槽的 confirmed_flush_lsn 为 NULL,相减结果是 NULL,所以排序要加 NULLS LAST,否则结果会被 NULL 挤到最前或最后,看版本行为。

预期输出片段(示例数据):

 slot_name  | plugin   | slot_type | active | active_pid | wal_status | retained_wal | consumer_lag | restart_lsn | confirmed_flush_lsn
------------+----------+-----------+--------+------------+------------+--------------+--------------+-------------+---------------------
 cdc_orders | pgoutput | logical   | f      |            | extended   | 62 GB        | 61 GB        | 3F/8A000000 | 3F/8A120000
 cdc_users  | pgoutput | logical   | t      |      19422 | reserved   | 1.2 GB       | 1.1 GB       | 4A/1C000000 | 4A/2D400000

第一行就是元凶:active = f(消费者不在),active_pid 为空,却压着 62 GB。第二行是健康的,位点跟得很紧。

wal_status 这一列(PostgreSQL 13 起)值得单独说,它直接告诉你危险到什么程度:reserved 表示还在 max_slot_wal_keep_size 范围内;extended 表示已越过上限但文件暂时还在;unreserved 表示随时可能被回收;lost 表示相关 WAL 已被删除,槽实际已失效,只能重建。各状态的确切边界以官方视图文档为准。

顺序不要搞反。我见过有团队看到 lost 就直接重建订阅,结果下游重复消费了三天的订单,对账对了两个通宵。先确认下游消费到了哪个业务时间点,再动手。

排查脚本如果跑在容器或独立跳板机里,动手前先验证出网和链路,避免「以为发了告警其实请求都没出去」。GET 示例接口这类轻量端点适合做探针,参数 apikey 是调用凭证,ip 是要查询的地址,只看返回的 http_code 是否为 200 即可,不解析业务字段。

# 环境:bash 5.x + curl 8.x,在运行巡检脚本的主机上执行
# 示例仅用于逻辑验证,生产环境需补充日志、监控与降级
curl -sS --max-time 3 --retry 2 --retry-delay 1 \
  -o /dev/null \
  -w "http=%{http_code} dns=%{time_namelookup}s total=%{time_total}s\n" \
  "https://www.api.plus/api/get?apikey=YOUR_KEY&ip=8.8.8.8" || echo "probe failed"

--max-time 3 是硬超时,防止探针自己把巡检卡死;--retry 2 只对连接类错误和 5xx 生效,不会对 4xx 重试,这个边界要清楚,否则 401 会被当成网络抖动重试三遍。预期输出形如 http=200 dns=0.004s total=0.118s。拿不到 200 就别急着归因到数据库。

回收怎么下手:drop、advance 还是让 PG 自己丢?#

三种手段,代价完全不同。选错的代价是下游数据缺口。

对比表:三种回收方式的取舍

方式适用场景数据影响可逆性
pg_drop_replication_slot()槽已废弃,下游可以重做全量位点之后的变更不再保证可读不可逆,需重建槽 + 快照
pg_replication_slot_advance()下游只需要从某个新位点续接中间变更被跳过不可逆,但可精确控制断点
max_slot_wal_keep_size防线,不是手段超限后 PG 主动让槽失效槽失效后仍需重建

max_slot_wal_keep_size 是 PostgreSQL 13 引入的参数,默认 -1 表示不设上限 —— 这个默认值在生产环境基本等于「让磁盘替我兜底」。它不是回收手段,是保险丝:超限后 Postgres 会把最老的槽标记为失效并释放 WAL,日志里会出现超出上限之类的报错。有保险丝比直接炸盘好,但保险丝烧断的那一刻下游就断了。

可执行脚本:安全回收一个逻辑槽(bash 4.4+ / psql 14+)

#!/usr/bin/env bash
# 环境:PostgreSQL 14+ / psql 14+ / bash 4.4+,在数据库主机或堡垒机执行
# 用法:./reclaim_slot.sh cdc_orders drop
# 示例仅用于逻辑验证,生产环境需补充日志、监控与降级
set -euo pipefail

SLOT="${1:?用法: reclaim_slot.sh <slot_name> <drop|advance>}"
ACTION="${2:-drop}"
DSN="host=10.0.12.31 port=5432 dbname=postgres user=dba connect_timeout=3"

# 1. 存在性检查:槽不存在就直接退出,避免把拼写错误当成功
ACTIVE=$(psql "$DSN" -At -v slot="$SLOT" \
  -c "SELECT active FROM pg_replication_slots WHERE slot_name = :'slot';")
if [ -z "$ACTIVE" ]; then
  echo "slot $SLOT 不存在"; exit 1
fi

# 2. 活跃保护:active=t 时绝不继续,必须先停消费者
if [ "$ACTIVE" = "t" ]; then
  echo "slot $SLOT 仍被消费者占用,请先停止订阅端"; exit 2
fi

# 3. 执行,ON_ERROR_STOP 保证失败立即中断
if [ "$ACTION" = "advance" ]; then
  psql "$DSN" -v ON_ERROR_STOP=1 -v slot="$SLOT" -At \
    -c "SELECT pg_replication_slot_advance(:'slot', pg_current_wal_lsn());"
else
  psql "$DSN" -v ON_ERROR_STOP=1 -v slot="$SLOT" -At \
    -c "SELECT pg_drop_replication_slot(:'slot');"
fi

# 4. 复核:pg_wal 目录应当在一个 checkpoint 周期内回落
psql "$DSN" -At -c "SELECT pg_size_pretty(sum(size)) FROM pg_ls_waldir();"

几个细节。槽名不要用字符串拼接进 SQL,用 psql 的 :'slot' 变量插值,能挡掉大部分注入和引号问题。第 4 步的 pg_ls_waldir() 需要超级用户或 pg_monitor 角色。删完不会立刻回落,要等下一个 checkpoint 把旧段标记为可回收,典型等待时间由 max_wal_size 和 checkpoint_timeout 决定,默认配置下可能要几分钟,别删完看没变化就以为失败又删一遍。

硬提醒:执行 advance 之前,先把当前 confirmed_flush_lsn 记下来。一旦跳过了变更,下游只能靠这个 LSN 去比对源库补数据,没记就只能全量重来。

监控与告警清单#

事后处理一次就够痛了,把它变成指标才不会再痛第二次。核心指标只有三个:保留字节数、槽的活跃状态、保留量占 max_slot_wal_keep_size 的比例。

监控表:关键指标、建议阈值与告警动作

指标采集方式建议阈值告警动作
单槽保留 WALpg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)20 GiB 警告 / 50 GiB 严重严重级直接通知值班 + 工单
槽 inactive 时长active = false 持续计时(PG 17 起视图有 inactive_since)15 分钟警告 / 60 分钟严重通知槽的归属方确认
保留量 / 上限比retained ÷ max_slot_wal_keep_size70% 警告 / 90% 严重严重时准备手动 drop
pg_wal 目录占用node_exporter 采集磁盘或 pg_ls_waldir() 求和可用空间 < 15%立即止血,不受工作时间限制
consumer lag 增速两次采样差值 ÷ 间隔连续 3 个周期 > 0 且递增下游消费能力不足,扩消费者

Prometheus 侧用 postgres_exporter 就够,它的 pg_replication_slots collector 会输出 pg_replication_slots_pg_wal_lsn_diff 和 pg_replication_slots_active,带 slot_name 标签。

配置片段:Prometheus 告警规则(Prometheus 2.45+ / postgres_exporter 0.15+)

# 示例仅用于逻辑验证,生产环境需补充日志、监控与降级
groups:
  - name: pg_logical_slot
    rules:
      - alert: PgSlotRetainedWalHigh
        expr: pg_replication_slots_pg_wal_lsn_diff / 1073741824 > 20
        for: 10m
        labels:
          severity: warning
        annotations:
          summary: "槽 {{ $labels.slot_name }} 保留 WAL {{ printf \"%.1f\" $value }} GiB"

      - alert: PgSlotInactiveTooLong
        expr: pg_replication_slots_active == 0
        for: 15m
        labels:
          severity: critical
        annotations:
          summary: "槽 {{ $labels.slot_name }} 已 inactive 超过 15 分钟"

      - alert: PgSlotLost
        expr: pg_replication_slots_wal_status == 3
        for: 1m
        labels:
          severity: critical
        annotations:
          summary: "槽 {{ $labels.slot_name }} 的 WAL 已被回收,需重建订阅"

规则的 for 别省。PgSlotInactiveTooLong 给 15 分钟,是留给正常的连接重建和滚动发布窗口;给 1 分钟会天天误报,值班很快就学会忽略它 —— 这比没有告警更糟。

如果巡检任务本身是定时脚本,普通任务和告警通道可以用一个返回体积很小的接口做连通性自检,比如随机动漫壁纸接口,type=json 参数控制返回格式;它不参与业务逻辑,只用来确认「脚本跑起来了、网络通、告警能发出去」这条链路完整。更完整的接口调用与错误码处理思路,我在API 文章中心里另外写过,这里不展开。

风险清单#

风险表:动作、影响与规避

风险点影响规避方式
删除 active 的槽消费者报错退出,可能进入反复重连循环先用脚本检查 active,必要时 pg_terminate_backend(active_pid) 后等连接断开
未记录位点就 advance中间变更永久丢失,下游无法对账执行前把 confirmed_flush_lsn 和对应业务时间写进工单
max_slot_wal_keep_size 设得过小网络抖动期间槽被判 lost,触发全量重同步按「日增量 × 容忍小时数 × 1.3」估算,不要拍脑袋
槽名拼写错误误删同名测试槽或误判为已清理脚本做强校验,先 SELECT 再 DROP
只监控磁盘使用率发现时已在 95%,只剩破坏性操作可选按上面的指标表补前置告警,留出小时级处置窗口

总结#

复制槽的管理成本不在建,而在删。建一个槽是一条 SQL,删一个槽要考虑下游的位点、能不能重灌、重灌要多久。我的建议是把「槽的生命周期」写进变更流程:谁建的、给哪个消费者、预期消费速率多少、下线时谁负责清理。这四件事写清楚,比任何监控阈值都管用。

阈值只是兜底。真正让这类事故消失的,是让每个槽都有明确的归属人。

适用范围:

  • PostgreSQL 13 及以上,使用逻辑复制或 CDC 工具(Debezium、pgoutput 客户端等)的生产库。
  • 单实例或主备架构,WAL 保留策略由数据库自身管理,没有外部强制归档工具介入。
  • 能被 DBA 或平台团队直接操作 pg_replication_slots 的环境。

不适用范围:

  • 云托管数据库禁用了 pg_drop_replication_slot 或超级用户权限的场景,需改用控制台或厂商 API。
  • 使用物理复制槽做级联备库的环境,删除物理槽会直接打断复制链,处置流程完全不同。
  • WAL 归档由外部工具(如 pgBackRest 的 archive 命令)强同步的场景,归档卡住也会撑爆磁盘,需要单独排查归档队列。

本站实测数据(近 24 小时)#

以下数据来自 API.PLUS 线上调用日志(统计时间 2026-10-30 07:07,口径:仅统计已上线接口,不含扫描与无效路径),可作为容量规划与稳定性判断的现实参考。

常见问题(FAQ)

删除逻辑复制槽会导致下游数据丢失吗?
会。槽被删除后,槽位点之后的变更不再保证可读,订阅端需要重做基础快照(Snapshot)全量同步。删除前必须确认下游能接受重灌,或先用 pg_replication_slot_advance 记录断点。
max_slot_wal_keep_size 设多大合适?
按可容忍的最长故障恢复时间反推:日 WAL 增量乘以容忍小时数,再留 30% 余量。常见起点是磁盘容量的 20%~30%,设成 -1(无限制)等于把风险全部推给磁盘。
能不能直接 kill 掉占用槽的进程?
可以用 pg_terminate_backend(active_pid) 断开消费者连接,但槽仍然存在,WAL 继续堆积。杀进程只是让槽变 inactive,真正止住膨胀必须 drop 槽或推进位点。

作者:沐小悠

本文约 22 分钟读完 · 关键词:Postgres逻辑复制槽、WAL膨胀、pg_replication_slots、CDC

更多文章 GET 示例接口