PostgreSQL备份方案踩坑记:为什么我最终放弃了pg_rman

64 阅读19分钟

写在前面

去年年底,我们组接了个活,要把数据库备份方案优化一下。当时的情况是这样的:一台PostgreSQL实例,大概50G数据,每天晚上用1Panel做全量备份。老板说这样不行,万一白天出事了,一天的数据就没了,让我们改成增量备份。

我当时想,这不简单吗?翻了翻《数据库实战派》那本书,看到pg_rman这个工具,支持全量、增量备份,看起来挺完美的。于是就开始搞,结果被折腾了快两个月,最后发现这玩意儿就是个坑。

现在回过头看,其实用PostgreSQL自带的pg_basebackup配合WAL归档就够了,简单又可靠。写这篇文章,主要是想记录一下当时的决策过程和踩过的坑,希望其他人别再掉进去。

我们的需求到底是什么

先说说我们的实际需求,这个很重要,因为后面会发现很多工具其实是在解决别的问题:

硬件环境:

  • 主数据库: 192.168.1.100 (50G左右)
  • 备份服务器: 192.168.1.101 (专门用来存备份)

业务要求:

  • 增量备份,不能每次都全量(太占空间和时间)
  • 备份文件要自动同步到备份服务器
  • 出事了在备份服务器上恢复,人工确认后再同步回主库
  • 恢复到小时级别就行,能做到分钟级更好

硬约束:

  • 全量备份要控制在30分钟以内
  • 增量备份要控制在5分钟以内
  • 不能影响主库的正常业务

看起来很合理对吧?我当时也这么觉得。

PostgreSQL备份的基本原理

在说我怎么踩坑之前,先把PostgreSQL的备份原理说清楚,不然后面看不懂。

PostgreSQL的数据持久化主要靠两个东西:

数据文件和WAL日志

graph LR
    A[客户端写入] --> B[写入WAL日志]
    B --> C[WAL写入磁盘]
    C --> D[返回客户端成功]
    D --> E[后台进程刷数据文件]
    
    style B fill:#f9f,stroke:#333
    style C fill:#bbf,stroke:#333
    style E fill:#bfb,stroke:#333

这个机制很关键:

  • WAL日志(Write-Ahead Log): 先记日志,16MB一个文件,写满了就切换到下一个
  • 数据文件: 真正的表数据,通过checkpoint机制从WAL异步刷新

所以理论上,只要有一个基础备份 + 之后的所有WAL文件,就能恢复到任意时间点。这就是PITR(Point-In-Time Recovery)。

备份恢复的本质

graph TB
    subgraph "备份阶段"
        A[基础备份<br/>base backup] --> B[持续归档WAL]
        B --> C[WAL-1]
        B --> D[WAL-2]
        B --> E[WAL-3]
        B --> F[WAL-...]
    end
    
    subgraph "恢复阶段"
        G[恢复基础备份] --> H[按顺序应用WAL]
        H --> I[恢复到指定时间点]
    end
    
    A --> G
    C --> H
    D --> H
    E --> H
    F --> H
    
    style A fill:#f96,stroke:#333
    style B fill:#bbf,stroke:#333
    style I fill:#9f9,stroke:#333

理解这个原理很重要,因为后面会发现,pg_rman和pg_basebackup本质上都在做这件事,只是实现方式不同。

第一次尝试:pg_rman方案

为什么选了pg_rman

当时我看文档,pg_rman的卖点是这样的:

  • 支持全量、增量、差异备份三种模式
  • 自动管理备份集,可以设置保留策略
  • 备份文件自动压缩
  • 看起来很"专业"

最打动我的是"增量备份"这个功能。我当时想,每小时只备份变化的数据,多省空间啊。

架构设计

我们的方案是这样的:

graph TB
    subgraph db1["主数据库 192.168.1.100"]
        A[PostgreSQL实例] --> B[WAL归档]
        B --> C[WAL归档目录]
        A --> D[pg_rman备份进程]
        D --> E[备份目录]
    end
    
    subgraph task["定时任务"]
        F[周日凌晨全量备份] --> D
        G[每小时增量备份] --> D
        H[Python脚本读取最新备份] --> I[rsync同步]
    end
    
    subgraph db2["备份服务器 192.168.1.101"]
        I --> J[备份目录副本]
        I --> K[WAL归档副本]
    end
    
    E --> H
    C --> I
    
    style D fill:#f96,stroke:#333
    style I fill:#bbf,stroke:#333

看起来还挺完美的对吧?周期性全量+增量,自动同步到备份服务器。

安装和配置

安装过程倒是不复杂:

# 两台服务器都要装
sudo yum install -y https://download.postgresql.org/pub/repos/yum/reporpms/EL-7-x86_64/pgdg-redhat-repo-latest.noarch.rpm
yum install postgresql15-libs
rpm -ivh ./pg_rman-1.3.15-1.pg15.rhel7.x86_64.rpm

然后配置环境变量,这里有个坑,待会说:

# ~/.bash_profile
export BACKUP_PATH=/data/db-backup/pg-backup/fullbackup
export ARCLOG_PATH=/data/db-backup/pg-backup/walbackup
export SRVLOG_PATH=/data/db-backup/pg-backup/srvlog
export PGDATA=/data/postgres/data  # 这个必须配,不然会报错

修改postgresql.conf开启WAL归档:

wal_level = replica
archive_mode = 'on'
archive_command = 'mkdir -p /data/db-backup/pg-backup/walbackup && cp %p /data/db-backup/pg-backup/walbackup/%f'

重启数据库,初始化pg_rman:

pg_ctl -D /data/postgres/data restart
pg_rman init

第一个大坑:PGDATA必须配置

配置的时候我偷懒,没仔细看文档,PGDATA这个环境变量没配。结果全量备份时直接报错:

ERROR: switched WAL could not be archived in 10 seconds

找了半天才发现,pg_rman内部要用这个变量去读配置文件。这个设计就很迷,明明可以通过参数传,为什么非要环境变量?

执行备份

全量备份:

pg_rman backup --backup-mode=full --with-serverlog --progress -h localhost
pg_rman validate  # 验证备份
pg_rman show      # 查看备份列表

增量备份:

pg_rman backup --backup-mode=incremental --progress --compress-data -h localhost
pg_rman validate

看起来还行,全量备份15分钟,50G数据。增量备份3分钟,每次几百MB。

备份文件结构

这是pg_rman生成的文件结构:

/data/db-backup/pg-backup/fullbackup/
├── 20241218/
│   ├── 084826/              # 全量备份时间戳
│   │   ├── database/        # 数据文件
│   │   ├── arclog/          # 归档日志
│   │   ├── srvlog/          # 服务日志
│   │   ├── backup.ini       # 备份元数据
│   │   ├── file_database.txt
│   │   └── file_arclog.txt
│   └── 113349/              # 增量备份时间戳
│       └── ...
└── pg_rman.ini

每次增量备份都会创建一个新的时间戳目录,里面保存了变化的数据块。

恢复测试

在备份服务器上测试恢复:

# 停止数据库,删除data目录
pg_ctl -D /data/postgres/data stop
rm -rf /data/postgres/data

# 从主库scp备份文件过来(这里就很麻烦)
scp -r postgres@192.168.1.100:/data/db-backup/pg-backup/* /data/db-backup/pg-backup/

# 恢复到指定时间点
pg_rman restore -B /data/db-backup/pg-backup/fullbackup \
  --recovery-target-time "2024-12-18 11:33:49" \
  --hard-copy

结果启动数据库又报错了:

FATAL: could not access file "pg_stat_statements": No such file or directory

原来主库装了pg_stat_statements插件,备份服务器没装。得注释掉postgresql.conf里的:

#shared_preload_libraries = 'repmgr,pg_stat_statements'

这又是一个坑:环境不一致就会有问题。

自动化脚本

为了每小时自动备份和同步,我写了个Python脚本:

# -*- coding: utf-8 -*-
import subprocess
import re

def run_shell_command(command):
    process = subprocess.Popen(
        command, shell=True, 
        stdout=subprocess.PIPE, 
        stderr=subprocess.PIPE
    )
    stdout, stderr = process.communicate()
    
    if process.returncode != 0:
        error = subprocess.CalledProcessError(process.returncode, command)
        error.output = stdout
        error.stderr = stderr
        raise error
    
    return stdout

def extract_date_time(line):
    # 解析pg_rman show的输出
    match = re.match(r"(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2})", line)
    if match:
        date_str = match.group(1).replace('-', '')
        time_str = match.group(2).replace(':', '')
        return date_str, time_str
    return None, None

try:
    # 增量备份
    print("执行增量备份...")
    backup_result = run_shell_command(
        "pg_rman backup --backup-mode=incremental --progress --compress-data -h localhost"
    )
    
    # 验证备份
    validate_result = run_shell_command("pg_rman validate")
    
    # 获取最新备份信息
    command_output = run_shell_command('pg_rman show')
    lines = command_output.strip().split('\n')
    first_line = lines[3]  # 第三行是最新的备份
    
    date_str, time_str = extract_date_time(first_line)
    print("最新备份: {} {}".format(date_str, time_str))
    
    # 同步backup目录
    full_backup_source = "/data/db-backup/pg-backup/fullbackup/{}/{}".format(date_str, time_str)
    full_backup_dest = "/data/db-backup/pg-backup/fullbackup/{}".format(date_str)
    rsync_command = "rsync -avzuc {} postgres@192.168.1.101:{}".format(
        full_backup_source, full_backup_dest
    )
    print("同步备份目录...")
    run_shell_command(rsync_command)
    
    # 同步WAL目录
    wal_source = "/data/db-backup/pg-backup/walbackup"
    wal_dest = "/data/db-backup/pg-backup/"
    sync_wal_command = "rsync -avzuc {} postgres@192.168.1.101:{}".format(
        wal_source, wal_dest
    )
    print("同步WAL日志...")
    run_shell_command(sync_wal_command)
    
    print("备份同步完成!")
    
except subprocess.CalledProcessError as e:
    print("命令执行失败: {}".format(e.stderr))

这个脚本的问题是:

  1. 要解析pg_rman show的输出,很脆弱
  2. 目录层级复杂,容易写错路径
  3. 要分两次rsync,而且目标路径还不一样

使用pg_rman一个月后的问题

用了一个月,问题逐渐暴露出来了。

问题一:存储开销太大

我实际测了一下,每天的备份文件大概是这样的:

pg_rman自己的文件: 2-3GB

  • database目录: 记录变化的数据块,即使压缩了也要1-2GB
  • arclog目录: 备份期间的WAL日志副本,几百MB
  • 各种元数据文件

WAL归档文件: 1-2GB

  • 16MB一个WAL文件,一天大概产生60-100个

加起来每天4-5GB,而且是重复存储。因为pg_rman的arclog目录和系统的walbackup目录里的WAL是重复的!

后来我算了一下,如果只保留WAL文件,每天也就1-2GB。pg_rman等于把存储成本翻了一倍多。

问题二:恢复粒度受限

这个问题更要命。我们最开始的需求是"恢复到小时级别,分钟级更好"。

但pg_rman的增量备份每次要3分钟,备份期间还会对主库有IO影响。我们不可能每5分钟就做一次增量备份,那主库就别干活了。

所以实际上只能每小时做一次增量备份。这意味着:

  • 如果10:30出问题,只能恢复到10:00的增量备份
  • 中间30分钟的数据,还得从WAL里恢复
  • 那为什么不直接用WAL恢复呢?

pg_rman的增量备份,本质上是把变化的数据块打包。但PostgreSQL的PITR机制,本来就可以从基础备份+WAL恢复到任意时间点,根本不需要这个"增量备份"。

问题三:可靠性隐患

pg_rman是NTT开发的第三方工具,看起来很久没更新了:

  • 官方文档很简略,很多细节没说清楚
  • 社区不活跃,遇到问题基本没人回答
  • GitHub上的issue很多都没关闭
  • 最新版本还是2019年发布的

最让我担心的是,万一pg_rman出bug,备份文件损坏了,我们根本没能力修。而PostgreSQL自带的工具,至少有官方保证。

问题四:复杂度太高

回头看看我们的方案:

  1. 配置pg_rman的环境变量
  2. 配置WAL归档
  3. 周期性执行全量和增量备份
  4. 解析pg_rman show的输出
  5. 根据时间戳定位文件目录
  6. 分两次rsync同步文件
  7. 恢复时还要指定recovery-target-time

每一步都可能出错,而且很难排查。比如那个PGDATA环境变量,我当时找了半天才发现。

转折:咨询赵渝强老师

用了一个月pg_rman,我越用越觉得不对劲。正好看到《数据库实战派》的作者赵渝强老师在B站,于是我就把我们的方案发过去请教。

我大概是这么问的:

赵老师,我们现在用pg_rman做增量备份,每周全量一次,每小时增量一次。但感觉有几个问题:

  1. 存储开销比预期大很多,pg_rman的文件加WAL文件,大概是纯WAL文件的1.5倍
  2. 增量备份要3分钟,频率高了影响性能,只能每小时一次
  3. pg_rman是第三方工具,文档和社区都不太行

我在想,是不是直接用pg_basebackup + WAL归档就够了?周日做一个基础备份,平时就监控WAL文件变化,实时rsync到备份服务器。这样恢复粒度能到分钟级(最多丢16MB),而且更简单可靠。

这个思路有问题吗?

赵老师回复很快,大意是:

你的思路是对的。pg_rman确实有这些问题,它的增量备份在大部分场景下是多余的。PostgreSQL的PITR机制已经足够强大,基础备份+持续归档就能满足需求。

pg_basebackup + WAL归档的方案更简单,也更可靠。唯一要注意的是WAL归档的及时性,可以用inotify监控,或者改成流复制。

看到这个回复,我心里的石头算是落地了。决定推翻之前的方案,重新来。

更优的方案:pg_basebackup + 持续归档

新方案的设计思路

既然PostgreSQL自带的PITR机制已经够用,我们就直接用:

graph TB
    subgraph db1["主数据库 192.168.1.100"]
        A[PostgreSQL实例] --> B[产生WAL日志]
        B --> C[archive_command归档]
        C --> D[WAL归档目录]
    end
    
    subgraph monitor["监控同步"]
        E[inotifywait监控WAL目录] --> F[检测到新文件]
        F --> G[rsync实时同步]
    end
    
    subgraph db2["备份服务器 192.168.1.101"]
        G --> H[WAL归档副本]
        I[基础备份 base.tar] --> J[恢复时解压]
        H --> K[按顺序应用WAL]
        J --> K
        K --> L[恢复到任意时间点]
    end
    
    D --> E
    
    style B fill:#f96,stroke:#333
    style G fill:#bbf,stroke:#333
    style L fill:#9f9,stroke:#333

核心思路:

  1. 周日凌晨用pg_basebackup做一个基础备份(base.tar)
  2. 平时通过inotify监控WAL目录,有新文件就rsync到备份服务器
  3. 恢复时解压base.tar,然后应用WAL日志

这个方案的好处:

  • 简单: 不需要第三方工具,不需要复杂的脚本
  • 可靠: 全是PostgreSQL官方机制
  • 粒度细: WAL文件16MB,最多丢几秒钟数据
  • 省空间: 只有base.tar和WAL文件,没有重复

PostgreSQL的WAL归档机制

再画个图说清楚WAL归档的原理:

sequenceDiagram
    participant PG as PostgreSQL进程
    participant WAL as WAL写入进程
    participant File as WAL文件
    participant Archive as 归档进程
    participant Backup as 备份目录
    
    PG->>WAL: 写入事务日志
    WAL->>File: 写满16MB
    File->>WAL: 切换新文件
    WAL->>Archive: 触发archive_command
    Archive->>Backup: cp/rsync到归档目录
    Archive->>PG: 返回成功
    
    Note over PG,Backup: 失败会不断重试,直到成功

关键配置:

# postgresql.conf
wal_level = replica
archive_mode = on
archive_command = 'test ! -f /data/db-backup/walbackup/%f && cp %p /data/db-backup/walbackup/%f'

archive_command的几个要点:

  • %p: WAL文件的完整路径
  • %f: WAL文件名
  • test ! -f: 确保不会覆盖已存在的文件(很重要)
  • 命令返回0表示成功,非0表示失败会重试

基础备份:pg_basebackup

每周日凌晨执行一次基础备份:

#!/bin/bash
# 基础备份脚本

BACKUP_DIR="/data/db-backup/basebackup"
DATE=$(date +%Y%m%d)
BACKUP_FILE="${BACKUP_DIR}/base_${DATE}.tar.gz"

# 创建备份目录
mkdir -p ${BACKUP_DIR}

# 执行备份,压缩格式
pg_basebackup -h 192.168.1.100 -U postgres \
  -D - -Ft -z -Xs -P \
  > ${BACKUP_FILE}

# 验证备份文件
if [ $? -eq 0 ] && [ -f ${BACKUP_FILE} ]; then
    echo "基础备份成功: ${BACKUP_FILE}"
    
    # 同步到备份服务器
    rsync -avz ${BACKUP_FILE} postgres@192.168.1.101:${BACKUP_DIR}/
    
    # 删除7天前的备份
    find ${BACKUP_DIR} -name "base_*.tar.gz" -mtime +7 -delete
else
    echo "基础备份失败!"
    exit 1
fi

参数说明:

  • -D -: 输出到stdout
  • -Ft: tar格式
  • -z: gzip压缩
  • -Xs: 流式备份,包含备份期间的WAL
  • -P: 显示进度

WAL实时同步:inotify + rsync

这是整个方案的核心,监控WAL目录,实时同步:

#!/bin/bash
# WAL实时同步脚本

WAL_DIR="/data/db-backup/walbackup"
REMOTE_HOST="192.168.1.101"
REMOTE_DIR="/data/db-backup/walbackup"

# 安装inotify-tools
# yum install -y inotify-tools

# 初次同步所有文件
rsync -az ${WAL_DIR}/ postgres@${REMOTE_HOST}:${REMOTE_DIR}/

# 监控目录变化
inotifywait -m -e close_write,moved_to ${WAL_DIR} |
while read path action file; do
    # 只同步WAL文件
    if [[ "$file" =~ ^[0-9A-F]{24}$ ]] || [[ "$file" =~ \.backup$ ]]; then
        echo "$(date '+%Y-%m-%d %H:%M:%S') - 检测到新文件: $file"
        
        # 实时同步
        rsync -az ${path}${file} postgres@${REMOTE_HOST}:${REMOTE_DIR}/
        
        if [ $? -eq 0 ]; then
            echo "同步成功: $file"
        else
            echo "同步失败: $file"
        fi
    fi
done

inotify的几个事件:

  • close_write: 文件写入完成并关闭
  • moved_to: 文件被移动到监控目录(PostgreSQL会先写临时文件再rename)

恢复流程

假设现在要恢复到2024-12-25 15:30:00:

#!/bin/bash
# 恢复脚本

TARGET_TIME="2024-12-25 15:30:00"
BASE_BACKUP="/data/db-backup/basebackup/base_20241222.tar.gz"
WAL_DIR="/data/db-backup/walbackup"
PGDATA="/data/postgres/data"

# 1. 停止数据库
pg_ctl -D ${PGDATA} stop

# 2. 清理数据目录
rm -rf ${PGDATA}/*

# 3. 解压基础备份
echo "解压基础备份..."
tar -xzf ${BASE_BACKUP} -C ${PGDATA}

# 4. 创建恢复配置
cat > ${PGDATA}/recovery.signal << EOF
# 这个文件表示要进入恢复模式
EOF

cat >> ${PGDATA}/postgresql.conf << EOF
restore_command = 'cp ${WAL_DIR}/%f %p'
recovery_target_time = '${TARGET_TIME}'
recovery_target_action = 'promote'
EOF

# 5. 启动数据库,自动进入恢复模式
echo "启动恢复..."
pg_ctl -D ${PGDATA} start

# 6. 等待恢复完成
while [ -f ${PGDATA}/recovery.signal ]; do
    echo "恢复中..."
    sleep 5
done

echo "恢复完成!"

恢复过程:

  1. PostgreSQL启动后,检测到recovery.signal文件
  2. 从基础备份开始,按顺序应用WAL日志
  3. 应用到recovery_target_time指定的时间点
  4. 自动promote成正常运行状态
  5. 删除recovery.signal文件

方案对比

现在两个方案都很清楚了,对比一下:

存储开销:

  • pg_rman: 每天4-5GB (自己的文件2-3GB + WAL 1-2GB)
  • pg_basebackup: 每天1-2GB (只有WAL)
  • 基础备份50GB一周一次,摊到每天7GB

算下来,两周的存储:

  • pg_rman: (4-5GB × 7天 + 50GB) × 2 = 156-170GB
  • pg_basebackup: (1-2GB × 7天 + 50GB) × 2 = 114-128GB

省了30-40GB,大概25%的空间。

恢复粒度:

  • pg_rman: 每小时一次增量备份,最多丢1小时数据
  • pg_basebackup: WAL文件16MB,几秒钟的数据

这个差距就很大了。而且pg_basebackup理论上可以恢复到任意时间点(只要WAL文件还在),pg_rman只能恢复到增量备份的时间点。

可靠性:

  • pg_rman: 第三方工具,维护状态不明
  • pg_basebackup: PostgreSQL官方工具,随版本更新

这个没得比,肯定选官方的。

复杂度:

  • pg_rman: 需要配置环境变量、解析命令输出、处理复杂的目录结构
  • pg_basebackup: 标准的tar包 + WAL文件,简单直接

从维护角度看,简单就是可靠。

实施过程中的几个坑

切换到新方案后,也不是一帆风顺,还是遇到了几个问题。

坑一:SSH免密登录

rsync要能自动同步,得先配置SSH免密登录:

# 在主库生成密钥对
ssh-keygen -t rsa -b 2048

# 拷贝公钥到备份服务器
ssh-copy-id postgres@192.168.1.101

# 测试
ssh postgres@192.168.1.101 "echo 'SSH连接成功'"

注意要用postgres用户操作,不要用root。我一开始用root配的,结果脚本跑不起来。

坑二:WAL文件数量太多

监控了一周发现,WAL目录里的文件越来越多,有几千个文件了。这会导致:

  • 目录遍历慢
  • rsync初次同步很慢
  • 磁盘inode可能耗尽

解决办法是定期清理历史WAL文件:

#!/bin/bash
# WAL清理脚本,保留7天

WAL_DIR="/data/db-backup/walbackup"

# 找出最后一次基础备份需要的最早WAL文件
# 从base.backup文件读取
BACKUP_FILE="${WAL_DIR}/000000010000000000000001.00000028.backup"
if [ -f ${BACKUP_FILE} ]; then
    START_WAL=$(grep "START WAL LOCATION" ${BACKUP_FILE} | awk '{print $6}' | sed 's/[()]//g')
    echo "最早需要的WAL: ${START_WAL}"
fi

# 删除7天前的WAL文件
find ${WAL_DIR} -name "0*" -mtime +7 -delete

echo "WAL清理完成"

更优雅的做法是用pg_archivecleanup,它会自动计算可以删除的WAL:

pg_archivecleanup /data/db-backup/walbackup 000000010000000000000001

坑三:inotify监控的文件数量限制

inotify默认的监控数量有限制:

# 查看当前限制
cat /proc/sys/fs/inotify/max_user_watches

# 修改限制
echo 524288 | sudo tee /proc/sys/fs/inotify/max_user_watches

# 永久生效
echo "fs.inotify.max_user_watches=524288" | sudo tee -a /etc/sysctl.conf
sudo sysctl -p

如果WAL文件太多,inotify可能会报错"Too many open files"。

坑四:archive_command执行失败

有一次发现主库的pg_wal目录爆满,检查日志发现archive_command一直失败:

WARNING: archiving write-ahead log file "00000001000000000000007F" failed too many times, will try again later

原因是归档目录的磁盘满了。archive_command失败后,PostgreSQL会不断重试,同时WAL文件会堆积在pg_wal目录里。

解决办法:

  1. 清理归档目录的历史文件
  2. 手动执行archive_command,处理积压的WAL
  3. 监控归档目录的磁盘使用率

这个要特别注意,如果归档失败时间太长,pg_wal目录满了,数据库会停止写入。

生产环境的完整方案

最后把整个方案整理一下,这是我们实际在用的:

目录规划

/data/db-backup/
├── basebackup/          # 基础备份
│   ├── base_20241222.tar.gz
│   └── base_20241229.tar.gz
├── walbackup/           # WAL归档
│   ├── 000000010000000000000001
│   ├── 000000010000000000000002
│   └── ...
└── scripts/             # 脚本
    ├── basebackup.sh
    ├── wal_sync.sh
    └── recovery.sh

定时任务

# 每周日凌晨2点做基础备份
0 2 * * 0 /data/db-backup/scripts/basebackup.sh >> /var/log/pg_backup.log 2>&1

# 每天凌晨3点清理7天前的WAL文件
0 3 * * * pg_archivecleanup /data/db-backup/walbackup $(ls -t /data/db-backup/basebackup/*.backup | head -1) >> /var/log/pg_wal_cleanup.log 2>&1

# WAL实时同步由systemd管理,开机自启
# systemctl enable pg_wal_sync.service

监控指标

需要监控这几个指标:

  1. WAL生成速度: 每小时生成多少个WAL文件
  2. 归档延迟: archive_command的平均耗时
  3. 同步延迟: 备份服务器的WAL文件是否及时同步
  4. 磁盘使用率: 归档目录的空间是否充足

可以用简单的脚本监控:

#!/bin/bash
# 监控脚本

WAL_DIR="/data/db-backup/walbackup"

# 统计最近1小时的WAL文件数
WAL_COUNT=$(find ${WAL_DIR} -name "0*" -mmin -60 | wc -l)
echo "最近1小时WAL文件数: ${WAL_COUNT}"

# 检查最新WAL文件的时间
LATEST_WAL=$(ls -t ${WAL_DIR}/0* | head -1)
LATEST_TIME=$(stat -c %Y ${LATEST_WAL})
CURRENT_TIME=$(date +%s)
DELAY=$((CURRENT_TIME - LATEST_TIME))

if [ ${DELAY} -gt 300 ]; then
    echo "警告: WAL同步延迟 ${DELAY} 秒!"
fi

# 检查磁盘使用率
DISK_USAGE=$(df -h ${WAL_DIR} | tail -1 | awk '{print $5}' | sed 's/%//')
if [ ${DISK_USAGE} -gt 80 ]; then
    echo "警告: 归档目录磁盘使用率 ${DISK_USAGE}%"
fi

恢复演练

这个很重要: 备份方案一定要定期演练,不然等真出事了才发现恢复不了,那就惨了。

我们每个月会在测试环境做一次完整的恢复演练:

  1. 从备份服务器拷贝基础备份和WAL文件
  2. 恢复到上周的某个随机时间点
  3. 检查数据一致性,对比几个关键表的记录数
  4. 记录恢复耗时

实际测试下来,恢复50GB的数据库到上周的时间点,大概需要20-30分钟。主要时间花在解压base.tar和应用WAL上。

回头看这个决策过程

现在回头看,当初选pg_rman其实是被"增量备份"这个概念误导了。

我们的需求本质上是:能恢复到尽可能近的时间点,同时备份文件要可控

PostgreSQL的PITR机制天生就能满足这个需求:

  • 基础备份提供恢复的起点
  • WAL日志提供细粒度的恢复能力
  • 两者结合,可以恢复到任意时间点

pg_rman的"增量备份",实际上是在PITR之上又加了一层:把变化的数据块定期打包。但这带来的好处很有限:

  • 恢复速度?差不多,因为最终还是要应用WAL
  • 存储空间?反而更大,因为既有自己的文件,又有WAL
  • 恢复粒度?反而更粗,因为只能恢复到增量备份的时间点

所以pg_rman的增量备份,对我们来说就是多余的复杂度。

几个要注意的地方

最后总结几个经验教训:

1. 理解需求的本质

不要被工具的功能牵着走。我们的需求是"增量备份",但这不代表一定要用有"增量备份"功能的工具。理解PostgreSQL的备份恢复原理后,会发现很多工具都是在做相同的事,只是包装不同。

2. 优先选择官方工具

第三方工具可能有更多功能,但可靠性和维护性是问题。除非官方工具真的满足不了需求,否则不要轻易引入第三方依赖。

3. 简单就是可靠

复杂的方案意味着更多的出错点。pg_rman要配环境变量、解析输出、处理复杂目录,每一步都可能出问题。相比之下,tar包+WAL文件,简单到不会出错。

4. 一定要做恢复演练

备份做了不等于能恢复。我们之前遇到过好几次恢复时才发现的问题:

  • 环境不一致(插件、配置)
  • 文件权限不对
  • 磁盘空间不够
  • 恢复时间超预期

这些问题只有真正恢复一次才会发现。

5. 监控比备份更重要

再好的备份方案,如果不知道它是否正常工作,也没用。监控这几个关键点:

  • 备份任务是否执行成功
  • WAL文件是否及时归档
  • 备份文件是否及时同步
  • 磁盘空间是否充足

写在最后

这个pg_rman的坑,我前前后后折腾了快两个月。现在想想,如果当初多问几个"为什么",可能就不会走弯路了:

  • 为什么需要增量备份?是为了省空间还是省时间?
  • PostgreSQL自带的机制能不能满足需求?
  • 第三方工具解决了什么问题?引入了什么复杂度?

技术选型不只是比较功能列表,更重要的是理解需求的本质,评估方案的权衡。

现在我们的备份方案很简单:一个base.tar + 一堆WAL文件。但它可靠、可控、可理解。这比一个功能强大但你搞不懂的工具要好得多。

希望这篇文章能帮到其他正在选备份方案的朋友。如果你也在考虑用pg_rman,先想想是不是真的需要它。


P.S.: 写这篇文章的时候,我又去看了一眼pg_rman的GitHub仓库,上一次提交是2022年。果然,这个项目基本没人维护了。庆幸当时及时止损,没有继续往坑里跳。

最后聊两句

写这篇文章花了我一个晚上,把当时踩过的坑都翻出来重新整理了一遍。说实话,有些细节我都快忘了,翻日志才想起来。

如果这篇文章对你有帮助,麻烦点个赞让我知道。我后面还想写几篇PostgreSQL相关的实战文章:

  • 主从复制的几种方案对比
  • pg_stat_statements的性能分析实践
  • 连接池的选择和配置(pgbouncer vs pgpool)

你们想看哪个?可以在评论区留言。

另外,如果你也在用pg_rman或者有其他备份方案,欢迎交流。我这个方案也不敢说是最优的,肯定还有可以改进的地方。比如:

  • 流复制是不是比WAL归档更好?
  • 压缩算法能不能优化?
  • 有没有更好的监控方案?

技术这东西,就是要多交流才能进步。不要憋着自己研究,遇到问题多问问老司机,能少走很多弯路。我当初要是早点问赵老师,也不至于折腾两个月。

好了,不多说了,如果觉得有收获就点个赞吧,你的认可是我继续分享的动力!

有问题欢迎评论区留言,我看到会回复的。