写在前面
去年年底,我们组接了个活,要把数据库备份方案优化一下。当时的情况是这样的:一台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))
这个脚本的问题是:
- 要解析
pg_rman show的输出,很脆弱 - 目录层级复杂,容易写错路径
- 要分两次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自带的工具,至少有官方保证。
问题四:复杂度太高
回头看看我们的方案:
- 配置pg_rman的环境变量
- 配置WAL归档
- 周期性执行全量和增量备份
- 解析pg_rman show的输出
- 根据时间戳定位文件目录
- 分两次rsync同步文件
- 恢复时还要指定recovery-target-time
每一步都可能出错,而且很难排查。比如那个PGDATA环境变量,我当时找了半天才发现。
转折:咨询赵渝强老师
用了一个月pg_rman,我越用越觉得不对劲。正好看到《数据库实战派》的作者赵渝强老师在B站,于是我就把我们的方案发过去请教。
我大概是这么问的:
赵老师,我们现在用pg_rman做增量备份,每周全量一次,每小时增量一次。但感觉有几个问题:
- 存储开销比预期大很多,pg_rman的文件加WAL文件,大概是纯WAL文件的1.5倍
- 增量备份要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
核心思路:
- 周日凌晨用pg_basebackup做一个基础备份(base.tar)
- 平时通过inotify监控WAL目录,有新文件就rsync到备份服务器
- 恢复时解压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 "恢复完成!"
恢复过程:
- PostgreSQL启动后,检测到
recovery.signal文件 - 从基础备份开始,按顺序应用WAL日志
- 应用到
recovery_target_time指定的时间点 - 自动promote成正常运行状态
- 删除
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目录里。
解决办法:
- 清理归档目录的历史文件
- 手动执行archive_command,处理积压的WAL
- 监控归档目录的磁盘使用率
这个要特别注意,如果归档失败时间太长,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
监控指标
需要监控这几个指标:
- WAL生成速度: 每小时生成多少个WAL文件
- 归档延迟: archive_command的平均耗时
- 同步延迟: 备份服务器的WAL文件是否及时同步
- 磁盘使用率: 归档目录的空间是否充足
可以用简单的脚本监控:
#!/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
恢复演练
这个很重要: 备份方案一定要定期演练,不然等真出事了才发现恢复不了,那就惨了。
我们每个月会在测试环境做一次完整的恢复演练:
- 从备份服务器拷贝基础备份和WAL文件
- 恢复到上周的某个随机时间点
- 检查数据一致性,对比几个关键表的记录数
- 记录恢复耗时
实际测试下来,恢复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归档更好?
- 压缩算法能不能优化?
- 有没有更好的监控方案?
技术这东西,就是要多交流才能进步。不要憋着自己研究,遇到问题多问问老司机,能少走很多弯路。我当初要是早点问赵老师,也不至于折腾两个月。
好了,不多说了,如果觉得有收获就点个赞吧,你的认可是我继续分享的动力!
有问题欢迎评论区留言,我看到会回复的。