你好呀,我是小邹。
假设你遇到这样一个现场:
业务表的大小没有明显变化,服务器磁盘使用率却持续上涨。进一步检查发现,增长的主要是 pg_wal 目录。配置中的 max_wal_size 明明只有几 GB,这个目录却已经膨胀到几十 GB。
第一反应可能是:检查点是不是没执行?是不是应该做一次 VACUUM?那些看起来很旧的 WAL 文件能不能直接删除?
真正需要先回答的问题是:
这些 WAL 为什么还必须保留,究竟是谁阻止了它们被回收?
PostgreSQL 的 WAL 回收受到恢复、复制和归档等条件共同影响。即使检查点正常执行,只要某些保留条件没有解除,旧 WAL 仍可能继续占用磁盘。
本文以 PostgreSQL 18 的主库为示例,诊断 SQL 以只读查询为主。文中的容量和速率均为教学假设,不代表某个真实生产环境的测量结果。
一、先纠正一个误区:max_wal_size 不是磁盘硬上限
max_wal_size 很容易被理解成“WAL 最多只能占这么多空间”。
但它实际上是与自动检查点相关的软限制。在高写入负载、归档失败、较大的 WAL 保留配置等情况下,实际占用可以超过这个值。把它从 8 GB 调成 4 GB,并不意味着系统会立即删除多出来的文件。
几个容易混淆的参数,可以这样区分:
| 参数 | 主要作用 | 不能据此得出的结论 |
|---|---|---|
max_wal_size |
影响自动检查点的触发 | 整个 WAL 目录绝不会超过这个大小 |
wal_keep_size |
为复制额外保留一定数量的历史 WAL | 这是历史 WAL 的保留上限 |
max_slot_wal_keep_size |
在检查点时限制复制槽可以要求保留的 WAL 范围 | 达到限制后,系统会自动暂停业务写入 |
其中,max_slot_wal_keep_size 默认是 -1,表示不通过这个参数限制复制槽保留 WAL 的数量。设置有限值可以约束风险,但也可能使落后过多的消费者失去继续复制所需的 WAL。
可以用下面这张图理解回收过程:
graph LR
A["业务写入"] --> B["生成 WAL"]
B --> C{"仍被恢复、复制或归档需要?"}
C -->|是| D["继续保留"]
C -->|否| E["按策略删除或复用"]
F["复制槽进度停滞"] --> C
G["归档失败或落后"] --> C
H["检查点与保留配置"] --> C
因此,排查方向不应只是“为什么没有删文件”,而应该转向“哪些保留条件还没有解除”。这也是为什么盲目调整检查点参数,经常不能解决 WAL 堆积。
二、先确认现场:查的是哪台库,增长的是什么
1. 确认版本和主备角色
先执行:
SELECT
current_setting('server_version') AS server_version,
pg_is_in_recovery() AS is_standby;
本文后续使用的 pg_current_wal_lsn() 查询应在主库上执行。若 is_standby 为 true,不要直接照搬主库的 LSN 诊断方式,应切换到相应主库,或针对备库使用恢复进度相关函数。
2. 查看 WAL 目录中的文件大小
SELECT
count(*) AS file_count,
pg_size_pretty(
COALESCE(sum(size), 0)::bigint
) AS wal_directory_size
FROM pg_ls_waldir();
pg_ls_waldir() 返回 WAL 目录中普通文件的名称、大小和修改时间,默认可由超级用户以及具有 pg_monitor 权限的角色使用。这里汇总的是文件长度,不等于底层文件系统实际分配块的精确统计。
实际处置时,我建议同时记录两组数据:数据库看到的 WAL 文件大小,以及 WAL 所在存储卷的可用空间。后续所有“还剩多少时间”的判断,都应以真正承载 WAL 的存储卷为准。
3. 查看当前生效配置
不要只看仓库里的配置模板,直接查询实例当前使用的值:
SELECT
name,
setting,
unit,
source,
pending_restart
FROM pg_settings
WHERE name IN (
'data_directory',
'wal_level',
'max_wal_size',
'min_wal_size',
'wal_keep_size',
'max_slot_wal_keep_size',
'archive_mode',
'archive_command',
'archive_library'
)
ORDER BY name;
source 有助于确认配置来源,pending_restart 可以提示是否存在尚未通过重启生效的配置变更。排查时应围绕实例实际生效值展开,而不是围绕“我记得应该配置过什么”展开。
三、复制槽排查:连接还在,不代表进度正常
复制槽会保存消费者的进度,其生命周期并不依赖某一条连接。消费者断开之后,槽仍然可能保留,这正是它能支持后续继续消费的基础,也是无人维护的槽可能带来容量风险的原因。
下面的 SQL 可以同时查看槽状态、保留边界和逻辑消费确认进度:
WITH wal_head AS MATERIALIZED (
SELECT pg_current_wal_lsn() AS current_lsn
)
SELECT
s.slot_name,
s.slot_type,
s.active,
s.wal_status,
s.restart_lsn,
s.confirmed_flush_lsn,
pg_wal_lsn_diff(
h.current_lsn,
s.restart_lsn
) AS restart_gap_bytes,
pg_size_pretty(
pg_wal_lsn_diff(h.current_lsn, s.restart_lsn)
) AS restart_gap,
pg_size_pretty(
pg_wal_lsn_diff(h.current_lsn, s.confirmed_flush_lsn)
) AS confirmed_gap,
pg_size_pretty(s.safe_wal_size) AS slot_headroom
FROM pg_replication_slots AS s
CROSS JOIN wal_head AS h
ORDER BY restart_gap_bytes DESC NULLS LAST;
这里最重要的是区分三个含义:
| 字段 | 应该如何理解 |
|---|---|
active |
当前是否正在通过这个槽进行流式消费 |
restart_lsn |
消费者仍可能需要的最早 WAL 位置 |
confirmed_flush_lsn |
逻辑槽消费者已确认接收数据的位置;物理槽为 NULL |
消费者已经确认到某个位置,不等于更早的所有 WAL 都已经满足回收条件。(PostgreSQL)
假设得到下面这样一组教学示例:
| 复制槽 | active | restart_gap | confirmed_gap |
|---|---|---|---|
orders_cdc |
true | 40 GiB | 32 MiB |
report_cdc |
false | 12 GiB | 12 GiB |
第二个槽明显需要检查消费者是否断开;但第一个槽同样值得关注:它仍在连接和确认数据,保留边界却远远落后。
另外,不能把 40 GiB 和 12 GiB 直接相加,认定两个槽独占了 52 GiB 磁盘。这是两个 LSN 区间的长度,它们可能重叠;restart_gap 应当被理解为保留压力的估计,而不是独占空间账单。这个判断来自 WAL 共享保留机制和 LSN 字节距离的含义。
对于 wal_status,尤其要关注 unreserved 和 lost:前者表示槽已不再保证保留所需 WAL,后者表示槽已经不可用。safe_wal_size 为 NULL 也不能一律解释为“安全”,它既可能对应无限制配置,也可能对应已经失效的槽。
为什么 confirmed_flush_lsn 在前进,restart_lsn 却不动?
在逻辑解码场景中,长事务可能使重新解码所需的起点停留在较早位置。因此,看到确认进度较新、保留边界却长期不动时,应把长事务列为排查方向,而不是只反复重启消费者。
可以先筛选事务持续时间较长的会话:
SELECT
pid,
usename,
application_name,
state,
clock_timestamp() - xact_start AS transaction_age,
wait_event_type,
wait_event,
left(query, 160) AS query_sample
FROM pg_stat_activity
WHERE xact_start IS NOT NULL
AND pid <> pg_backend_pid()
ORDER BY xact_start
LIMIT 10;
这一步是收集线索,不是自动终止依据。建议把事务开始时间、复制槽停滞时间和应用日志放在一起核对,再判断它是否相关、是否允许结束。
四、归档排查:failed_count 没增长,也不能直接判定正常
开启归档后,尚未成功归档的 WAL 不能正常进入后续回收流程。归档目的地写不进去,问题最终可能表现为主库本地磁盘不断增长。(PostgreSQL)
先查看归档统计:
SELECT
archived_count,
failed_count,
last_archived_wal,
last_archived_time,
last_failed_wal,
last_failed_time,
stats_reset
FROM pg_stat_archiver;
这些字段是累计统计和最近一次事件记录。建议间隔一段时间连续采样,重点看计数是否继续变化,而不是只看某个历史失败时间。最近一次成功归档的文件,也不能证明所有更早文件都已经归档成功。
排查时,我会把以下现象放在一起看:
业务仍在持续写入,WAL 占用持续上升,但归档成功计数长期不动。
接着再检查归档目的地容量、运行用户权限、挂载状态、网络访问和数据库日志。单独一个“最近很久没有归档”的时间戳,不足以区分数据库空闲与归档故障。
还有一个容易漏掉的细节:某些导致归档进程退出并重启的错误,不会计入 pg_stat_archiver 的失败统计,例如部分信号终止、特定 shell 错误,以及归档函数抛出的某些错误。因此,failed_count = 0 不能替代日志检查。
更不能为了让告警消失,把归档命令改成一个永远返回成功、实际却不保存文件的空操作。PostgreSQL 会把成功返回值视为归档完成;错误地报告成功,可能破坏后续恢复所依赖的 WAL 归档链。
五、容量判断:同时计算两个“剩余时间”
“磁盘还有 30%”并不是一个足够有用的处置依据。
我更关心两个问题:
当前磁盘还能承受多久的净增长?当前复制槽的保留预算还能支撑多久?
这两个时间可能相差很大。
1. 先测 WAL 生成速率
可以间隔一段时间,在独立的自动提交查询中分别采集:
SELECT
clock_timestamp() AS sampled_at,
wal_bytes,
stats_reset
FROM pg_stat_wal;
wal_bytes 是累计生成的 WAL 字节数。两次采样应检查 stats_reset 是否一致,并避免在同一个长事务里反复读取被缓存的累计统计。(PostgreSQL)
计算方式是:
WAL 生成速率 ≈ 两次 wal_bytes 的差值 ÷ 实际采样间隔
随后建立两种不同的估算:
| 估算目标 | 计算思路 | 使用条件 |
|---|---|---|
| 磁盘还能撑多久 | 扣除安全预留后的可用空间 ÷ 存储卷净增长速率 | 净增长速率持续为正 |
| 槽保留预算还能撑多久 | safe_wal_size ÷ WAL 生成速率 |
保留上限有限,且槽的保留边界近似不动 |
第二个估算只针对 WAL 保留预算耗尽的风险,不包含其他失效原因,也不是精确倒计时;槽进度、写入变化和检查点时机会影响实际结果。(PostgreSQL)
假设:
- 扣除安全预留后,磁盘还剩 30 GiB,净增长为 6 MiB/s。
- 某个槽的
safe_wal_size还剩 20 GiB,WAL 生成速率为 8 MiB/s。
按这些假设计算,磁盘约能支撑 85 分钟,而槽的保留预算约只能支撑 43 分钟。
也就是说,即使磁盘尚未写满,下游复制也可能先遇到问题。
2. 用恢复窗口反推保留预算
假设业务要求消费者断开 90 分钟后,仍有机会接着消费;故障期间的 WAL 生成速率按 8 MiB/s 估算。
仅这一段中断产生的 WAL 就约为:
8 × 90 × 60 ÷ 1024 ≈ 42.2 GiB
这还没有加入既有积压、写入突增、恢复启动时间和其他保留需求。
因此,我建议把复制槽容量设计成一个明确的业务约定:允许中断多久、峰值写入是多少、恢复后能以多快速度追赶,以及超过预算后如何补数。
保留上限不是越大越安全,也不是越小越节省。它是在主库容量安全与下游恢复能力之间作出的取舍。
尤其不要在已有大量积压时,突然把上限调到低于当前需要的值,再主动执行检查点。限制是在检查点相关处理中落实的,这样做可能加速所需 WAL 的移除,而不是完成一次无损清理。
六、真正的处置顺序:先保护容量,再解除保留条件
当 WAL 所在文件系统耗尽时,PostgreSQL 可能触发 PANIC 停止服务。因此,临近写满时,应优先争取安全空间,而不是继续进行没有时间边界的诊断。
我的建议是按三个阶段处理。
第一阶段:争取处置时间
根据剩余空间和增长速率,评估扩容、暂停可延期批量写入或降低非核心写入压力。
不要直接删除 pg_wal 中的文件,也不要在尚未确认用途时清理数据库目录中的其他内容。先把“还能安全运行多久”变成一个明确数字,再安排修复顺序。
第二阶段:修复真正停滞的环节
复制槽停滞,就检查消费者、事务和解码流程;归档停滞,就恢复真实的归档能力。
对于疑似废弃的槽,我建议先核对所属服务、负责人、最后使用情况和下游恢复方案,再决定是否删除。active = false 只能说明当前没有消费连接,不能代替业务上的废弃确认。槽本来就可以独立于连接持续存在。
第三阶段:验证恢复,而不只是验证“连接成功”
建议至少跟踪这些结果:保留边界是否持续前进、归档是否重新完成、下游数据是否追赶,以及存储卷是否停止危险增长。
不要把“WAL 文件数量没有立即下降”直接判定为修复失败。PostgreSQL 可能把不再需要的旧文件复用为后续 WAL 文件,而不是全部删除,目录大小因此不一定立即回到很小的值。
验证目标应该是恢复可持续的运行状态,而不是让某一个数字立刻变得好看。
七、两个常见“清理动作”,为什么不该作为默认答案
1. 频繁执行 CHECKPOINT
检查点可以推进恢复所需的 WAL 边界,但不能替代消费者确认,也不能让未成功归档的文件凭空满足归档要求。
此外,检查点涉及脏页写出,过于频繁会增加 I/O 压力;在启用全页写保护时,更短的检查点间隔还可能增加后续 WAL 生成量。用反复检查点去追赶一个仍在失控增长的 WAL 目录,可能适得其反。
2. 直接执行 VACUUM FULL
VACUUM 主要处理表和索引中的旧行版本空间,不是 WAL 目录清理命令。
VACUUM FULL 还会重写表,需要额外空间,并对目标表获取排他性很强的锁。对于一个已经接近磁盘耗尽、根因却是 WAL 保留的实例,它通常不是合适的第一步。
这两种操作都不是绝对不能使用,关键在于先证明它们能解决当前问题,而不是因为名字里带着“检查”或“清理”,就把它们当成通用修复按钮。
八、总结:不是找到大目录就结束了
排查 WAL 堆积,我建议始终围绕三个问题展开:
为什么还要保留?保留边界有没有前进?剩余容量能否覆盖修复时间?
找到 pg_wal 很大,只完成了现象定位。进一步区分复制槽停滞、归档受阻和正常的保留复用行为,才能决定下一步该恢复消费者、修复归档、调整容量,还是继续观察。
真正可靠的处置,不是尽快删掉最多的文件,而是在保住数据和恢复能力的前提下,让 WAL 的生成、保留与回收重新回到可持续状态。

评论区 0