Skip to content

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

常见恢复场景

  1. 硬件故障:服务器硬盘损坏,需要完全恢复
  2. 误操作:错误删除了重要数据,需要恢复到误操作前
  3. 数据损坏:数据文件损坏,需要从备份恢复
  4. 灾难恢复:整个数据中心不可用,需要在新环境恢复

恢复流程

基础恢复步骤

第一步:准备恢复环境

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:恢复到指定事务 ID
  • recovery_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

生产环境备份策略

  1. 完整备份频率:每周一次完整备份
  2. 增量备份频率:每日增量备份
  3. WAL 归档:连续归档,archive_timeout 设置为 5-15 分钟
  4. 异地备份:将备份复制到远程位置
  5. 定期恢复测试:每月进行一次恢复演练

性能优化

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) 是一个强大的备份解决方案,它提供了:

  1. 数据保护:通过 WAL 归档实现连续的数据保护
  2. 灵活恢复:支持恢复到任意时间点
  3. 高可用性:可用于构建热备系统
  4. 可扩展性:适合各种规模的数据库环境

通过合理配置和定期演练,PITR 可以成为生产环境中数据安全的重要保障。记住要定期测试备份和恢复流程,确保在真正需要时能够快速有效地恢复数据。