Appearance
PostgreSQL 连续归档和时间点恢复 (PITR) 完全指南
概述
PostgreSQL 的连续归档和时间点恢复 (Point-in-Time Recovery, PITR) 是一种高级备份策略,它通过结合文件系统级别备份和预写式日志 (WAL) 文件来实现数据库的连续保护和任意时间点恢复。
解决的业务问题
在实际生产环境中,传统的备份方式存在以下限制:
TIP
业务场景示例假设你运营一个电商网站:
- 传统备份:每天凌晨做一次完整备份,如果下午发生数据损坏,最多丢失一天的交易数据
- PITR 方案:可以恢复到任意时间点,比如恢复到故障发生前的最后一秒,几乎零数据丢失
PITR 的核心优势
WAL 归档设置
基本概念
WAL (Write-Ahead Logging) 是 PostgreSQL 的核心机制,它记录了对数据库的每一项更改。
配置 WAL 归档
第一步:基础配置
编辑 postgresql.conf
文件:
bash
# 设置 WAL 级别(支持归档和流复制)
wal_level = replica # 最小级别为 replica
# 启用归档模式
archive_mode = on # 启用 WAL 归档
# 设置归档命令
archive_command = 'test ! -f /backup/archive/%f && cp %p /backup/archive/%f'
# 可选:设置归档超时(防止长时间无活动)
archive_timeout = 300 # 5分钟强制切换 WAL 段
Details
配置参数详解
%p
:要归档文件的完整路径%f
:要归档文件的文件名test ! -f
:检查目标文件是否已存在,避免覆盖&&
:只有测试成功才执行复制命令
第二步:创建归档目录
bash
# 创建归档目录
sudo mkdir -p /backup/archive
sudo mkdir -p /backup/basebackup
# 设置权限(使用 postgres 用户)
sudo chown postgres:postgres /backup/archive
sudo chown postgres:postgres /backup/basebackup
sudo chmod 700 /backup/archive
sudo chmod 700 /backup/basebackup
第三步:高级归档命令示例
bash
# 简单本地归档
archive_command = 'test ! -f /backup/archive/%f && cp %p /backup/archive/%f'
bash
# 通过 rsync 远程归档
archive_command = 'test ! -f /backup/archive/%f && rsync -a %p backup-server:/backup/archive/%f'
bash
# 压缩后归档
archive_command = 'test ! -f /backup/archive/%f.gz && gzip -c %p > /backup/archive/%f.gz'
bash
# 上传到 AWS S3
archive_command = 'aws s3 cp %p s3://my-backup-bucket/wal/%f'
监控归档状态
sql
-- 查看归档状态
SELECT
archived_count, -- 已归档的文件数
last_archived_wal, -- 最后归档的 WAL 文件
last_archived_time, -- 最后归档时间
failed_count, -- 归档失败次数
last_failed_wal, -- 最后失败的 WAL 文件
last_failed_time -- 最后失败时间
FROM pg_stat_archiver;
WARNING
归档监控重要性如果归档失败,pg_wal/
目录将持续填充未归档的 WAL 文件,可能导致磁盘空间耗尽和数据库崩溃。
制作基础备份
使用 pg_basebackup 工具
pg_basebackup
是最简单的基础备份方法:
基本用法
bash
# 创建基础备份
pg_basebackup \
-h localhost \ # 数据库主机
-p 5432 \ # 端口
-U postgres \ # 用户名
-D /backup/basebackup/$(date +%Y%m%d_%H%M%S) \ # 备份目录
-Ft \ # tar 格式
-z \ # 启用压缩
-P \ # 显示进度
-v # 详细输出
高级备份选项
bash
# 标准备份到目录
pg_basebackup -D /backup/base_$(date +%Y%m%d_%H%M%S) -Fp -Xs -P -v
bash
# 创建压缩的 tar 备份
pg_basebackup -D /backup -Ft -z -Xs -P -v --label="daily_backup_$(date +%Y%m%d)"
bash
# 排除某些目录的备份
pg_basebackup -D /backup/base -Fp -Xs -P -v \
--exclude-dir=log \
--exclude-dir=tmp
备份脚本示例
创建一个完整的备份脚本:
bash
#!/bin/bash
# 文件:backup_script.sh
# 配置变量
BACKUP_DIR="/backup"
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_PATH="${BACKUP_DIR}/base_${DATE}"
LOG_FILE="${BACKUP_DIR}/backup_${DATE}.log"
# 创建备份目录
mkdir -p "${BACKUP_PATH}"
# 执行备份
echo "开始备份: $(date)" | tee -a "${LOG_FILE}"
pg_basebackup \
-h localhost \
-p 5432 \
-U postgres \
-D "${BACKUP_PATH}" \
-Fp \ # 目录格式
-Xs \ # 流式传输 WAL
-P \ # 显示进度
-v \ # 详细输出
--label="backup_${DATE}" \ # 备份标签
2>&1 | tee -a "${LOG_FILE}"
# 检查备份结果
if [ $? -eq 0 ]; then
echo "备份成功完成: $(date)" | tee -a "${LOG_FILE}"
# 创建备份信息文件
cat > "${BACKUP_PATH}/backup_info.txt" << EOF
备份时间: $(date)
备份大小: $(du -sh "${BACKUP_PATH}" | cut -f1)
备份标签: backup_${DATE}
PostgreSQL 版本: $(psql -h localhost -p 5432 -U postgres -t -c "SELECT version();" 2>/dev/null)
EOF
else
echo "备份失败: $(date)" | tee -a "${LOG_FILE}"
exit 1
fi
# 清理旧备份(保留7天)
find "${BACKUP_DIR}" -name "base_*" -type d -mtime +7 -exec rm -rf {} \;
find "${BACKUP_DIR}" -name "backup_*.log" -mtime +7 -delete
echo "备份脚本执行完成: $(date)" | tee -a "${LOG_FILE}"
备份验证
sql
-- 验证备份历史
SELECT
file_name,
backup_label,
start_time,
end_time
FROM pg_backup_history
ORDER BY start_time DESC
LIMIT 5;
增量备份
增量备份只备份自上次备份以来发生变化的数据块,大大减少备份时间和存储空间。
增量备份原理
创建增量备份
bash
# 第一次:创建完整备份
pg_basebackup -D /backup/full_backup -Fp -Xs -P -v
# 后续:创建增量备份
pg_basebackup \
--incremental=/backup/full_backup/backup_manifest \ # 基于的完整备份
-D /backup/incremental_$(date +%Y%m%d_%H%M%S) \
-Fp -Xs -P -v
增量备份管理脚本
bash
#!/bin/bash
# 增量备份管理脚本
BACKUP_ROOT="/backup"
FULL_BACKUP_DIR="${BACKUP_ROOT}/full"
INCR_BACKUP_DIR="${BACKUP_ROOT}/incremental"
DATE=$(date +%Y%m%d_%H%M%S)
# 检查是否需要完整备份(每周日或不存在完整备份)
if [ $(date +%u) -eq 7 ] || [ ! -d "${FULL_BACKUP_DIR}" ]; then
echo "执行完整备份..."
# 清理旧的完整备份
[ -d "${FULL_BACKUP_DIR}" ] && rm -rf "${FULL_BACKUP_DIR}"
[ -d "${INCR_BACKUP_DIR}" ] && rm -rf "${INCR_BACKUP_DIR}"
# 创建新的完整备份
mkdir -p "${FULL_BACKUP_DIR}"
pg_basebackup -D "${FULL_BACKUP_DIR}" -Fp -Xs -P -v
echo "完整备份完成"
else
echo "执行增量备份..."
# 创建增量备份目录
INCR_DIR="${INCR_BACKUP_DIR}/incr_${DATE}"
mkdir -p "${INCR_DIR}"
# 执行增量备份
pg_basebackup \
--incremental="${FULL_BACKUP_DIR}/backup_manifest" \
-D "${INCR_DIR}" \
-Fp -Xs -P -v
echo "增量备份完成: ${INCR_DIR}"
fi
使用底层 API 制作基础备份
对于需要更精细控制的场景,可以使用 PostgreSQL 的底层备份 API。
备份流程
详细步骤实现
第一步:启动备份模式
sql
-- 启动备份模式
SELECT pg_backup_start(
label => 'manual_backup_' || to_char(now(), 'YYYY-MM-DD_HH24:MI:SS'),
fast => false -- true: 立即检查点,false: 等待下一个检查点
);
第二步:执行文件系统备份
bash
#!/bin/bash
# 文件系统备份脚本
PGDATA="/var/lib/postgresql/14/main" # PostgreSQL 数据目录
BACKUP_DIR="/backup/manual_$(date +%Y%m%d_%H%M%S)"
# 创建备份目录
mkdir -p "${BACKUP_DIR}"
# 使用 tar 进行备份,排除不必要的文件
tar -cf "${BACKUP_DIR}/base.tar" \
--exclude="pg_wal/*" \ # 排除 WAL 文件
--exclude="pg_replslot/*" \ # 排除复制槽
--exclude="postmaster.pid" \ # 排除进程 ID 文件
--exclude="postmaster.opts" \ # 排除启动选项文件
--exclude="pg_stat_tmp/*" \ # 排除临时统计文件
--exclude="pgsql_tmp*" \ # 排除临时文件
-C "${PGDATA}" .
echo "文件系统备份完成: ${BACKUP_DIR}/base.tar"
第三步:结束备份模式
sql
-- 结束备份模式并获取备份信息
SELECT
lsn, -- LSN 位置
labelfile, -- backup_label 文件内容
spcmapfile -- tablespace_map 文件内容
FROM pg_backup_stop(wait_for_archive => true);
第四步:保存备份元数据
bash
#!/bin/bash
# 保存备份元数据脚本
# 从 pg_backup_stop 的结果中获取信息
BACKUP_LABEL="$1" # backup_label 文件内容
TABLESPACE_MAP="$2" # tablespace_map 文件内容(可能为空)
# 保存 backup_label 文件
echo "${BACKUP_LABEL}" > "${BACKUP_DIR}/backup_label"
# 如果有表空间映射,保存它
if [ -n "${TABLESPACE_MAP}" ]; then
echo "${TABLESPACE_MAP}" > "${BACKUP_DIR}/tablespace_map"
fi
echo "备份元数据已保存"
完整的底层 API 备份脚本
bash
#!/bin/bash
# 完整的底层 API 备份脚本
set -e # 遇到错误时退出
# 配置
PGDATA="/var/lib/postgresql/14/main"
BACKUP_ROOT="/backup"
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_DIR="${BACKUP_ROOT}/manual_${DATE}"
LOG_FILE="${BACKUP_ROOT}/backup_${DATE}.log"
# 日志函数
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "${LOG_FILE}"
}
# 创建备份目录
mkdir -p "${BACKUP_DIR}"
log "开始手动备份流程"
# 第一步:启动备份模式
log "启动备份模式..."
BACKUP_LABEL="manual_backup_${DATE}"
psql -h localhost -p 5432 -U postgres -d postgres -t -c \
"SELECT pg_backup_start('${BACKUP_LABEL}', false);" >> "${LOG_FILE}"
if [ $? -ne 0 ]; then
log "启动备份模式失败"
exit 1
fi
log "备份模式已启动"
# 第二步:执行文件系统备份
log "开始文件系统备份..."
tar -cf "${BACKUP_DIR}/base.tar" \
--exclude="pg_wal/*" \
--exclude="pg_replslot/*" \
--exclude="postmaster.pid" \
--exclude="postmaster.opts" \
--exclude="pg_stat_tmp/*" \
--exclude="pgsql_tmp*" \
-C "${PGDATA}" . 2>> "${LOG_FILE}"
if [ $? -ne 0 ]; then
log "文件系统备份失败"
# 尝试停止备份模式
psql -h localhost -p 5432 -U postgres -d postgres -c \
"SELECT pg_backup_stop(false);" >> "${LOG_FILE}" 2>&1
exit 1
fi
log "文件系统备份完成"
# 第三步:停止备份模式并获取元数据
log "停止备份模式..."
BACKUP_INFO=$(psql -h localhost -p 5432 -U postgres -d postgres -t -c \
"SELECT lsn, labelfile, spcmapfile FROM pg_backup_stop(true);" 2>> "${LOG_FILE}")
if [ $? -ne 0 ]; then
log "停止备份模式失败"
exit 1
fi
# 解析备份信息
IFS='|' read -r LSN LABELFILE SPCMAPFILE <<< "${BACKUP_INFO}"
# 保存 backup_label 文件
echo "${LABELFILE}" > "${BACKUP_DIR}/backup_label"
# 保存 tablespace_map 文件(如果存在)
if [ -n "${SPCMAPFILE}" ] && [ "${SPCMAPFILE}" != " " ]; then
echo "${SPCMAPFILE}" > "${BACKUP_DIR}/tablespace_map"
fi
# 创建备份信息文件
cat > "${BACKUP_DIR}/backup_info.txt" << EOF
备份时间: $(date)
备份标签: ${BACKUP_LABEL}
备份 LSN: ${LSN}
备份大小: $(du -sh "${BACKUP_DIR}" | cut -f1)
PostgreSQL 版本: $(psql -h localhost -p 5432 -U postgres -t -c "SELECT version();" 2>/dev/null)
备份类型: 手动底层 API 备份
EOF
log "备份完成: ${BACKUP_DIR}"
log "备份 LSN: ${LSN}"
log "备份大小: $(du -sh "${BACKUP_DIR}" | cut -f1)"
使用连续归档备份进行恢复
恢复场景分析
在实际业务中,可能遇到以下恢复场景:
TIP
常见恢复场景
- 硬件故障:服务器硬盘损坏,需要完全恢复
- 误操作:错误删除了重要数据,需要恢复到误操作前
- 数据损坏:数据文件损坏,需要从备份恢复
- 灾难恢复:整个数据中心不可用,需要在新环境恢复
恢复流程
基础恢复步骤
第一步:准备恢复环境
bash
#!/bin/bash
# 恢复环境准备脚本
PGDATA="/var/lib/postgresql/14/main"
BACKUP_DIR="/backup/base_20231201_143000"
# 停止 PostgreSQL 服务
sudo systemctl stop postgresql
# 备份当前数据目录(如果存在)
if [ -d "${PGDATA}" ]; then
sudo mv "${PGDATA}" "${PGDATA}.bak.$(date +%Y%m%d_%H%M%S)"
fi
# 创建新的数据目录
sudo mkdir -p "${PGDATA}"
sudo chown postgres:postgres "${PGDATA}"
sudo chmod 700 "${PGDATA}"
echo "恢复环境准备完成"
第二步:恢复基础备份
bash
#!/bin/bash
# 恢复基础备份
# 解压备份(根据备份格式)
if [ -f "${BACKUP_DIR}/base.tar" ]; then
# tar 格式备份
sudo tar -xf "${BACKUP_DIR}/base.tar" -C "${PGDATA}"
elif [ -d "${BACKUP_DIR}" ]; then
# 目录格式备份
sudo cp -r "${BACKUP_DIR}"/* "${PGDATA}/"
fi
# 恢复备份标签文件
if [ -f "${BACKUP_DIR}/backup_label" ]; then
sudo cp "${BACKUP_DIR}/backup_label" "${PGDATA}/"
fi
# 恢复表空间映射文件
if [ -f "${BACKUP_DIR}/tablespace_map" ]; then
sudo cp "${BACKUP_DIR}/tablespace_map" "${PGDATA}/"
fi
# 设置权限
sudo chown -R postgres:postgres "${PGDATA}"
echo "基础备份恢复完成"
第三步:配置恢复参数
bash
# 编辑 postgresql.conf
cat >> "${PGDATA}/postgresql.conf" << EOF
# 恢复配置
restore_command = 'cp /backup/archive/%f %p' # 从归档获取 WAL
recovery_target_time = '2023-12-01 15:30:00' # 目标恢复时间点
recovery_target_action = 'promote' # 恢复后晋升为主库
EOF
Details
恢复目标选项
recovery_target_time
:恢复到指定时间点recovery_target_xid
:恢复到指定事务 IDrecovery_target_lsn
:恢复到指定 LSN 位置recovery_target_name
:恢复到指定的恢复点recovery_target_immediate
:恢复到备份完成点
第四步:创建恢复信号文件
bash
# 创建 recovery.signal 文件(PostgreSQL 12+)
touch "${PGDATA}/recovery.signal"
# 对于 PostgreSQL 11 及更早版本,需要创建 recovery.conf
# 并将恢复配置写入该文件而不是 postgresql.conf
第五步:启动恢复过程
bash
# 启动 PostgreSQL
sudo systemctl start postgresql
# 监控恢复过程
tail -f /var/log/postgresql/postgresql-14-main.log
高级恢复配置
时间点恢复示例
sql
-- 在 postgresql.conf 中配置
restore_command = 'cp /backup/archive/%f %p'
-- 恢复到特定时间点(在应用错误的 DROP TABLE 之前)
recovery_target_time = '2023-12-01 14:25:30'
recovery_target_action = 'promote'
-- 恢复到特定事务(已知好的事务 ID)
-- recovery_target_xid = '12345678'
-- 恢复到备份完成时立即停止
-- recovery_target_immediate = on
恢复脚本完整示例
bash
#!/bin/bash
# 完整的 PITR 恢复脚本
set -e
# 配置参数
PGDATA="/var/lib/postgresql/14/main"
BACKUP_DIR="/backup/base_20231201_143000"
ARCHIVE_DIR="/backup/archive"
TARGET_TIME="2023-12-01 15:30:00"
LOG_FILE="/tmp/recovery_$(date +%Y%m%d_%H%M%S).log"
# 日志函数
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "${LOG_FILE}"
}
log "开始 PITR 恢复过程"
# 第一步:准备恢复环境
log "准备恢复环境..."
sudo systemctl stop postgresql
if [ -d "${PGDATA}" ]; then
BACKUP_SUFFIX=$(date +%Y%m%d_%H%M%S)
log "备份现有数据目录到 ${PGDATA}.bak.${BACKUP_SUFFIX}"
sudo mv "${PGDATA}" "${PGDATA}.bak.${BACKUP_SUFFIX}"
fi
sudo mkdir -p "${PGDATA}"
sudo chown postgres:postgres "${PGDATA}"
sudo chmod 700 "${PGDATA}"
# 第二步:恢复基础备份
log "恢复基础备份..."
if [ -f "${BACKUP_DIR}/base.tar" ]; then
sudo tar -xf "${BACKUP_DIR}/base.tar" -C "${PGDATA}"
elif [ -d "${BACKUP_DIR}" ]; then
sudo cp -r "${BACKUP_DIR}"/* "${PGDATA}/"
else
log "错误:找不到备份文件"
exit 1
fi
# 恢复元数据文件
[ -f "${BACKUP_DIR}/backup_label" ] && sudo cp "${BACKUP_DIR}/backup_label" "${PGDATA}/"
[ -f "${BACKUP_DIR}/tablespace_map" ] && sudo cp "${BACKUP_DIR}/tablespace_map" "${PGDATA}/"
# 第三步:配置恢复参数
log "配置恢复参数..."
cat >> "${PGDATA}/postgresql.conf" << EOF
# PITR 恢复配置
restore_command = 'cp ${ARCHIVE_DIR}/%f %p'
recovery_target_time = '${TARGET_TIME}'
recovery_target_action = 'promote'
# 恢复相关日志
log_min_messages = debug1
log_line_prefix = '%t [%p]: [%l-1] user=%u,db=%d,app=%a,client=%h '
EOF
# 第四步:创建恢复信号文件
log "创建恢复信号文件..."
sudo touch "${PGDATA}/recovery.signal"
# 设置权限
sudo chown -R postgres:postgres "${PGDATA}"
# 第五步:启动恢复
log "启动 PostgreSQL 进行恢复..."
sudo systemctl start postgresql
# 监控恢复进度
log "监控恢复进度..."
sleep 5
# 检查恢复状态
while true; do
if sudo -u postgres psql -d postgres -t -c "SELECT pg_is_in_recovery();" 2>/dev/null | grep -q "f"; then
log "恢复完成!数据库已晋升为主库"
break
elif ! sudo systemctl is-active --quiet postgresql; then
log "PostgreSQL 服务异常,请检查日志"
exit 1
else
log "恢复进行中..."
sleep 10
fi
done
# 验证恢复结果
log "验证恢复结果..."
CURRENT_TIME=$(sudo -u postgres psql -d postgres -t -c "SELECT now();" 2>/dev/null)
log "当前数据库时间: ${CURRENT_TIME}"
log "PITR 恢复过程完成"
时间线概念
时间线的作用
PostgreSQL 使用时间线来跟踪数据库历史的分支。每次从备份恢复并晋升为新的主库时,都会创建一个新的时间线。
时间线文件
bash
# 查看时间线历史文件
ls -la /backup/archive/*.history
# 示例文件内容
# 00000002.history
1 0/14000028 no recovery target specified
跨时间线恢复
sql
-- 配置跨时间线恢复
recovery_target_timeline = 'latest' -- 恢复到最新时间线
-- recovery_target_timeline = '2' -- 恢复到特定时间线
实用脚本和工具
完整的备份管理系统
bash
#!/bin/bash
# PostgreSQL 备份管理系统
# 文件:pg_backup_manager.sh
# 配置文件
CONFIG_FILE="/etc/postgresql/backup.conf"
# 默认配置
PGDATA="/var/lib/postgresql/14/main"
BACKUP_ROOT="/backup"
ARCHIVE_DIR="${BACKUP_ROOT}/archive"
BASE_BACKUP_DIR="${BACKUP_ROOT}/base"
LOG_DIR="${BACKUP_ROOT}/logs"
RETENTION_DAYS=7
# 加载配置文件
[ -f "${CONFIG_FILE}" ] && source "${CONFIG_FILE}"
# 创建必要目录
mkdir -p "${BACKUP_ROOT}" "${ARCHIVE_DIR}" "${BASE_BACKUP_DIR}" "${LOG_DIR}"
# 日志函数
log() {
local level="$1"
shift
echo "[$(date '+%Y-%m-%d %H:%M:%S')] [${level}] $*" | tee -a "${LOG_DIR}/backup.log"
}
# 清理函数
cleanup_old_backups() {
log "INFO" "清理超过 ${RETENTION_DAYS} 天的旧备份"
# 清理基础备份
find "${BASE_BACKUP_DIR}" -name "base_*" -type d -mtime +${RETENTION_DAYS} -exec rm -rf {} \;
# 清理归档文件(保留额外的安全边际)
local archive_retention=$((RETENTION_DAYS + 1))
find "${ARCHIVE_DIR}" -name "0*" -type f -mtime +${archive_retention} -delete
# 清理日志文件
find "${LOG_DIR}" -name "*.log" -mtime +30 -delete
}
# 基础备份函数
create_base_backup() {
local backup_type="$1" # full 或 incremental
local backup_dir="${BASE_BACKUP_DIR}/base_$(date +%Y%m%d_%H%M%S)"
log "INFO" "开始创建 ${backup_type} 备份到 ${backup_dir}"
mkdir -p "${backup_dir}"
if [ "${backup_type}" = "incremental" ]; then
# 查找最新的完整备份
local latest_full=$(find "${BASE_BACKUP_DIR}" -name "base_*" -type d | sort | tail -1)
if [ -z "${latest_full}" ] || [ ! -f "${latest_full}/backup_manifest" ]; then
log "WARN" "未找到有效的完整备份,将创建完整备份"
backup_type="full"
fi
fi
if [ "${backup_type}" = "incremental" ]; then
pg_basebackup \
--incremental="${latest_full}/backup_manifest" \
-D "${backup_dir}" \
-Fp -Xs -P -v \
--label="incremental_$(date +%Y%m%d_%H%M%S)" \
2>> "${LOG_DIR}/backup.log"
else
pg_basebackup \
-D "${backup_dir}" \
-Fp -Xs -P -v \
--label="full_$(date +%Y%m%d_%H%M%S)" \
2>> "${LOG_DIR}/backup.log"
fi
if [ $? -eq 0 ]; then
log "INFO" "${backup_type} 备份成功完成"
# 创建备份信息文件
cat > "${backup_dir}/backup_info.json" << EOF
{
"backup_time": "$(date -Iseconds)",
"backup_type": "${backup_type}",
"backup_size": "$(du -sb "${backup_dir}" | cut -f1)",
"postgresql_version": "$(psql -t -c "SELECT version();" 2>/dev/null | xargs)",
"backup_label": "${backup_type}_$(date +%Y%m%d_%H%M%S)"
}
EOF
return 0
else
log "ERROR" "${backup_type} 备份失败"
rm -rf "${backup_dir}"
return 1
fi
}
# 验证备份函数
verify_backup() {
local backup_dir="$1"
log "INFO" "验证备份 ${backup_dir}"
# 检查必需文件
local required_files=("backup_label" "PG_VERSION")
for file in "${required_files[@]}"; do
if [ ! -f "${backup_dir}/${file}" ]; then
log "ERROR" "备份验证失败:缺少文件 ${file}"
return 1
fi
done
# 检查备份大小
local backup_size=$(du -sb "${backup_dir}" | cut -f1)
if [ "${backup_size}" -lt 1000000 ]; then # 小于 1MB 认为异常
log "WARN" "备份大小异常小:${backup_size} 字节"
fi
log "INFO" "备份验证通过"
return 0
}
# 监控归档状态
monitor_archiving() {
log "INFO" "检查 WAL 归档状态"
local archive_stats=$(psql -t -c "
SELECT
archived_count,
failed_count,
EXTRACT(EPOCH FROM (now() - last_archived_time)) as seconds_since_last
FROM pg_stat_archiver;
" 2>/dev/null)
if [ $? -eq 0 ]; then
IFS='|' read -r archived failed last_archive <<< "${archive_stats}"
log "INFO" "归档统计 - 成功: ${archived}, 失败: ${failed}"
if [ "${failed}" -gt 0 ]; then
log "WARN" "检测到归档失败,失败次数: ${failed}"
fi
if [ "${last_archive}" ] && [ "${last_archive}" -gt 3600 ]; then
log "WARN" "上次归档时间超过 1 小时前"
fi
else
log "ERROR" "无法获取归档状态"
fi
}
# 主函数
main() {
case "$1" in
"full")
create_base_backup "full"
;;
"incremental")
create_base_backup "incremental"
;;
"cleanup")
cleanup_old_backups
;;
"verify")
if [ -z "$2" ]; then
log "ERROR" "请指定要验证的备份目录"
exit 1
fi
verify_backup "$2"
;;
"monitor")
monitor_archiving
;;
"status")
monitor_archiving
log "INFO" "最近的备份:"
ls -lt "${BASE_BACKUP_DIR}" | head -5
;;
*)
echo "用法: $0 {full|incremental|cleanup|verify|monitor|status}"
echo ""
echo "命令说明:"
echo " full - 创建完整备份"
echo " incremental - 创建增量备份"
echo " cleanup - 清理旧备份"
echo " verify DIR - 验证指定备份"
echo " monitor - 监控归档状态"
echo " status - 显示备份状态"
exit 1
;;
esac
}
# 执行主函数
main "$@"
最佳实践和注意事项
备份策略建议
TIP
生产环境备份策略
- 完整备份频率:每周一次完整备份
- 增量备份频率:每日增量备份
- WAL 归档:连续归档,archive_timeout 设置为 5-15 分钟
- 异地备份:将备份复制到远程位置
- 定期恢复测试:每月进行一次恢复演练
性能优化
sql
-- 监控备份对性能的影响
SELECT
datname,
numbackends,
xact_commit,
xact_rollback,
blks_read,
blks_hit,
temp_files,
temp_bytes
FROM pg_stat_database
WHERE datname NOT IN ('template0', 'template1');
存储空间管理
bash
#!/bin/bash
# 存储空间监控脚本
# 检查备份目录空间使用
check_backup_space() {
local backup_usage=$(df -h "${BACKUP_ROOT}" | awk 'NR==2 {print $5}' | sed 's/%//')
if [ "${backup_usage}" -gt 80 ]; then
echo "警告:备份目录使用率超过 80% (${backup_usage}%)"
# 自动清理更老的备份
find "${BASE_BACKUP_DIR}" -name "base_*" -type d -mtime +3 -exec rm -rf {} \;
echo "已清理 3 天前的备份"
fi
}
# 预估 WAL 生成速度
estimate_wal_rate() {
local wal_stats=$(psql -t -c "
SELECT
pg_size_pretty(
pg_wal_lsn_diff(pg_current_wal_lsn(), '0/0') /
EXTRACT(EPOCH FROM (now() - pg_postmaster_start_time()))
) || '/sec' as wal_rate
" 2>/dev/null)
echo "WAL 生成速度: ${wal_stats}"
}
错误处理和故障排除
WARNING
常见问题及解决方案
问题 1:归档命令失败
bash
# 检查归档命令权限
ls -la /backup/archive/
sudo chown postgres:postgres /backup/archive/
sudo chmod 755 /backup/archive/
问题 2:恢复过程中 WAL 文件缺失
bash
# 检查归档完整性
pg_waldump /backup/archive/000000010000000000000001
# 查找缺失的 WAL 文件
ls -la /backup/archive/ | grep -E "^-.*00000001.*"
问题 3:恢复时间过长
sql
-- 调整恢复相关参数
max_wal_size = '4GB'
checkpoint_completion_target = 0.9
effective_cache_size = '75% of RAM'
安全考虑
bash
#!/bin/bash
# 备份安全配置脚本
# 设置备份文件权限
secure_backup_permissions() {
local backup_dir="$1"
# 只有 postgres 用户可以访问
chmod 700 "${backup_dir}"
chown -R postgres:postgres "${backup_dir}"
# 设置 ACL 进一步限制访问
setfacl -R -m u:postgres:rwx "${backup_dir}"
setfacl -R -m g::--- "${backup_dir}"
setfacl -R -m o::--- "${backup_dir}"
}
# 加密备份文件
encrypt_backup() {
local backup_file="$1"
local encrypted_file="${backup_file}.gpg"
# 使用 GPG 加密
gpg --symmetric --cipher-algo AES256 --output "${encrypted_file}" "${backup_file}"
# 删除原始文件
rm "${backup_file}"
echo "备份已加密: ${encrypted_file}"
}
总结
PostgreSQL 的连续归档和时间点恢复 (PITR) 是一个强大的备份解决方案,它提供了:
- 数据保护:通过 WAL 归档实现连续的数据保护
- 灵活恢复:支持恢复到任意时间点
- 高可用性:可用于构建热备系统
- 可扩展性:适合各种规模的数据库环境
通过合理配置和定期演练,PITR 可以成为生产环境中数据安全的重要保障。记住要定期测试备份和恢复流程,确保在真正需要时能够快速有效地恢复数据。