暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

PostgreSQL 12.5全库物理备份与恢复

skylines 2026-05-06
60
本期简单介绍PostgreSQL 12.5的备份和恢复。备份包括全库备份(零级备份、基本备份)和WAL归档备份(增量备份),都采用定时自动备份。
全库备份
    #!/bin/bash
    # 文件名: pg_full_backup.sh
    # 功能: 使用 pg_basebackup 执行全量物理备份,压缩备份目录并保留元数据文件


    set -euo pipefail


    BACKUP_ROOT="/home/postgres/backupfiles/phybackup"
    PG_HOST="localhost"
    PG_PORT="5432"
    PG_USER="postgres"
    export PGPASSWORD="${PGPASSWORD:-}"          # 建议使用 ~/.pgpass


    TIMESTAMP=$(date +%Y%m%d_%H%M%S)
    BACKUP_DIR="${BACKUP_ROOT}/full_${TIMESTAMP}"
    BACKUP_TAR="${BACKUP_ROOT}/full_${TIMESTAMP}.tar.gz"
    META_FILE="${BACKUP_ROOT}/full_meta_${TIMESTAMP}.txt"


    log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "${BACKUP_ROOT}/full_backup.log"; }


    mkdir -p "$BACKUP_DIR"


    log "开始全量备份 -> $BACKUP_DIR"
    pg_basebackup -h "$PG_HOST" -p "$PG_PORT" -U "$PG_USER" -D "$BACKUP_DIR" \
        --checkpoint=fast --wal-method=stream


    # 获取停止 LSN(全量备份结束后的当前 LSN)
    STOP_LSN=$(psql -h "$PG_HOST" -p "$PG_PORT" -U "$PG_USER" -d postgres \
        -t -c "SELECT pg_current_wal_lsn();" | tr -d ' ')


    if [[ -z "$STOP_LSN" ]]; then
        log "错误: 无法获取当前 LSN"
        exit 1
    fi


    # 写入元数据文件(独立保留)
    cat > "$META_FILE" <<EOF
    FULL_BACKUP_STOP_LSN=$STOP_LSN
    BACKUP_TIME=$TIMESTAMP
    BACKUP_METHOD=pg_basebackup
    BACKUP_TAR_FILE=$(basename "$BACKUP_TAR")
    EOF


    # 压缩备份目录
    log "正在压缩备份目录 -> $BACKUP_TAR"
    tar czf "$BACKUP_TAR" -C "$BACKUP_ROOT" "$(basename "$BACKUP_DIR")"
    if [[ $? -eq 0 ]]; then
        log "压缩完成,删除原始目录 $BACKUP_DIR"
        rm -rf "$BACKUP_DIR"
    else
        log "错误: 压缩失败,保留原始目录"
        exit 1
    fi


    log "全量备份完成,停止 LSN = $STOP_LSN,元数据文件: $META_FILE"


    WAL归档备份

    这个归档备份,需要开启archive_mode,并设置archive_command,设置这两项参数,需要重启数据库才能生效。

      vi postgresql.conf


      archive_mode = on
      archive_command = 'cp %p usr/local/pgsql/data/pg_archive/%f'

      归档备份脚本如下:

        #!/bin/bash
        # 文件名: pg_incr_from_archive.sh
        # 功能: 基于 WAL 归档目录生成增量备份包(累积自上次全量)
        # 改进: 使用 cp 代替 ln,增加详细错误检查,确保打包成功
        set -uo pipefail   # 移除 -e,避免命令失败直接退出,改为手动检查


        # ========== 配置 ==========
        BACKUP_ROOT="/home/postgres/backupfiles/phybackup"
        WAL_ARCHIVE_DIR="/usr/local/pgsql/data/pg_archive"
        PG_HOST="localhost"
        PG_PORT="5444"
        PG_USER="postgres"
        export PGPASSWORD="${PGPASSWORD:-}"


        # ========== 函数 ==========
        log() {
            echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "${BACKUP_ROOT}/incr_backup.log"
        }


        error_exit() {
            log "ERROR: $*"
            exit 1
        }


        # 根据 LSN 获取 WAL 文件名
        lsn_to_wal() {
            psql -h "$PG_HOST" -p "$PG_PORT" -U "$PG_USER" -d postgres -t -c "SELECT pg_walfile_name('$1');" | tr -d ' '
        }


        # 生成两个 WAL 文件名之间的所有文件名(十六进制递增)
        wal_range() {
            local start="$1" end="$2"
            local start_tl="${start:0:8}" start_log="${start:8:8}" start_seg="${start:16:8}"
            local end_tl="${end:0:8}"   end_log="${end:8:8}"   end_seg="${end:16:8}"


            if [[ "$start_tl" != "$end_tl" ]]; then
                log "警告: 起始和结束时间线不同 ($start_tl vs $end_tl),可能无法枚举全部,仅返回起始文件"
                echo "$start"
                return
            fi


            local cur_log=$((16#$start_log))
            local cur_seg=$((16#$start_seg))
            local end_log_val=$((16#$end_log))
            local end_seg_val=$((16#$end_seg))


            while :; do
                printf "%s%08X%08X\n" "$start_tl" $cur_log $cur_seg


                if [[ $cur_log -eq $end_log_val && $cur_seg -eq $end_seg_val ]]; then
                    break
                fi


                ((cur_seg++))
                if ((cur_seg >= 256)); then
                    cur_seg=0
                    ((cur_log++))
                fi


                # 防止无限循环(最多 256*256 个文件)
                if ((cur_log > end_log_val + 10)); then
                    log "警告: 枚举超出范围,提前终止"
                    break
                fi
            done
        }


        # 查找最近一次全量备份的元数据文件
        get_latest_full_meta() {
            find "$BACKUP_ROOT" -maxdepth 1 -type f -name "full_meta_*.txt" 2>/dev/null | sort -r | head -1
        }


        # ========== 主流程 ==========
        main() {
            # 检查目录
            if [[ ! -d "$BACKUP_ROOT" ]]; then
                error_exit "备份根目录不存在: $BACKUP_ROOT"
            fi
            if [[ ! -d "$WAL_ARCHIVE_DIR" ]]; then
                error_exit "WAL 归档目录不存在: $WAL_ARCHIVE_DIR"
            fi


            # 1. 找到全量元数据
            LATEST_META=$(get_latest_full_meta)
            if [[ -z "$LATEST_META" ]]; then
                error_exit "未找到全量备份元数据文件(full_meta_*.txt)"
            fi
            log "使用全量备份元数据: $(basename "$LATEST_META")"


            # 2. 读取停止 LSN
            STOP_LSN=$(grep '^FULL_BACKUP_STOP_LSN=' "$LATEST_META" | cut -d= -f2)
            if [[ -z "$STOP_LSN" ]]; then
                error_exit "无法从元数据中获取停止 LSN"
            fi
            log "全量备份停止 LSN = $STOP_LSN"


            # 3. 获取当前 LSN
            CURRENT_LSN=$(psql -h "$PG_HOST" -p "$PG_PORT" -U "$PG_USER" -d postgres -t -c "SELECT pg_current_wal_lsn();" | tr -d ' ')
            if [[ -z "$CURRENT_LSN" ]]; then
                error_exit "无法获取当前 LSN"
            fi
            log "当前 LSN = $CURRENT_LSN"


            # 4. 检查是否有新 WAL
            if [[ "$CURRENT_LSN" == "$STOP_LSN" ]]; then
                log "没有新 WAL 产生,跳过本次增量备份"
                exit 0
            fi


            # 5. 将 LSN 转换为 WAL 文件名
            START_WAL=$(lsn_to_wal "$STOP_LSN")
            END_WAL=$(lsn_to_wal "$CURRENT_LSN")
            if [[ -z "$START_WAL" || -z "$END_WAL" ]]; then
                error_exit "LSN 转 WAL 文件名失败"
            fi
            log "WAL 文件名范围: $START_WAL -> $END_WAL"


            # 6. 生成 WAL 文件列表并复制到临时目录
            TEMP_DIR=$(mktemp -d -t pg_incr_XXXXXX)
            if [[ ! -d "$TEMP_DIR" ]]; then
                error_exit "创建临时目录失败"
            fi
            log "临时目录: $TEMP_DIR"


            COUNT=0
            MISSING_COUNT=0
            for wal_file in $(wal_range "$START_WAL" "$END_WAL"); do
                src="${WAL_ARCHIVE_DIR}/${wal_file}"
                if [[ -f "$src" ]]; then
                    # 使用 cp 代替 ln(避免跨文件系统问题)
                    if cp "$src" "$TEMP_DIR/" 2>/dev/null; then
                        ((COUNT++))
                        log "已复制: $wal_file"
                    else
                        log "警告: 复制失败 $wal_file (权限或空间不足)"
                        ((MISSING_COUNT++))
                    fi
                else
                    log "警告: WAL 文件缺失: $src"
                    ((MISSING_COUNT++))
                fi
            done


            if [[ $COUNT -eq 0 ]]; then
                log "未找到任何可用的 WAL 文件,增量备份为空"
                rm -rf "$TEMP_DIR"
                exit 0
            fi


            log "成功复制 $COUNT 个 WAL 文件,缺失 $MISSING_COUNT 个"


            # 7. 打包
            TIMESTAMP=$(date +%Y%m%d_%H%M%S)
            INCR_TAR="${BACKUP_ROOT}/incr_${TIMESTAMP}.tar.gz"
            if tar -zcvf "$INCR_TAR" -C "$TEMP_DIR" . >/dev/null 2>&1; then
                log "增量包创建成功: $INCR_TAR"
            else
                error_exit "tar 打包失败"
            fi


            # 8. 生成元数据文件
            cat > "${INCR_TAR}.info" <<EOF
        BASE_FULL_BACKUP_META=$(basename "$LATEST_META")
        START_LSN=$STOP_LSN
        END_LSN=$CURRENT_LSN
        WAL_COUNT=$COUNT
        BACKUP_TIME=$TIMESTAMP
        EOF


            # 9. 清理临时目录
            rm -rf "$TEMP_DIR"
            log "增量备份完成,包大小: $(du -h "$INCR_TAR" | cut -f1)"
        }


        # 执行
        main "$@"


        部署自动任务

        全库备份,只需每天执行一次,增量备份,设定每2小时执行一次

          crontab -e
          0 02 * * * bin/bash home/postgres/scripts/pg_full_backup.sh


          10 01,03,05,07,09,11,13,15,17,19,21,23 * * * /bin/bash /home/postgres/scripts/pg_incr_from_archive.sh


          两个脚本生成的文件及内容

          主要生成的文件有全库备份的摘要信息、输出日志、备份集文件、增量备份日志、增量备份摘要内容和增量备份的备份集。

            full_meta_20260428_151115.txt
            full_20260428_151115.tar.gz
            full_backup.log
            incr_20260428_173747.tar
            incr_20260428_173747.tar.info
            incr_20260429_153422.tar.gz
            incr_20260429_153422.tar.gz.info
            incr_backup.log
            1
            [postgres@mytest01 phybackup]$ cat full_meta_20260428_151115.txt
            FULL_BACKUP_STOP_LSN=1/27000060
            BACKUP_TIME=20260428_151115
            BACKUP_METHOD=pg_basebackup
            BACKUP_TAR_FILE=full_20260428_151115.tar.gz
            [postgres@mytest01 phybackup]$
            [postgres@mytest01 phybackup]$ cat full_backup.log
            [2026-04-28 14:57:35] 开始全量备份 -> /home/postgres/backupfiles/phybackup/full_20260428_145735
            [2026-04-28 14:58:03] 全量备份完成,停止 LSN =
            [2026-04-28 15:11:15] 开始全量备份 -> /home/postgres/backupfiles/phybackup/full_20260428_151115
            [2026-04-28 15:11:43] 正在压缩备份目录 -> /home/postgres/backupfiles/phybackup/full_20260428_151115.tar.gz
            [2026-04-28 15:13:19] 压缩完成,删除原始目录 /home/postgres/backupfiles/phybackup/full_20260428_151115
            [2026-04-28 15:13:20] 全量备份完成,停止 LSN = 1/27000060,元数据文件: /home/postgres/backupfiles/phybackup/full_meta_20260428_151115.txt
            [postgres@mytest01 phybackup]$
            [postgres@mytest01 phybackup]$ cat incr_20260428_173747.tar.info
            BASE_FULL_BACKUP_META=full_meta_20260428_151115.txt
            START_LSN=1/27000060
            END_LSN=1/2C000060
            WAL_COUNT=5
            BACKUP_TIME=20260428_173747
            [postgres@mytest01 phybackup]$
            [postgres@mytest01 phybackup]$ cat incr_backup.log
            [2026-04-28 17:32:46] 使用全量备份元数据: full_meta_20260428_151115.txt
            [2026-04-28 17:32:46] 全量备份停止 LSN = 1/27000060
            [2026-04-28 17:32:46] 当前 LSN = 1/2B000000
            [2026-04-28 17:32:46] WAL 文件名范围: 000000050000000100000027 -> 00000005000000010000002A
            [2026-04-28 17:37:47] 使用全量备份元数据: full_meta_20260428_151115.txt
            [2026-04-28 17:37:47] 全量备份停止 LSN = 1/27000060
            [2026-04-28 17:37:47] 当前 LSN = 1/2C000060
            [2026-04-28 17:37:47] WAL 文件名范围: 000000050000000100000027 -> 00000005000000010000002C
            [2026-04-28 17:37:47] 临时目录: /tmp/pg_incr_EB3JDG
            [2026-04-28 17:37:47] 已复制: 000000050000000100000027
            [2026-04-28 17:37:47] 已复制: 000000050000000100000028
            [2026-04-28 17:37:47] 已复制: 000000050000000100000029
            [2026-04-28 17:37:47] 已复制: 00000005000000010000002A
            [2026-04-28 17:37:47] 已复制: 00000005000000010000002B
            [2026-04-28 17:37:47] 警告: WAL 文件缺失: /usr/local/pgsql/data/pg_archive/00000005000000010000002C
            [2026-04-28 17:37:47] 成功复制 5 个 WAL 文件,缺失 1 个
            [2026-04-28 17:37:47] 增量包创建成功: /home/postgres/backupfiles/phybackup/incr_20260428_173747.tar
            [2026-04-28 17:37:47] 增量备份完成,包大小: 81M
            [2026-04-29 15:34:22] 使用全量备份元数据: full_meta_20260428_151115.txt
            [2026-04-29 15:34:22] 全量备份停止 LSN = 1/27000060
            [2026-04-29 15:34:22] 当前 LSN = 1/2C000148
            [2026-04-29 15:34:22] WAL 文件名范围: 000000050000000100000027 -> 00000005000000010000002C
            [2026-04-29 15:34:22] 临时目录: /tmp/pg_incr_uxK8dP
            [2026-04-29 15:34:22] 已复制: 000000050000000100000027
            [2026-04-29 15:34:22] 已复制: 000000050000000100000028
            [2026-04-29 15:34:22] 已复制: 000000050000000100000029
            [2026-04-29 15:34:22] 已复制: 00000005000000010000002A
            [2026-04-29 15:34:22] 已复制: 00000005000000010000002B
            [2026-04-29 15:34:22] 警告: WAL 文件缺失: /usr/local/pgsql/data/pg_archive/00000005000000010000002C
            [2026-04-29 15:34:22] 成功复制 5 个 WAL 文件,缺失 1 个
            [2026-04-29 15:34:22] 增量包创建成功: /home/postgres/backupfiles/phybackup/incr_20260429_153422.tar.gz
            [2026-04-29 15:34:22] 增量备份完成,包大小: 88K


            异机备份恢复

            基于上面生成的备份文件,拿到异机做备份恢复,操作如下。

            1、恢复数据库文件

              tar -xzvf /data/pg_backup/pgfull_20260429_095341.tar.gz -C /data/pg/pgdata/
              mv pgfull_20260429_095341 pgfull
              chmod 700 pgfull

              2、恢复WAL归档

                tar -xvf /data/pg_backup/incr_20260429_151001.tar -C /data/pg/backup/backupfiles/archive

                3、编辑postgresql.conf参数

                  vi postgresql.conf
                  archive_command = 'cp %/data/pg/pgdata/pgfull/pg_archive/%f'
                  restore_command = 'cp /data/pg/backup/backupfiles/archive/%%p'

                  4、启动并恢复数据库

                    pg_ctl -D /data/pg/pgdata/pgfull -l logfile start

                    到此,整个数据库已经恢复好了。

                    文章转载自skylines,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

                    评论