1. 首页
  2. 技术博客
  3. UXDB WAL日志安全清理自动化实战

UXDB WAL日志安全清理自动化实战

  • 陈亮志
  • 发布于 2026-03-31
  • 176 次阅读

在 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)

存在的问题包括:

  1. 目录为空时会报错;

  2. -d 很可能只是模拟执行;

  3. 最重要的是:它把「最旧文件」直接当成安全边界。

但「最旧文件」只能说明它最早,不代表它已经不再被复制、备份或恢复依赖。因此,更稳妥的脚本示例如下:

#!/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 清理从「经验驱动的高风险手工操作」转化为「有边界、有依据、可审计的自动化常态运维」。其价值主要体现在以下几个方面:

  1. 消除人肉误判风险 通过把清理动作与备份成功状态、复制槽健康度、清理边界校验绑定,避免人工在缺乏上下文的情况下误删 WAL。

  2. 构建系统级降级能力 当归档链路拥塞或磁盘逼近高水位时,系统可以进入应急流程,优先保障数据库写入可用性,而不是直接被磁盘拖垮。

  3. 实现 WAL 生命周期可观测 借助数据库侧与文件系统侧的双口径监控,可以明确区分问题到底发生在本地 WAL 堆积、归档卡死,还是清理流程失效。

  4. 强化恢复链条完整性 通过以基础备份成功为主线、以备份元数据作为清理边界来源,可以最大程度降低「日志删掉了,但恢复链断了」的风险。

最终需要强调的是:

  • 当前 UXDB 对 archive_cleanup_commandux_archivecleanupux_basebackup 等对象的实现细节,应以现网版本帮助信息和测试结果为准;

  • 特别是主库侧归档目录清理,不应简单依赖 archive_cleanup_command 一条参数,而应以 归档确认 + 备份成功 + 边界校验 + 审计日志 为完整闭环;

  • 所有自动化清理脚本在正式上线前,都应先在测试环境完成命令校验、边界验证和回退演练。

只要同时满足 备份验证通过、复制链路健康、清理边界明确、归档与清理日志可追溯 这几个前提,UXDB 的 WAL 自动化治理才能真正做到既安全,又可持续。