在 UXDB 的日常运维中,WAL 预写日志管理一直是一个「高风险、强依赖、易误判」的环节。一旦清理策略过激,轻则导致备库追赶失败、复制中断,重则破坏恢复链条完整性;而如果清理不及时,又容易造成本地 ux_wal 或归档目录持续膨胀,最终触发磁盘写满、归档阻塞、实例异常等连锁故障。
在生产环境中,许多看似标准的配置实际上埋藏着隐患。如果不能准确理解 WAL 归档、备份、复制和清理之间的关系,所谓「自动清理」往往会变成故障放大器。
1. archive_command 的脆弱性:避免「假性归档」
很多环境中会直接使用如下配置:
archive_command = 'cp %p /wal_archive/%f'
这种写法虽然简单,但对目标盘状态、共享存储稳定性、权限以及瞬时 IO 抖动都极其敏感。一旦复制失败,UXDB 不会自动跳过,而是会继续保留相关 WAL,最终把本地 WAL 目录或归档目录拖满。
原始文章中曾给出如下「文件存在 + 标记」思路:
archive_command = 'test -f /wal_archive/%f || cp %p /wal_archive/%f && touch /wal_archive/%f.done'
这段写法存在逻辑缺陷:当 /wal_archive/%f 已存在时,test -f 返回成功,|| 右侧不会执行,最终可能出现 归档文件存在但 .done 不存在 的不一致状态。
更稳妥的写法应为:
archive_command = 'test -f /wal_archive/%f.done || (cp %p /wal_archive/%f && touch /wal_archive/%f.done)'
其逻辑含义是:
-
.done存在,说明归档已经完整完成,本次直接跳过; -
.done不存在,则执行复制; -
仅在复制成功后才创建
.done标记。
如果归档目标位于共享存储或远端挂载点,也可使用带校验能力的 rsync:
archive_command = 'test -f /wal_archive/%f.done || (rsync -a --checksum %p /wal_archive/%f && touch /wal_archive/%f.done)'
这里要特别注意:
-
%p是 WAL 源文件的完整路径; -
%f只是 WAL 文件名; -
不能把
%p当目录、把%f当路径去错误拼接。
2. archive_cleanup_command 的误判风险:不能把 %r 直接等同于复制侧最老需求
把 %r 理解为「最旧的需要保留的 WAL」,这个表述不够严谨。更准确地说,archive_cleanup_command 传入的 %r 是 供归档清理工具使用的 WAL 边界文件名,其常见语义是:
这个 WAL 文件之前的归档文件,可以考虑删除。
它并不等价于复制槽的 restart_lsn,也不能替代对复制滞后、基础备份窗口、恢复保留策略的独立判断。
因此,不能因为拿到了 %r,就认为「更老的 WAL 一定安全可删」。在以下场景中尤其危险:
-
复制槽长期停滞;
-
某个备库严重落后;
-
基础备份已经开始但尚未完成;
-
仍需保留较早归档用于恢复窗口。
在执行任何主动清理动作前,建议先做最基本的前置检查:
SELECT slot_name, slot_type, active, restart_lsn
FROM ux_replication_slots;
并结合当前 WAL 位置、接收位置判断延迟情况:
SELECT ux_current_wal_lsn(), ux_last_wal_receive_lsn();
3. archive_cleanup_command 的适用场景要讲清楚
文中对 archive_cleanup_command 的表述需要特别谨慎。更稳妥的定位应是:
-
主库侧,主线仍应是
archive_command+ 备份成功后的外部清理逻辑 + 定时巡检 / 应急脚本; -
恢复或备库场景下,再谨慎使用
archive_cleanup_command; -
若未核实当前 UXDB 版本对该参数的具体语义与触发方式,不建议把它写成主库侧归档目录清理的核心方案。
4. ux_archivecleanup 的参数不能用错
错误示例为:
ux_archivecleanup -d /wal_archive %r
如果当前版本的 ux_archivecleanup 中 -d 表示调试输出或模拟执行,那么这类写法很可能只是打印将删除的内容,并未实际删除文件。
正确的命令格式应为:
ux_archivecleanup /wal_archive %r
如果需要记录日志,则可以写成:
ux_archivecleanup /wal_archive %r >> /var/log/ux_archivecleanup.log 2>&1
这里的参数含义是:
-
第一个参数是归档目录;
-
第二个参数是边界 WAL 文件名;
-
不是目录加完整路径,更不是随便拿一个「最旧文件」来代替清理边界。
二、构建具备容错与自愈能力的治理方案
解决 WAL 清理问题,不能只靠单一数据库参数,而要把归档、备份、边界判断、应急脚本和监控告警组合起来,形成一套完整的治理链路。
1. 基础配置:提升归档的鲁棒性
可以采用如下更稳妥的基础配置:
archive_mode = on
archive_command = 'test -f /wal_archive/%f.done || (rsync -a --checksum %p /wal_archive/%f && touch /wal_archive/%f.done)'
wal_keep_size = 1GB
这组配置的设计意图是:
-
archive_mode = on:启用归档; -
archive_command:通过.done显式确认归档完成状态,同时使用rsync提高归档侧可靠性; -
wal_keep_size = 1GB:在本地 WAL 目录中尽量保留一定体量的 WAL,降低复制延迟时备库因缺段而断链的概率。
需要明确纠正一个常见误区:
wal_keep_size 不是归档失败时的「保险丝」。
它控制的是本地 WAL 目录的最小保留量,与归档是否成功没有直接因果关系。归档失败是否会导致 WAL 堆积,更多取决于:
-
归档命令是否成功;
-
复制槽是否推进;
-
检查点回收是否受阻;
-
本地和归档端磁盘容量是否足够。
如果复制槽长期滞留,真正应该关注的是复制槽本身,以及当前版本是否提供了相应的保留上限控制能力。
2. 引入守护脚本:解决磁盘空间「爆仓」
在归档目录使用率过高时,由守护脚本主动介入清理,避免数据库因磁盘写满而宕机。这个方向是对的,但原始实现不够严谨。
典型风险写法如下:
OLDEST_WAL=$(ls -t $WAL_DIR/0000* | tail -n 1)
ux_archivecleanup -d $WAL_DIR $(basename $OLDEST_WAL)
存在的问题包括:
-
目录为空时会报错;
-
-d很可能只是模拟执行; -
最重要的是:它把「最旧文件」直接当成安全边界。
但「最旧文件」只能说明它最早,不代表它已经不再被复制、备份或恢复依赖。因此,更稳妥的脚本示例如下:
#!/bin/bash
# /usr/local/bin/ux_wal_cleaner.sh
WAL_DIR="/wal_archive"
THRESHOLD=90
USAGE=$(df -P "$WAL_DIR" | awk 'NR==2 {print $5}' | tr -d '%')
if [ "$USAGE" -ge "$THRESHOLD" ]; then
OLDEST_WAL=$(ls -tr "$WAL_DIR"/0000* 2>/dev/null | head -n 1)
if [ -n "$OLDEST_WAL" ]; then
WAL_NAME=$(basename "$OLDEST_WAL")
# 这里只是示意。生产上线前应改为:
# 1. 读取最近成功备份对应的允许清理边界;
# 2. 校验复制槽/备库进度;
# 3. 再决定是否执行真正清理。
logger "Emergency WAL cleanup requested, oldest WAL=$WAL_NAME, disk usage=${USAGE}%"
else
logger "Emergency WAL cleanup skipped: no WAL files found in $WAL_DIR"
fi
fi
这里有一个重要调整:
应急脚本可以负责「触发应急流程」,但不应再直接示范「按最旧文件去删」。
因为磁盘高水位只能决定「要不要进入应急模式」,不能单独决定「删到哪里」。真正的删除边界,必须来自可信的备份或恢复元数据。
3. 集成备份验证:让清理逻辑与备份生命周期绑定
最安全的清理时机,不是「目录里文件看起来太多了」,而是:
某次基础备份已经成功完成,且其对应的可恢复边界已经明确。
因此,主线方案应当是把 WAL 清理和备份流程绑定。
原始写法如下:
ux_basebackup -D /backups/ux -Ft -Xs -P
if [ $? -eq 0 ]; then
START_WAL=$(cat /backups/ux/backup_label | grep "START WAL" | awk '{print $3}')
ux_archivecleanup /wal_archive $START_WAL
fi
问题在于,backup_label 不是这种简单结构。典型内容类似:
START WAL LOCATION: 0/2000028 (file 000000010000000000000002)
CHECKPOINT LOCATION: 0/2000060
...
因此,更稳妥的提取方式应为:
START_WAL=$(grep "START WAL LOCATION" /backups/ux/backup_label | grep -o 'file [0-9A-F]*' | awk '{print $2}')
再结合备份成功判断执行后续动作:
ux_basebackup -D /backups/ux -Ft -Xs -P
if [ $? -eq 0 ]; then
START_WAL=$(grep "START WAL LOCATION" /backups/ux/backup_label | grep -o 'file [0-9A-F]*' | awk '{print $2}')
if [ -n "$START_WAL" ]; then
ux_archivecleanup /wal_archive "$START_WAL"
echo "Backup succeeded, cleaned WAL before $START_WAL"
else
echo "Backup succeeded, but failed to parse START_WAL from backup_label" >&2
fi
fi
如果条件允许,建议在备份成功之外,再增加一层校验,例如:
-
备份目标目录内容完整;
-
ux_verifybackup或等价校验已通过; -
备份记录中的边界与归档目录状态一致。
三、从「可用」到「可靠」
如果希望这套机制在无人值守的生产环境中稳定运行,还必须增加并发控制、监控告警和日志审计三道防线。
1. 双重锁定机制:不要把 flock 直接塞进内核回调
写法类似:
archive_cleanup_command = 'flock -n /tmp/wal_cleanup.lock ux_archivecleanup /wal_archive %r'
这种做法看似可以防并发,但工程上并不理想,原因主要有两点:
-
数据库内部调用外部命令时环境受限,一旦
flock -n抢锁失败会立即返回非零; -
如果数据库把这种返回视为清理失败,就可能持续重试,产生大量无意义日志,甚至引发误判。
更推荐的方式是:
-
把复杂并发控制放到独立脚本;
-
archive_cleanup_command如果确需使用,也只调用简单、可预期的包装脚本; -
锁、日志、退出码统一在脚本内部管理。
例如:
#!/bin/bash
LOCK_FILE="/tmp/wal_cleanup.lock"
WAL_DIR="/wal_archive"
BOUNDARY="$1"
exec 9>"$LOCK_FILE"
flock -n 9 || exit 0
ux_archivecleanup "$WAL_DIR" "$BOUNDARY" >> /var/log/ux_archivecleanup.log 2>&1
2. 实时监控告警:数据库本地 WAL 与归档目录要分开监控
这里是最终修订中最需要强调的一点。
如果使用 ux_ls_waldir(),监控到的是 数据库实例本地 WAL 目录,而不是 /wal_archive 这样的归档目录。因此,不能把两者混为一谈。
更稳妥的监控口径应拆成两套:
(1)数据库本地 WAL 目录监控
用于判断本地 WAL 是否异常堆积,可参考如下 SQL:
SELECT
name,
modification AS last_modified,
EXTRACT(EPOCH FROM (now() - modification)) AS age_seconds
FROM ux_ls_waldir()
WHERE name ~ '^[0-9A-F]{24}$'
ORDER BY modification ASC
LIMIT 1;
统计 WAL 文件数量:
SELECT COUNT(*)
FROM ux_ls_waldir()
WHERE name ~ '^[0-9A-F]{24}$';
(2)归档目录 /wal_archive 监控
归档目录属于文件系统对象,建议由操作系统侧或监控代理侧直接采集:
-
目录磁盘使用率;
-
WAL 文件数量;
-
最旧文件修改时间;
-
.done缺失率; -
清理任务成功率 / 失败率。
只有把「数据库本地 WAL」与「归档目录归档文件」分开监控,才能真正定位问题是在本地回收受阻,还是在归档链路堵塞。
3. 日志审计追踪:所有清理动作必须留痕
无论是备份成功后的正常清理,还是空间高水位触发的应急流程,所有动作都必须留下独立日志。推荐至少记录:
-
清理触发时间;
-
触发原因;
-
使用的清理边界;
-
实际执行结果;
-
失败原因。
例如:
ux_archivecleanup /wal_archive "$START_WAL" >> /var/log/ux_archivecleanup.log 2>&1
必要时还可以将关键事件额外写入 logger,便于统一接入系统日志平台。
四、WAL 清理自动化流程图
下面给出一个更符合 UXDB 实际运维语义的自动化流程:
这个流程的核心思想是:
-
清理以「成功备份」为主线;
-
高水位只决定是否进入应急模式,不直接决定删到哪里;
-
每一次删除都必须有边界依据和日志记录。
总结
本方案的核心目标,是把 WAL 清理从「经验驱动的高风险手工操作」转化为「有边界、有依据、可审计的自动化常态运维」。其价值主要体现在以下几个方面:
-
消除人肉误判风险 通过把清理动作与备份成功状态、复制槽健康度、清理边界校验绑定,避免人工在缺乏上下文的情况下误删 WAL。
-
构建系统级降级能力 当归档链路拥塞或磁盘逼近高水位时,系统可以进入应急流程,优先保障数据库写入可用性,而不是直接被磁盘拖垮。
-
实现 WAL 生命周期可观测 借助数据库侧与文件系统侧的双口径监控,可以明确区分问题到底发生在本地 WAL 堆积、归档卡死,还是清理流程失效。
-
强化恢复链条完整性 通过以基础备份成功为主线、以备份元数据作为清理边界来源,可以最大程度降低「日志删掉了,但恢复链断了」的风险。
最终需要强调的是:
-
当前 UXDB 对
archive_cleanup_command、ux_archivecleanup、ux_basebackup等对象的实现细节,应以现网版本帮助信息和测试结果为准; -
特别是主库侧归档目录清理,不应简单依赖
archive_cleanup_command一条参数,而应以 归档确认 + 备份成功 + 边界校验 + 审计日志 为完整闭环; -
所有自动化清理脚本在正式上线前,都应先在测试环境完成命令校验、边界验证和回退演练。
只要同时满足 备份验证通过、复制链路健康、清理边界明确、归档与清理日志可追溯 这几个前提,UXDB 的 WAL 自动化治理才能真正做到既安全,又可持续。