¥
立即购买

数据库脚本生成

213 浏览
16 试用
3 购买
Nov 19, 2025更新

本提示词用于根据用户提供的数据库类型、备份频率、存储位置等关键参数,生成可直接部署的数据库备份与恢复脚本。所有逻辑均基于用户显式输入,不依赖外部数据推断,确保脚本安全、可控与可复现。适用于企业运维、灾备设计和自动化流程构建,可提供完整脚本、配置信息、执行方法与故障排查要点,帮助提升数据库运维效率与数据保护可靠性。

脚本概述

以下为基于 MySQL 8.0 的企业级全量备份/恢复脚本方案,支持:

  • 安全认证:MySQL 密码通过环境变量传递(BACKUP_MYSQL_PASSWORD),TLS 强制启用并验证 CA
  • 一致性备份:mysqldump 使用 --single-transaction、包含 routines/triggers/events
  • 压缩与校验:gzip/pigz 压缩,生成并上传 sha256 校验文件
  • S3 兼容对象存储:支持自定义 endpoint、SSE 加密(AES256 或 KMS)、带宽与并发控制
  • 保留策略:S3 与本地分阶段清理,默认保留 14 天
  • 完整日志与错误处理:可直接用于生产环境的可观测性与审计
  • 恢复校验:下载并校验 SHA256,支持恢复最新或指定日期的备份

注意:

  • 由于连接地址使用 IP,脚本使用 --ssl-mode=VERIFY_CA(校验证书链),而非 VERIFY_IDENTITY(需要域名匹配)
  • 提供了服务端加密参数 encryption=AES256;如需使用 KMS 可将 ENCRYPTION_MODE 改为 aws:kms 并指定 KMS_KEY_ID

备份脚本

文件名建议:/usr/local/sbin/mysql_full_backup.sh

#!/usr/bin/env bash
# MySQL 8.0 全量备份到 S3 兼容存储
# - 压缩 + SHA256 校验
# - TLS 验证 CA
# - SSE 加密(SSE-S3 或 SSE-KMS)
# - 并发与带宽限制
# - 保留策略(S3 与本地)
# - 完整日志与错误处理
set -Eeuo pipefail

########################
# 配置区(请按需修改)
########################
# MySQL 连接
DB_HOST="10.12.5.21"
DB_PORT="3306"
DB_NAME="finance_prod"
DB_USER="backup_admin"
MYSQL_SSL_CA="/etc/ssl/certs/ca.pem"
# 认证:从环境变量 BACKUP_MYSQL_PASSWORD 读取,并安全地导出为 MYSQL_PWD
PASSWORD_ENV_VAR="BACKUP_MYSQL_PASSWORD"

# 导出选项(在用户给定基础上附加 --add-drop-table 便于恢复覆盖)
DUMP_OPTIONS="--single-transaction --routines --triggers --events --add-drop-table"

# S3 兼容存储
S3_ENDPOINT="https://s3.internal.example"
S3_BUCKET="db-backup"
# 目录模板按 UTC 时间展开(例:mysql/finance_prod/2025/11/19)
S3_PREFIX_TEMPLATE="mysql/finance_prod/%Y/%m/%d"

# 加密(默认为 SSE-S3:AES256;如需 KMS,将 ENCRYPTION_MODE=aws:kms 并设置 KMS_KEY_ID)
ENCRYPTION_MODE="AES256"         # 可选:AES256 或 aws:kms
KMS_KEY_ID="kms-key-01"          # 仅当 ENCRYPTION_MODE=aws:kms 时生效

# 暂存目录与日志
STAGING_DIR="/var/backups/mysql"
LOG_DIR="${STAGING_DIR}/logs"

# 资源控制
S3_CONCURRENCY="2"               # s3.max_concurrent_requests
S3_BANDWIDTH_LIMIT="50MB/s"      # s3.max_bandwidth
REGION="${AWS_DEFAULT_REGION:-us-east-1}"

# 保留策略(天)
RETENTION_DAYS="14"

########################
# 结束配置区
########################

# 全局变量
DATE_UTC="$(date -u +%Y%m%d-%H%M%SZ)"
DATE_DIR_UTC="$(date -u +%Y/%m/%d)"
HOSTNAME_SHORT="$(hostname -s || echo 'host')"
DUMP_BASENAME="${DB_NAME}-${DATE_UTC}"
SQL_FILE="${STAGING_DIR}/${DUMP_BASENAME}.sql"
ARCHIVE_FILE="${SQL_FILE}.gz"
CHECKSUM_FILE="${ARCHIVE_FILE}.sha256"
AWS_CONFIG_FILE="${STAGING_DIR}/awscli-backup-config"
S3_DIR="$(date -u +"${S3_PREFIX_TEMPLATE}")"    # 展开 %Y/%m/%d
S3_URI_DIR="s3://${S3_BUCKET}/${S3_DIR}"
S3_URI_ARCHIVE="${S3_URI_DIR}/${DUMP_BASENAME}.sql.gz"
S3_URI_CHECKSUM="${S3_URI_ARCHIVE}.sha256"
LOG_FILE="${LOG_DIR}/backup-${DATE_UTC}.log"

# 日志函数
log() { echo "[$(date -u +'%Y-%m-%dT%H:%M:%SZ')] $*" | tee -a "$LOG_FILE" >&2; }
die() { log "ERROR: $*"; exit 1; }

# 清理与陷阱
cleanup() {
  # 保留归档和校验文件,其他中间文件可根据需要清理
  :
}
trap cleanup EXIT

precheck() {
  mkdir -p "$STAGING_DIR" "$LOG_DIR"

  # 将所有输出重定向到日志文件与控制台
  exec > >(tee -a "$LOG_FILE") 2>&1

  # 依赖检查
  command -v mysqldump >/dev/null 2>&1 || die "mysqldump 未安装"
  command -v mysql >/dev/null 2>&1 || die "mysql 客户端未安装"
  command -v aws >/dev/null 2>&1 || die "aws CLI 未安装"
  command -v sha256sum >/dev/null 2>&1 || {
    command -v shasum >/dev/null 2>&1 || command -v openssl >/dev/null 2>&1 || die "缺少 sha256sum/shasum/openssl"
  }
  [[ -r "$MYSQL_SSL_CA" ]] || die "TLS CA 文件不可读: $MYSQL_SSL_CA"

  # 密码与安全
  local pw="${!PASSWORD_ENV_VAR-}"
  [[ -n "$pw" ]] || die "未设置环境变量 ${PASSWORD_ENV_VAR}(用于 MySQL 认证)"
  export MYSQL_PWD="$pw"    # 仅导出到子进程,不写入日志

  # AWS 凭据需外部提供 (环境变量/实例角色)
  : "${AWS_ACCESS_KEY_ID?未发现 AWS_ACCESS_KEY_ID(或使用实例角色)}"
  : "${AWS_SECRET_ACCESS_KEY?未发现 AWS_SECRET_ACCESS_KEY(或使用实例角色)}"

  # AWS CLI 临时配置(并发与带宽限制)
  cat >"$AWS_CONFIG_FILE" <<EOF
[default]
region = ${REGION}
s3 =
  max_concurrent_requests = ${S3_CONCURRENCY}
  max_bandwidth = ${S3_BANDWIDTH_LIMIT}
  signature_version = s3v4
EOF
  chmod 600 "$AWS_CONFIG_FILE"
}

compressor() {
  if command -v pigz >/dev/null 2>&1; then
    pigz -6
  else
    gzip -6
  fi
}

calc_sha256() {
  if command -v sha256sum >/dev/null 2>&1; then
    sha256sum "$1" | awk '{print $1}'
  elif command -v shasum >/dev/null 2>&1; then
    shasum -a 256 "$1" | awk '{print $1}'
  else
    openssl dgst -sha256 "$1" | awk '{print $2}'
  fi
}

s3_put_args() {
  # 生成 SSE 参数
  if [[ "$ENCRYPTION_MODE" == "aws:kms" ]]; then
    echo "--sse aws:kms --sse-kms-key-id ${KMS_KEY_ID}"
  else
    echo "--sse AES256"
  fi
}

upload_to_s3() {
  local src="$1" dst="$2"
  local sse_args; sse_args=$(s3_put_args)
  aws --endpoint-url "$S3_ENDPOINT" --config-file "$AWS_CONFIG_FILE" s3 cp "$src" "$dst" $sse_args --no-progress
}

verify_s3_object() {
  local uri="$1"
  aws --endpoint-url "$S3_ENDPOINT" --config-file "$AWS_CONFIG_FILE" s3 ls "$uri" >/dev/null
}

purge_local() {
  # 清理本地超过保留期的归档与日志
  find "$STAGING_DIR" -type f -name "${DB_NAME}-*.sql.gz" -mtime +"$RETENTION_DAYS" -print -delete || true
  find "$STAGING_DIR" -type f -name "${DB_NAME}-*.sql.gz.sha256" -mtime +"$RETENTION_DAYS" -print -delete || true
  find "$LOG_DIR" -type f -name "backup-*.log" -mtime +"$RETENTION_DAYS" -print -delete || true
}

purge_s3() {
  # 依据对象 key 中的日期前缀 mysql/finance_prod/YYYY/MM/DD 进行清理
  local cutoff_epoch
  cutoff_epoch=$(date -u -d "-${RETENTION_DAYS} days" +%s)

  log "开始 S3 过期清理(保留 ${RETENTION_DAYS} 天内备份)"
  # 列出所有对象,提取唯一日期目录
  local list tmpfile
  tmpfile="$(mktemp)"
  aws --endpoint-url "$S3_ENDPOINT" --config-file "$AWS_CONFIG_FILE" s3 ls "s3://${S3_BUCKET}/$(dirname "${S3_DIR}")/" --recursive \
    | awk '{print $4}' \
    | grep -E '^mysql/finance_prod/[0-9]{4}/[0-9]{2}/[0-9]{2}/' \
    | awk -F/ '{printf "%s/%s/%s\n",$3,$4,$5}' \
    | sort -u > "$tmpfile" || true

  while IFS= read -r ymd; do
    [[ -z "$ymd" ]] && continue
    # ymd 形如 2025/11/19
    local dir_epoch
    dir_epoch=$(date -u -d "$ymd" +%s || echo 0)
    if (( dir_epoch > 0 && dir_epoch < cutoff_epoch )); then
      local prefix="mysql/finance_prod/${ymd}"
      log "删除过期目录:s3://${S3_BUCKET}/${prefix}/"
      aws --endpoint-url "$S3_ENDPOINT" --config-file "$AWS_CONFIG_FILE" s3 rm "s3://${S3_BUCKET}/${prefix}/" --recursive || true
    fi
  done < "$tmpfile"
  rm -f "$tmpfile" || true
}

do_backup() {
  log "开始 MySQL 全量备份:${DB_NAME} @ ${DB_HOST}:${DB_PORT} (UTC ${DATE_UTC})"
  log "暂存目录:${STAGING_DIR}"
  log "S3 目标:${S3_URI_DIR}"

  # 低优先级执行(若存在)
  local NICE=""; command -v nice >/dev/null 2>&1 && NICE="nice -n 10"
  local IONICE=""; command -v ionice >/dev/null 2>&1 && IONICE="ionice -c2 -n7"

  # 1) 生成 SQL dump 并压缩
  log "执行 mysqldump 并压缩到 ${ARCHIVE_FILE}"
  ${IONICE} ${NICE} bash -c "
    set -o pipefail
    mysqldump \
      --host='${DB_HOST}' --port='${DB_PORT}' --user='${DB_USER}' \
      --ssl-mode=VERIFY_CA --ssl-ca='${MYSQL_SSL_CA}' \
      --databases '${DB_NAME}' \
      ${DUMP_OPTIONS} \
    | $(declare -f compressor); compressor \
    > '${ARCHIVE_FILE}'
  "

  # 2) 基本校验
  [[ -s "$ARCHIVE_FILE" ]] || die "归档文件为空:$ARCHIVE_FILE"
  local size; size=$(stat -c%s "$ARCHIVE_FILE" 2>/dev/null || stat -f%z "$ARCHIVE_FILE")
  log "归档完成,大小:${size} bytes"

  # 3) 生成 SHA256
  local sum; sum=$(calc_sha256 "$ARCHIVE_FILE")
  echo "${sum}  $(basename "$ARCHIVE_FILE")" > "$CHECKSUM_FILE"
  log "生成校验文件:$CHECKSUM_FILE"

  # 4) 上传归档与校验文件
  log "上传到 S3:$S3_URI_ARCHIVE"
  upload_to_s3 "$ARCHIVE_FILE" "$S3_URI_ARCHIVE"
  upload_to_s3 "$CHECKSUM_FILE" "$S3_URI_CHECKSUM"
  verify_s3_object "$S3_URI_ARCHIVE" || die "S3 对象校验失败:$S3_URI_ARCHIVE"
  verify_s3_object "$S3_URI_CHECKSUM" || die "S3 对象校验失败:$S3_URI_CHECKSUM"
  log "上传成功"

  # 5) 清理策略
  purge_local
  purge_s3

  log "备份完成:$S3_URI_ARCHIVE"
}

main() {
  precheck
  do_backup
}
main "$@"

恢复脚本

文件名建议:/usr/local/sbin/mysql_restore_from_s3.sh

#!/usr/bin/env bash
# 从 S3 兼容存储恢复 MySQL 8.0 全量备份
# - 下载并校验 SHA256
# - TLS 验证 CA
# - 支持恢复最新或指定日期(YYYY/MM/DD)
set -Eeuo pipefail

########################
# 配置区(请按需修改)
########################
# MySQL 连接
DB_HOST="10.12.5.21"
DB_PORT="3306"
DB_USER="backup_admin"
MYSQL_SSL_CA="/etc/ssl/certs/ca.pem"
PASSWORD_ENV_VAR="BACKUP_MYSQL_PASSWORD"

# S3 兼容存储
S3_ENDPOINT="https://s3.internal.example"
S3_BUCKET="db-backup"
S3_PREFIX_BASE="mysql/finance_prod"   # 与备份中的前缀根一致(不含 %Y/%m/%d)
REGION="${AWS_DEFAULT_REGION:-us-east-1}"

# 暂存目录与日志
STAGING_DIR="/var/backups/mysql"
LOG_DIR="${STAGING_DIR}/logs"

# 恢复目标数据库(若备份使用 --databases finance_prod,dump 内含 CREATE DATABASE/USE)
# 留空表示按 dump 中包含的数据库名进行恢复
TARGET_DB=""   # 例如需要重定向恢复到其他库名,可设置为 "finance_prod_restore"

########################
# 结束配置区
########################

LOG_FILE="${LOG_DIR}/restore-$(date -u +%Y%m%d-%H%M%SZ).log"
S3_LATEST_CACHE="${STAGING_DIR}/.latest_list.txt"

log() { echo "[$(date -u +'%Y-%m-%dT%H:%M:%SZ')] $*" | tee -a "$LOG_FILE" >&2; }
die() { log "ERROR: $*"; exit 1; }

usage() {
  cat <<EOF
用法:
  $0 --latest [--force]
  $0 --date YYYY/MM/DD [--force]
  $0 --object s3://bucket/path/to/file.sql.gz [--force]

说明:
  --latest             恢复最新一份备份(按 S3 时间排序)
  --date YYYY/MM/DD    恢复指定日期目录下最新一份备份
  --object s3://...    指定精确对象路径恢复
  --force              跳过交互确认
EOF
}

precheck() {
  mkdir -p "$STAGING_DIR" "$LOG_DIR"
  exec > >(tee -a "$LOG_FILE") 2>&1

  command -v mysql >/dev/null 2>&1 || die "mysql 客户端未安装"
  command -v aws >/dev/null 2>&1 || die "aws CLI 未安装"
  command -v zcat >/dev/null 2>&1 || die "缺少 zcat (gzip)"
  command -v sha256sum >/dev/null 2>&1 || {
    command -v shasum >/dev/null 2>&1 || command -v openssl >/dev/null 2>&1 || die "缺少 sha256sum/shasum/openssl"
  }
  [[ -r "$MYSQL_SSL_CA" ]] || die "TLS CA 文件不可读: $MYSQL_SSL_CA"

  local pw="${!PASSWORD_ENV_VAR-}"
  [[ -n "$pw" ]] || die "未设置环境变量 ${PASSWORD_ENV_VAR}(用于 MySQL 认证)"
  export MYSQL_PWD="$pw"

  : "${AWS_ACCESS_KEY_ID?未发现 AWS_ACCESS_KEY_ID(或使用实例角色)}"
  : "${AWS_SECRET_ACCESS_KEY?未发现 AWS_SECRET_ACCESS_KEY(或使用实例角色)}"
}

find_latest_object() {
  # 列出基础前缀下的所有 .sql.gz,按时间排序取最后一个
  aws --endpoint-url "$S3_ENDPOINT" s3 ls "s3://${S3_BUCKET}/${S3_PREFIX_BASE}/" --recursive \
    | grep -E '\.sql\.gz$' \
    | sort -k1,2 \
    | awk '{print $4}' \
    | tail -n1
}

find_latest_in_date() {
  local ymd="$1"  # YYYY/MM/DD
  aws --endpoint-url "$S3_ENDPOINT" s3 ls "s3://${S3_BUCKET}/${S3_PREFIX_BASE}/${ymd}/" --recursive \
    | grep -E '\.sql\.gz$' \
    | sort -k1,2 \
    | awk '{print $4}' \
    | tail -n1
}

download_and_verify() {
  local key="$1"
  local s3_uri="s3://${S3_BUCKET}/${key}"
  local archive="${STAGING_DIR}/$(basename "$key")"
  local checksum="${archive}.sha256"

  log "下载归档:$s3_uri"
  aws --endpoint-url "$S3_ENDPOINT" s3 cp "$s3_uri" "$archive" --no-progress
  aws --endpoint-url "$S3_ENDPOINT" s3 cp "${s3_uri}.sha256" "$checksum" --no-progress || die "缺少校验文件:${s3_uri}.sha256"

  # 校验 SHA256
  local ok=0
  if command -v sha256sum >/dev/null 2>&1; then
    (cd "$(dirname "$archive")" && sha256sum -c "$(basename "$checksum")") && ok=1
  elif command -v shasum >/dev/null 2>&1; then
    (cd "$(dirname "$archive")" && shasum -a 256 -c "$(basename "$checksum")") && ok=1
  else
    local expected; expected=$(awk '{print $1}' "$checksum")
    local actual; actual=$(openssl dgst -sha256 "$archive" | awk '{print $2}')
    [[ "$expected" == "$actual" ]] && ok=1
  fi
  [[ $ok -eq 1 ]] || die "SHA256 校验失败:$archive"
  log "校验通过:$archive"
  echo "$archive"
}

restore_into_mysql() {
  local archive="$1"
  local db_redirect="$2"  # 目标数据库名,可为空

  log "开始恢复:$archive"
  if [[ -n "$db_redirect" ]]; then
    log "注意:将使用 --database=${db_redirect} 覆盖 dump 中的 USE 语句"
    # 通过 --database 将所有导入重定向到指定库(要求该库已存在)
    zcat "$archive" | mysql \
      --host="$DB_HOST" --port="$DB_PORT" --user="$DB_USER" \
      --ssl-mode=VERIFY_CA --ssl-ca="$MYSQL_SSL_CA" \
      --database="$db_redirect"
  else
    # 按 dump 内部的 CREATE DATABASE/USE 执行
    zcat "$archive" | mysql \
      --host="$DB_HOST" --port="$DB_PORT" --user="$DB_USER" \
      --ssl-mode=VERIFY_CA --ssl-ca="$MYSQL_SSL_CA"
  fi
  log "恢复完成"
}

confirm_if_needed() {
  local force="$1"
  if [[ "${force:-0}" -eq 1 ]]; then
    return
  fi
  echo "即将对 MySQL 实例 ${DB_HOST}:${DB_PORT} 执行恢复操作,可能覆盖现有数据。"
  read -r -p "确认继续?(yes/NO): " ans
  [[ "$ans" == "yes" ]] || die "用户取消"
}

main() {
  precheck

  local mode="" date_ymd="" object_uri="" force=0
  while [[ $# -gt 0 ]]; do
    case "$1" in
      --latest) mode="latest"; shift ;;
      --date) mode="date"; date_ymd="$2"; shift 2 ;;
      --object) mode="object"; object_uri="$2"; shift 2 ;;
      --force) force=1; shift ;;
      -h|--help) usage; exit 0 ;;
      *) usage; die "未知参数:$1" ;;
    esac
  done
  [[ -n "$mode" ]] || { usage; die "必须指定 --latest 或 --date 或 --object"; }

  confirm_if_needed "$force"

  local key=""
  case "$mode" in
    latest)
      key=$(find_latest_object)
      [[ -n "$key" ]] || die "未找到任何备份对象"
      ;;
    date)
      [[ "$date_ymd" =~ ^[0-9]{4}/[0-9]{2}/[0-9]{2}$ ]] || die "日期格式须为 YYYY/MM/DD"
      key=$(find_latest_in_date "$date_ymd")
      [[ -n "$key" ]] || die "指定日期下未找到备份:$date_ymd"
      ;;
    object)
      if [[ "$object_uri" =~ ^s3:// ]]; then
        key="${object_uri#s3://*/}"   # 占位截取不可行,改为通用处理
        key="${object_uri#s3://}"
        key="${key#${S3_BUCKET}/}"
      else
        die "object 必须为 s3:// 开头的 URI"
      fi
      ;;
  esac

  log "选定对象 key:$key"
  local archive; archive=$(download_and_verify "$key")
  restore_into_mysql "$archive" "$TARGET_DB"
  log "恢复成功:$key"
}
main "$@"

配置参数

请根据实际环境检查或修改以下关键参数:

  • MySQL 连接

    • DB_HOST=10.12.5.21
    • DB_PORT=3306
    • DB_NAME=finance_prod(备份)
    • DB_USER=backup_admin
    • MYSQL_SSL_CA=/etc/ssl/certs/ca.pem
    • BACKUP_MYSQL_PASSWORD(以环境变量方式提供,脚本内不出现明文)
  • 导出选项

    • DUMP_OPTIONS="--single-transaction --routines --triggers --events --add-drop-table"
    • 如不希望删除已有表,可移除 --add-drop-table(恢复可能因已存在表失败)
  • S3 兼容存储

    • S3_ENDPOINT=https://s3.internal.example
    • S3_BUCKET=db-backup
    • S3_PREFIX_TEMPLATE=mysql/finance_prod/%Y/%m/%d(UTC 展开)
    • ENCRYPTION_MODE=AES256(或 aws:kms)
    • KMS_KEY_ID=kms-key-01(仅 aws:kms 时有效)
    • S3_CONCURRENCY=2
    • S3_BANDWIDTH_LIMIT=50MB/s
    • REGION=us-east-1(可按对象存储要求调整)
  • 暂存和保留

    • STAGING_DIR=/var/backups/mysql
    • RETENTION_DAYS=14
  • 恢复脚本

    • S3_PREFIX_BASE=mysql/finance_prod
    • TARGET_DB=""(为空则按 dump 内数据库名恢复;指定则重定向至该库)

使用指南

  1. 前置准备
  • 安装依赖
    • MySQL 客户端:mysqldump、mysql
    • AWS CLI v2
    • gzip 或 pigz(可选提速)
  • 配置凭据(建议使用最小权限的专用访问凭据)
    • 导出 S3 凭据(或使用实例角色):
      • export AWS_ACCESS_KEY_ID=...
      • export AWS_SECRET_ACCESS_KEY=...
      • 可选:export AWS_SESSION_TOKEN=...
    • 设置 MySQL 备份密码(仅当前会话/服务):
      • export BACKUP_MYSQL_PASSWORD='******'
  • 确认 TLS CA 文件可用:/etc/ssl/certs/ca.pem
  1. 首次执行测试
  1. 定时任务部署(每日一次)
  • 使用 cron(示例:每日 02:15 UTC)
    • sudo crontab -e
    • 添加:
      • 15 2 * * * BACKUP_MYSQL_PASSWORD='******' AWS_ACCESS_KEY_ID='...' AWS_SECRET_ACCESS_KEY='...' /usr/local/sbin/mysql_full_backup.sh >/dev/null 2>&1
  • 推荐使用 systemd 定义 EnvironmentFile 引用凭据(权限 600),避免直接写入 crontab
  1. 执行恢复
  • 恢复最新备份:
    • BACKUP_MYSQL_PASSWORD='******' AWS_ACCESS_KEY_ID='...' AWS_SECRET_ACCESS_KEY='...' sudo -E /usr/local/sbin/mysql_restore_from_s3.sh --latest
  • 恢复指定日期最新备份:
    • sudo -E /usr/local/sbin/mysql_restore_from_s3.sh --date 2025/11/19
  • 指定精确对象:
  • 若需无交互执行(自动化场景):
    • 添加 --force
  1. 恢复验证(强烈建议)
  • 恢复完成后基本验证:
    • mysql -h 10.12.5.21 -P 3306 -u backup_admin --ssl-mode=VERIFY_CA --ssl-ca=/etc/ssl/certs/ca.pem -e "USE finance_prod; SHOW TABLES LIMIT 5;"
  • 业务侧做一致性自检(行数、关键表 hash 等)

故障排查

  • 无法连接 MySQL(SSL 或认证失败)

    • 检查 BACKUP_MYSQL_PASSWORD 是否正确、未过期
    • 检查 MYSQL_SSL_CA 路径与证书链是否匹配服务端
    • 由于连接使用 IP,--ssl-mode 使用 VERIFY_CA;若需 VERIFY_IDENTITY,必须使用证书匹配的主机名
  • 备份文件过小或为空

    • 确认 mysqldump 退出码、日志中是否有错误
    • 移除 --single-transaction 对 MyISAM 等非事务表无一致性保证;若混用存储引擎,考虑维护窗口或逻辑锁定策略
  • S3 上传失败或慢

    • 检查 S3_ENDPOINT 可达性、认证是否正确
    • 调整 S3_CONCURRENCY、S3_BANDWIDTH_LIMIT(例如提高并发、降低带宽以避开限流)
    • 若是非 AWS S3,确认服务端是否支持所选加密模式:
      • AES256(SSE-S3)通常广泛支持
      • aws:kms 需服务端支持 KMS 语义;不支持时改用 AES256
  • 校验失败(SHA256 不一致)

    • 重新下载并校验
    • 确认网络传输与中间缓存未篡改
    • 如多次失败,重新执行备份
  • 保留清理未生效

    • 确认 S3 key 结构仍为 mysql/finance_prod/YYYY/MM/DD/...
    • 确认脚本中 RETENTION_DAYS 与服务器时区(脚本使用 UTC)
  • 恢复阻塞或耗时过长

    • 目标库请尽量关闭额外负载
    • 根据需求考虑调整 innodb_buffer_pool_size、禁用外键检查(需要在 dump/恢复策略中评估)
    • 网络带宽不足时可在近端落地下载后再恢复
  • 权限建议

    • MySQL 账号 backup_admin 建议最小权限:SELECT, SHOW VIEW, TRIGGER, EVENT, LOCK TABLES(若使用 --single-transaction 一般无需全局锁)
    • 恢复需要更高权限(CREATE, DROP, INSERT, ALTER, TRIGGER, EVENT 等)
    • S3 凭据建议仅允许对指定 bucket/prefix 的读写与列出

以上脚本遵循安全最佳实践:不在脚本中保存明文密码;使用 TLS 校验;使用 SSE 加密;包含错误处理、日志与可恢复性验证。可直接在生产环境部署,按需调整参数与策略。

脚本概述

本方案面向 PostgreSQL 14,采用“持续WAL日志归档 + 定期全量基准备份”的组合策略,满足“日志备份、每小时频率、NFS远端存储、Zstandard压缩、SHA256校验、SSL加密连接、pgpass认证、备份可验证”的要求。

  • WAL日志归档:使用 pg_receivewal 持续接收WAL到 NFS(/mnt/backup/pg/wal),并通过小时维护任务强制切换WAL、压缩归档段、清理过期日志(72小时)。
  • 全量基准备份:使用 pg_basebackup 以目录格式生成基准备份,完成后通过 pg_verifybackup 校验,再打包为 tar.zst、生成 SHA256 校验文件,并清理超过 7 天的旧备份。
  • 安全合规:密码仅通过 /etc/pgpass 使用(600 权限),连接强制 SSL(sslmode=require),备份目录挂载校验防止误写本地磁盘。
  • 恢复:提供物理恢复脚本,支持基于归档WAL的时间点恢复(PITR),可一键解压基准备份、配置 restore_command 解压 .zst WAL 并启动恢复。

目录约定(均可在脚本中调整):

  • NFS 挂载点:/mnt/backup/pg (必须已挂载 10.1.1.20:/export/pg-archive)
  • WAL 目录:/mnt/backup/pg/wal
  • 基准备份目录:/mnt/backup/pg/base
  • 压缩算法:zstd -3
  • 校验:sha256

备份脚本

以下为三个备份相关脚本,均为完整可执行脚本:

  • 01_wal_archiver.sh:持续接收WAL(建议以 systemd 常驻)
  • 02_wal_hourly_maint.sh:每小时维护(可crontab),强制切换WAL、压缩、清理过期
  • 03_base_backup.sh:按需/定时执行全量基准备份、校验、压缩、保留策略

另外提供 systemd 单元文件保持 WAL 归档常驻运行。

注意:请先阅读“配置参数”章节并按需修改脚本头部配置变量。


脚本:/usr/local/bin/01_wal_archiver.sh

#!/usr/bin/env bash
# 持续接收WAL至 NFS 目录(建议配合 systemd 常驻)
set -euo pipefail

# ================= 用户可配置区域 =================
PG_HOST="10.20.8.17"
PG_PORT="5432"
PG_USER="pg_backup"
PG_DATABASE="orders_core"          # 仅用于可选的SQL检测/日志,不参与复制连接
PGPASSFILE_PATH="/etc/pgpass"      # 必须 600 权限
PG_SSLMODE="require"

WAL_DIR="/mnt/backup/pg/wal"
NFS_MOUNT="/mnt/backup/pg"         # 用于挂载校验
REPL_SLOT="wal_archiver_slot"      # 物理复制槽(用于避免WAL丢失)
LOG_DIR="/var/log/pg_backup"
LOG_FILE="${LOG_DIR}/wal_archiver.log"
# =================================================

umask 077
mkdir -p "${LOG_DIR}" "${WAL_DIR}"

# 输出到日志与标准输出(便于systemd/journalctl查看)
exec >> >(awk -v d="$(date +'%Y-%m-%d %H:%M:%S')" '{print strftime("%Y-%m-%d %H:%M:%S"), $0 }' | tee -a "${LOG_FILE}") 2>&1

echo "[INFO] Starting WAL archiver..."

# 1) 前置检查
command -v pg_receivewal >/dev/null 2>&1 || { echo "[ERR ] pg_receivewal not found"; exit 1; }
command -v psql         >/dev/null 2>&1 || { echo "[ERR ] psql not found"; exit 1; }

# 校验 pgpass 权限
if [[ ! -f "${PGPASSFILE_PATH}" ]]; then
  echo "[ERR ] ${PGPASSFILE_PATH} not found"
  exit 1
fi
perm=$(stat -c "%a" "${PGPASSFILE_PATH}")
if [[ "${perm}" != "600" ]]; then
  echo "[ERR ] ${PGPASSFILE_PATH} must be 600 permission, current: ${perm}"
  exit 1
fi

# 校验 NFS 已挂载(避免误写到本地)
if ! command -v findmnt >/dev/null 2>&1; then
  echo "[ERR ] findmnt not found (util-linux). Install it first."
  exit 1
fi
if ! findmnt -n -T "${NFS_MOUNT}" >/dev/null 2>&1; then
  echo "[ERR ] NFS mount ${NFS_MOUNT} is not mounted. Abort to prevent local writes."
  exit 1
fi

export PGPASSFILE="${PGPASSFILE_PATH}"
export PGSSLMODE="${PG_SSLMODE}"
export PGAPPNAME="wal_archiver"

# 2) 确保复制槽存在(需要具有 REPLICATION 权限)
# 注意:首次运行将尝试创建物理复制槽;若权限不足,请由DBA预先创建:
#   SELECT pg_create_physical_replication_slot('wal_archiver_slot');
echo "[INFO] Ensuring physical replication slot '${REPL_SLOT}' exists..."
set +e
psql "host=${PG_HOST} port=${PG_PORT} user=${PG_USER} dbname=${PG_DATABASE} sslmode=${PG_SSLMODE}" -v ON_ERROR_STOP=1 -Atc \
"SELECT 1 FROM pg_replication_slots WHERE slot_name='${REPL_SLOT}' AND plugin IS NULL;" >/dev/null
if [[ $? -ne 0 ]]; then
  # 无法查询或无权限仅打印提示,不中断(pg_receivewal 无槽也可运行,但不保证WAL保留)
  echo "[WARN] Could not verify/create replication slot (insufficient privilege?). Consider creating it manually:"
  echo "       SELECT pg_create_physical_replication_slot('${REPL_SLOT}');"
fi
set -e

# 3) 启动接收(持续运行,断线自动重连)
# 提示:pg_receivewal(14)不支持zstd内置压缩,因此采用小时维护脚本对完成段进行zstd压缩
echo "[INFO] Launching pg_receivewal to ${WAL_DIR} ..."
exec pg_receivewal \
  --host="${PG_HOST}" \
  --port="${PG_PORT}" \
  --username="${PG_USER}" \
  --slot="${REPL_SLOT}" \
  --directory="${WAL_DIR}" \
  --verbose \
  --status-interval=60

建议的 systemd 单元:/etc/systemd/system/pg-wal-archiver.service

[Unit]
Description=PostgreSQL WAL Archiver (pg_receivewal)
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=pg_backup
Group=pg_backup
Environment=PGPASSFILE=/etc/pgpass
Environment=PGSSLMODE=require
ExecStart=/usr/local/bin/01_wal_archiver.sh
Restart=always
RestartSec=5
# 保护
PrivateTmp=yes
NoNewPrivileges=yes
ProtectSystem=full
ProtectHome=yes
ReadWritePaths=/mnt/backup/pg /var/log/pg_backup

[Install]
WantedBy=multi-user.target

脚本:/usr/local/bin/02_wal_hourly_maint.sh

#!/usr/bin/env bash
# 每小时运行:强制切换WAL(可选)、压缩已完成段为 .zst、生成sha256、清理过期WAL
set -euo pipefail

# ================= 用户可配置区域 =================
PG_HOST="10.20.8.17"
PG_PORT="5432"
PG_USER="pg_backup"
PG_DATABASE="orders_core"          # 用于可选 pg_switch_wal()
PGPASSFILE_PATH="/etc/pgpass"
PG_SSLMODE="require"

WAL_DIR="/mnt/backup/pg/wal"
NFS_MOUNT="/mnt/backup/pg"

RETENTION_WAL_HOURS=72             # 保留72小时
ZSTD_LEVEL=3

LOG_DIR="/var/log/pg_backup"
LOG_FILE="${LOG_DIR}/wal_hourly_maint.log"
LOCK_FILE="/var/lock/wal_hourly_maint.lock"
# =================================================

umask 077
mkdir -p "${LOG_DIR}" "${WAL_DIR}"
exec 200>"${LOCK_FILE}"
flock -n 200 || { echo "[WARN] Another wal_hourly_maint is running, exit."; exit 0; }

exec >> >(awk '{print strftime("%Y-%m-%d %H:%M:%S"), $0 }' | tee -a "${LOG_FILE}") 2>&1
echo "[INFO] WAL hourly maintenance started."

command -v zstd >/dev/null 2>&1 || { echo "[ERR ] zstd not found"; exit 1; }
command -v sha256sum >/dev/null 2>&1 || { echo "[ERR ] sha256sum not found"; exit 1; }
command -v find >/dev/null 2>&1 || { echo "[ERR ] find not found"; exit 1; }

# 校验 NFS
if ! findmnt -n -T "${NFS_MOUNT}" >/dev/null 2>&1; then
  echo "[ERR ] NFS mount ${NFS_MOUNT} is not mounted. Abort."
  exit 1
fi

export PGPASSFILE="${PGPASSFILE_PATH}"
export PGSSLMODE="${PG_SSLMODE}"
export PGAPPNAME="wal_hourly_maint"

# 可选:强制切换WAL(需要SUPERUSER)
set +e
psql "host=${PG_HOST} port=${PG_PORT} user=${PG_USER} dbname=${PG_DATABASE} sslmode=${PG_SSLMODE}" -v ON_ERROR_STOP=1 -Atc \
"SELECT pg_switch_wal();" >/dev/null 2>&1
if [[ $? -ne 0 ]]; then
  echo "[INFO] pg_switch_wal() skipped (likely insufficient privileges)."
fi
set -e

# 压缩已完成的WAL段:匹配24位十六进制文件名,排除 .partial,若尚未压缩则以zstd压缩为 .zst,生成sha256并删除原文件
echo "[INFO] Compressing completed WAL segments to .zst ..."
shopt -s nullglob
for wal in "${WAL_DIR}"/[0-9A-F][0-9A-F][0-9A-F][0-9A-F][0-9A-F][0-9A-F][0-9A-F][0-9A-F][0-9A-F][0-9A-F][0-9A-F][0-9A-F][0-9A-F][0-9A-F][0-9A-F][0-9A-F][0-9A-F][0-9A-F][0-9A-F][0-9A-F][0-9A-F][0-9A-F][0-9A-F][0-9A-F]; do
  [[ "${wal}" == *.partial ]] && continue
  if [[ ! -f "${wal}.zst" ]]; then
    tmp="${wal}.zst.tmp"
    echo "[INFO] Compress ${wal} -> ${wal}.zst"
    zstd -"${ZSTD_LEVEL}" -T0 -f -q --output="${tmp}" "${wal}"
    mv -f "${tmp}" "${wal}.zst"
    sha256sum "${wal}.zst" > "${wal}.zst.sha256"
    rm -f -- "${wal}"
  fi
done

# 同步压缩 timeline history / backup history 文件(*.history, *.backup)
for hist in "${WAL_DIR}"/*.history "${WAL_DIR}"/*.backup; do
  [[ -e "${hist}" ]] || continue
  if [[ ! -f "${hist}.zst" ]]; then
    tmp="${hist}.zst.tmp"
    echo "[INFO] Compress ${hist} -> ${hist}.zst"
    zstd -"${ZSTD_LEVEL}" -T0 -f -q --output="${tmp}" "${hist}"
    mv -f "${tmp}" "${hist}.zst"
    sha256sum "${hist}.zst" > "${hist}.zst.sha256"
    rm -f -- "${hist}"
  fi
done

# 清理过期 .zst 及其 .sha256
echo "[INFO] Pruning WAL older than ${RETENTION_WAL_HOURS}h ..."
find "${WAL_DIR}" -type f -name "*.zst" -mmin +$((RETENTION_WAL_HOURS*60)) -print -delete
# 清理孤立的sha256
find "${WAL_DIR}" -type f -name "*.zst.sha256" -exec sh -c 'f="{}"; b="${f%.sha256}"; [[ -f "$b" ]] || { echo "[INFO] Remove orphan $f"; rm -f -- "$f"; }' \;

echo "[INFO] WAL hourly maintenance completed."

脚本:/usr/local/bin/03_base_backup.sh

#!/usr/bin/env bash
# 生成全量基准备份(目录格式)-> 验证(pg_verifybackup) -> 打包为 tar.zst -> 生成sha256 -> 清理过期
set -euo pipefail

# ================= 用户可配置区域 =================
PG_HOST="10.20.8.17"
PG_PORT="5432"
PG_USER="pg_backup"
PGPASSFILE_PATH="/etc/pgpass"
PG_SSLMODE="require"

BASE_DIR="/mnt/backup/pg/base"       # 最终tar.zst储存目录
STAGING_DIR="${BASE_DIR}/.staging"   # 中间目录(用于目录格式与验证)
NFS_MOUNT="/mnt/backup/pg"

RETENTION_BASE_DAYS=7
ZSTD_LEVEL=3

LOG_DIR="/var/log/pg_backup"
LOG_FILE="${LOG_DIR}/base_backup.log"
LOCK_FILE="/var/lock/base_backup.lock"
# =================================================

umask 077
mkdir -p "${LOG_DIR}" "${BASE_DIR}" "${STAGING_DIR}"
exec 200>"${LOCK_FILE}"
flock -n 200 || { echo "[WARN] Another base_backup is running, exit."; exit 0; }

exec >> >(awk '{print strftime("%Y-%m-%d %H:%M:%S"), $0 }' | tee -a "${LOG_FILE}") 2>&1
echo "[INFO] Base backup started."

for cmd in pg_basebackup pg_verifybackup zstd sha256sum tar; do
  command -v "$cmd" >/dev/null 2>&1 || { echo "[ERR ] $cmd not found"; exit 1; }
done

# 校验NFS
if ! findmnt -n -T "${NFS_MOUNT}" >/dev/null 2>&1; then
  echo "[ERR ] NFS mount ${NFS_MOUNT} is not mounted. Abort."
  exit 1
fi

export PGPASSFILE="${PGPASSFILE_PATH}"
export PGSSLMODE="${PG_SSLMODE}"
export PGAPPNAME="base_backup"

ts="$(date +'%Y%m%d_%H%M%S')"
stage="${STAGING_DIR}/base_${ts}"
mkdir -p "${stage}"

# 1) 生成目录格式基准备份(不包含WAL,依赖归档WAL进行恢复)
# 选择目录格式便于使用pg_verifybackup验证
echo "[INFO] Running pg_basebackup to ${stage} ..."
pg_basebackup \
  --host="${PG_HOST}" \
  --port="${PG_PORT}" \
  --username="${PG_USER}" \
  --pgdata="${stage}" \
  --format=plain \
  --wal-method=none \
  --checkpoint=fast \
  --verbose \
  --progress \
  --manifest-checksums=SHA256

# 2) 验证备份
echo "[INFO] Verifying backup with pg_verifybackup ..."
pg_verifybackup "${stage}"

# 3) 打包压缩为 tar.zst
target="${BASE_DIR}/base_${ts}.tar.zst"
echo "[INFO] Archiving ${stage} -> ${target}"
tar -C "${stage}" -I "zstd -T0 -${ZSTD_LEVEL}" -cf "${target}" .

# 4) 生成SHA256
sha256sum "${target}" > "${target}.sha256"

# 5) 清理中间目录
rm -rf -- "${stage}"

# 6) 清理过期基备
echo "[INFO] Pruning base backups older than ${RETENTION_BASE_DAYS}d ..."
find "${BASE_DIR}" -type f -name "base_*.tar.zst" -mtime +${RETENTION_BASE_DAYS} -print -delete
find "${BASE_DIR}" -type f -name "base_*.tar.zst.sha256" -exec sh -c 'f="{}"; b="${f%.sha256}"; [[ -f "$b" ]] || { echo "[INFO] Remove orphan $f"; rm -f -- "$f"; }' \;

echo "[INFO] Base backup completed: ${target}"

恢复脚本

脚本支持按最新基准备份恢复,或指定某个基备文件,并可选择目标时间进行PITR。恢复时通过 restore_command 从 WAL 归档目录解压 .zst 到实例 pg_wal。


脚本:/usr/local/bin/04_restore_physical.sh

#!/usr/bin/env bash
# 使用基准备份 + 归档WAL进行物理恢复(支持可选时间点)
# 用法示例:
#   04_restore_physical.sh \
#     --pgdata /var/lib/pgsql/14/data \
#     --wal-archive /mnt/backup/pg/wal \
#     --base LATEST \
#     --owner postgres \
#     --target-time "2025-11-18 12:30:00"
set -euo pipefail

# 默认参数
PGDATA=""
WAL_ARCHIVE=""
BASE="LATEST"        # 可为具体文件路径,如 /mnt/backup/pg/base/base_YYYYMMDD_HHMMSS.tar.zst
OWNER="postgres"
TARGET_TIME=""       # 为空则恢复到归档的最新一致点
ZSTD_BIN="zstd"
LOG_FILE="/var/log/pg_backup/restore.log"

usage() {
  cat <<EOF
Usage: $0 --pgdata DIR --wal-archive DIR [--base LATEST|/path/to/base_xxx.tar.zst] [--owner USER] [--target-time "YYYY-MM-DD HH:MM:SS"]
EOF
  exit 1
}

# 解析参数
while [[ $# -gt 0 ]]; do
  case "$1" in
    --pgdata) PGDATA="$2"; shift 2;;
    --wal-archive) WAL_ARCHIVE="$2"; shift 2;;
    --base) BASE="$2"; shift 2;;
    --owner) OWNER="$2"; shift 2;;
    --target-time) TARGET_TIME="$2"; shift 2;;
    -h|--help) usage;;
    *) echo "Unknown arg: $1"; usage;;
  esac
done

[[ -n "${PGDATA}" && -n "${WAL_ARCHIVE}" ]] || usage

umask 077
mkdir -p "$(dirname "${LOG_FILE}")"
exec >> >(awk '{print strftime("%Y-%m-%d %H:%M:%S"), $0 }' | tee -a "${LOG_FILE}") 2>&1

for cmd in tar "${ZSTD_BIN}"; do
  command -v "$cmd" >/dev/null 2>&1 || { echo "[ERR ] $cmd not found"; exit 1; }
done

# 0) 风险保护与准备
if [[ -e "${PGDATA}" && -n "$(ls -A "${PGDATA}" 2>/dev/null || true)" ]]; then
  echo "[ERR ] PGDATA ${PGDATA} is not empty. Please stop PostgreSQL and provide an empty directory."
  exit 1
fi
mkdir -p "${PGDATA}"
chown -R "${OWNER}:${OWNER}" "${PGDATA}"

# 1) 选择基准备份
if [[ "${BASE}" == "LATEST" ]]; then
  BASE_FILE=$(ls -1t "${WAL_ARCHIVE%/}/../base"/base_*.tar.zst 2>/dev/null | head -n1 || true)
  if [[ -z "${BASE_FILE}" ]]; then
    echo "[ERR ] No base backup found in ${WAL_ARCHIVE%/}/../base"
    exit 1
  fi
else
  BASE_FILE="${BASE}"
fi
[[ -f "${BASE_FILE}" ]] || { echo "[ERR ] Base backup file not found: ${BASE_FILE}"; exit 1; }
[[ -f "${BASE_FILE}.sha256" ]] && sha256sum -c "${BASE_FILE}.sha256"

echo "[INFO] Restoring base backup: ${BASE_FILE} -> ${PGDATA}"
# 2) 解压基备
tar -I "${ZSTD_BIN} -d -T0" -xf "${BASE_FILE}" -C "${PGDATA}"
chown -R "${OWNER}:${OWNER}" "${PGDATA}"

# 3) 配置恢复(PostgreSQL >= 12 使用 recovery.signal + postgresql.auto.conf)
# restore_command 解压 .zst WAL;若存在未压缩同名文件也可兼容(可按需调整)
RESTORE_CMD="bash -c 'if [ -f \"${WAL_ARCHIVE}/%f.zst\" ]; then zstd -d -c \"${WAL_ARCHIVE}/%f.zst\" > \"%p\"; elif [ -f \"${WAL_ARCHIVE}/%f\" ]; then cat \"${WAL_ARCHIVE}/%f\" > \"%p\"; else exit 1; fi'"

AUTO_CONF="${PGDATA}/postgresql.auto.conf"
echo "[INFO] Writing restore settings to ${AUTO_CONF}"
{
  echo "restore_command = '${RESTORE_CMD}'"
  if [[ -n "${TARGET_TIME}" ]]; then
    echo "recovery_target_time = '${TARGET_TIME}'"
  else
    echo "# recovery_target_time not set (restore to latest consistent point)"
  fi
  echo "recovery_target_action = 'pause'"
} >> "${AUTO_CONF}"

# 创建 recovery.signal 以启用归档恢复
: > "${PGDATA}/recovery.signal"
chown "${OWNER}:${OWNER}" "${PGDATA}/recovery.signal" "${AUTO_CONF}"

cat <<EOM
[INFO] Restore prepared successfully.

Next steps:
1) Start PostgreSQL service manually as user '${OWNER}'.
2) Monitor logs for recovery progress. When recovery completes (or pauses at target),
   you can promote the server if needed:
   - To resume to latest: ALTER SYSTEM RESET recovery_target_time; SELECT pg_reload_conf();
   - Or to end recovery and accept writes: pg_ctl -D "${PGDATA}" promote
EOM

配置参数

请根据实际环境调整以下参数(均在脚本头部):

  • 通用连接与安全
    • PG_HOST=10.20.8.17
    • PG_PORT=5432
    • PG_USER=pg_backup(需具备 REPLICATION 权限;用于 psql 的库连接可使用 orders_core 或 postgres)
    • PGPASSFILE_PATH=/etc/pgpass(600 权限,包含 host:port:*:pg_backup:
    • PG_SSLMODE=require
  • 存储与目录
    • NFS_MOUNT=/mnt/backup/pg(必须已挂载 10.1.1.20:/export/pg-archive)
    • WAL_DIR=/mnt/backup/pg/wal
    • BASE_DIR=/mnt/backup/pg/base
    • STAGING_DIR=/mnt/backup/pg/base/.staging(中间目录)
  • 策略与性能
    • RETENTION_WAL_HOURS=72(WAL保留小时)
    • RETENTION_BASE_DAYS=7(基备保留天)
    • ZSTD_LEVEL=3(zstd 压缩级别)
  • 恢复
    • 04_restore_physical.sh 的 --pgdata、--wal-archive、--owner、--target-time

使用指南

  1. 前置准备
  • 数据库端设置(已满足:archive_mode=on, wal_level=replica, max_wal_senders=5)
  • 创建备份用户与权限(由DBA执行)
    • 创建用户并授予 REPLICATION(示例):
      • CREATE ROLE pg_backup WITH LOGIN REPLICATION PASSWORD '******';
    • pg_hba.conf 增加(示例,强制SSL):
      • hostssl replication pg_backup 10.0.0.0/8 md5
      • hostssl all pg_backup 10.0.0.0/8 md5
    • 重新加载配置:SELECT pg_reload_conf();
  • 复制槽(建议,用于避免WAL过早回收)
    • SELECT pg_create_physical_replication_slot('wal_archiver_slot');
  • 备份主机准备
    • 安装工具:postgresql14-client(含 pg_basebackup/pg_receivewal/pg_verifybackup)、zstd、tar、coreutils、util-linux
    • 创建 /etc/pgpass(600 权限),内容示例:
      • 10.20.8.17:5432:*:pg_backup:
    • 挂载NFS:10.1.1.20:/export/pg-archive -> /mnt/backup/pg(建议 /etc/fstab 配置,开启 nolock、hard、timeo、retrans 等合适参数)
    • 创建目录并授权:/mnt/backup/pg/{wal,base}、/var/log/pg_backup,属主为 pg_backup
  1. 启动WAL持续归档
  • 放置 01_wal_archiver.sh 并赋权:
    • install -o pg_backup -g pg_backup -m 0750 01_wal_archiver.sh /usr/local/bin/
  • 部署并启动 systemd 服务:
    • install -m 0644 pg-wal-archiver.service /etc/systemd/system/
    • systemctl daemon-reload
    • systemctl enable --now pg-wal-archiver.service
    • journalctl -u pg-wal-archiver -f 检查日志
  1. 配置每小时维护(符合“备份频率:每小时”)
  • 安装 02_wal_hourly_maint.sh:
    • install -o pg_backup -g pg_backup -m 0750 02_wal_hourly_maint.sh /usr/local/bin/
  • 为 pg_backup 用户添加 crontab:
    • crontab -u pg_backup -e
    • 添加:
      • 5 * * * * /usr/local/bin/02_wal_hourly_maint.sh
  1. 配置定期全量基准备份
  • 安装 03_base_backup.sh:
    • install -o pg_backup -g pg_backup -m 0750 03_base_backup.sh /usr/local/bin/
  • 建议每日低峰期执行(示例 02:30):
    • 30 2 * * * /usr/local/bin/03_base_backup.sh
  1. 备份验证
  • 基准备份生成流程已内置 pg_verifybackup 校验
  • 可定期抽检:
    • 解压某一份 base_*.tar.zst 到临时目录,运行 pg_verifybackup 临时目录,确保校验通过
  • WAL 检查:
    • 随机抽检 .zst 的 sha256sum:sha256sum -c file.zst.sha256
  • 恢复演练(建议在测试环境):
    • 使用 04_restore_physical.sh 指向测试实例 PGDATA 与 WAL 归档目录,指定 target-time 进行PITR 验证
  1. 恢复操作(生产应急)
  • 停库并备份现有 PGDATA(若存在)
  • 执行:
    • /usr/local/bin/04_restore_physical.sh --pgdata /var/lib/pgsql/14/data --wal-archive /mnt/backup/pg/wal --base LATEST --owner postgres --target-time "YYYY-MM-DD HH:MM:SS"
  • 启动 PostgreSQL,观察日志直至恢复完成或到达目标时间点

安全注意:

  • 严禁将密码写入脚本与日志;仅使用 /etc/pgpass(600)与 sslmode=require
  • 恢复脚本不会覆盖非空 PGDATA(避免误操作)

故障排查

  • 01_wal_archiver.sh 启动失败

    • 检查 /etc/pgpass 权限是否为 600,内容是否正确
    • 检查 NFS 是否已挂载:findmnt -n -T /mnt/backup/pg
    • 检查网络连通与 pg_hba.conf 中的 replication 访问配置
    • 检查用户 pg_backup 是否有 REPLICATION 权限;必要时手动创建复制槽
  • WAL 未生成 .zst 或数量过少

    • 确保 02_wal_hourly_maint.sh cron 正常运行(查看 /var/log/pg_backup/wal_hourly_maint.log)
    • 用户 pg_backup 无法执行 pg_switch_wal() 时属正常(非超级用户);在写入较少的业务下,可不强制切换
  • 03_base_backup.sh 报错

    • 检查 pg_basebackup、pg_verifybackup 是否存在且版本为 14 客户端
    • 确认 NFS 已挂载,磁盘空间充足
    • 确认 pg_backup 用户具备 REPLICATION 连接权限
  • 校验失败(sha256 对不上)

    • 可能发生传输/存储损坏,重新生成该备份;检查NFS与网络稳定性
  • 恢复失败:启动后无法找到 WAL

    • 检查 postgresql.auto.conf 中 restore_command 路径是否正确
    • 确认 WAL 目录存在相应时间点所需的段文件(可能超过保留期被清理)
    • 如需要特定时间点,请确保保留期覆盖目标时间与安全裕量
  • PITR 到达目标时间但未恢复到期望状态

    • 检查时区(服务器与目标时间的时区偏差)
    • 使用 recovery_target_action='pause'(脚本已设置),在暂停状态下核对数据后再决定继续或 promote
  • 性能建议

    • zstd -T0 使用多核压缩;如 CPU 突增可降低并发或级别
    • NFS 挂载选项建议启用较大 rsize/wsize 与合适的超时,减少抖动

如需进一步适配(例如以 archive_command 方式压缩推送而非 pg_receivewal 拉取、基备使用并行、多仓库冗余等),可在现有脚本基础上进行扩展。

以下方案基于“SQL Server 2019 + SMB 共享存储 + 每小时日志备份 + 48 小时保留 + 备份压缩 + VERIFY 验证 + 并发=1”的需求设计,脚本适配 Linux 上的 SQL Server(使用 sqlcmd),通过 CIFS 挂载 SMB 共享,符合加密连接要求且不在脚本中存储明文密码。备份脚本包含完善的错误处理、日志记录、并发控制和备份可用性验证;恢复脚本支持在已有完整备份(可选差异备份)基础上按时间点恢复日志链或恢复至最新。

重要安全提示:

  • 加密连接 encrypt=True 且 trust_server_certificate=False 意味着客户端会严格校验证书匹配。请确保脚本中的 -S 使用的“服务器名称/FQDN”与 SQL Server 证书的 CN/SAN 匹配(使用 IP 可能因证书不匹配而失败)。如当前仅提供 IP,请在配置参数中将 SERVER 改为证书匹配的 DNS 名称。
  • SQL 登录密码不写入脚本,运行前通过环境变量 MSSQL_BACKUP_PASS 注入,脚本内部只临时导出 SQLCMDPASSWORD。
  1. 脚本概述
  • 备份脚本(backup_log.sh)

    • 每小时执行一次日志备份(BACKUP LOG),写入 SMB 挂载目录。
    • 启用 WITH COMPRESSION, CHECKSUM,备份完成后执行 RESTORE VERIFYONLY 验证。
    • 按命名规则文件名 CRM_LOG_{yyyyMMddHHmm}.trn。
    • 采用 flock 锁保证并发=1。
    • 自动清理超 48 小时的旧日志备份。
    • 详细日志写入 /var/log/sqlbackup/crm_log_backup.log。
  • 恢复脚本(restore_logs.sh)

    • 支持选项:
      • 可选:先恢复指定完整备份(+ 可选差异备份),自动生成 MOVE 映射到目标目录。
      • 然后根据目录中的日志备份按时间顺序恢复日志链。
      • 支持指定时间点 STOPAT 恢复或恢复到最新。
      • 自动检查数据库状态(需要在 RESTORING 状态下追加日志恢复)。
    • 使用 WITH CHECKSUM 验证、STATS 进度显示,出错即中止。
  1. 备份脚本 请将以下脚本保存为 /usr/local/sbin/backup_log.sh 并赋予可执行权限。
#!/usr/bin/env bash
# SQL Server 2019 - 事务日志备份脚本(SMB 存储,压缩+校验+验证,保留期清理,单实例并发)
# Author: backup_automation
# Exit codes: 0=success, non-zero=failure

set -euo pipefail
umask 077

# ========= 用户参数(请根据“配置参数”章节调整) =========
SERVER="tcp:10.30.4.10,1433"          # 建议修改为证书匹配的FQDN以满足 encrypt=True & trust_server_certificate=False
DATABASE="crm_prod"
SQLUSER="backup_svc"
APP_NAME="backup_automation"

# 加密要求
ENCRYPT="true"                        # 固定为true
TRUST_SERVER_CERT="false"             # 固定为false(不使用 -C)

# SMB 存储
SMB_SHARE="//10.1.2.50/sqlbackups/crm"
SMB_CRED_FILE="/etc/backup/smb.cred"
MOUNT_POINT="/mnt/sqlbackups/crm"

# 备份文件命名
FILE_PREFIX="CRM_LOG"

# 保留与校验
RETENTION_HOURS=48                    # 48h
ENABLE_VERIFY="true"                  # RESTORE VERIFYONLY
ENABLE_COMPRESSION="true"             # BACKUP WITH COMPRESSION

# 并发控制与日志
LOCK_FILE="/var/lock/crm_log_backup.lock"
LOG_DIR="/var/log/sqlbackup"
LOG_FILE="${LOG_DIR}/crm_log_backup.log"

# ========= 工具与函数 =========
log() {
  echo "[$(date '+%F %T%z')] $*" | tee -a "$LOG_FILE"
}

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

need_cmd() {
  command -v "$1" >/dev/null 2>&1 || fail "命令不存在: $1"
}

run_sql() {
  # $1: T-SQL
  # 使用加密,禁止信任服务器证书
  sqlcmd -S "$SERVER" -U "$SQLUSER" -b -l 30 -d "master" \
         -N \
         -Q "$1"
}

# ========= 预检查 =========
need_cmd sqlcmd
need_cmd flock
need_cmd mount
need_cmd findmnt

mkdir -p "$LOG_DIR"
touch "$LOG_FILE"
chmod 640 "$LOG_FILE" || true

# 密码通过环境变量 MSSQL_BACKUP_PASS 注入,脚本不保存明文密码
: "${MSSQL_BACKUP_PASS:?必须在环境变量 MSSQL_BACKUP_PASS 中提供 SQL 登录密码}"
export SQLCMDPASSWORD="$MSSQL_BACKUP_PASS"

# 加密连接提示
if [[ "$TRUST_SERVER_CERT" == "false" ]]; then
  # 若使用IP连接,证书CN/SAN可能不匹配,建议改为FQDN
  if [[ "$SERVER" =~ ^tcp:[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+,?[0-9]*$ ]]; then
    log "WARNING: 当前 SERVER 使用 IP。encrypt=True & trust_server_certificate=False 时需确保证书CN/SAN匹配该地址;否则请将 SERVER 改为FQDN。"
  fi
fi

# ========= 挂载 SMB(如未挂载) =========
if [[ ! -f "$SMB_CRED_FILE" ]]; then
  fail "未找到 SMB 凭据文件: $SMB_CRED_FILE"
fi

# 确保凭据文件权限安全
perm=$(stat -c "%a" "$SMB_CRED_FILE" 2>/dev/null || echo "")
if [[ "$perm" != "600" && "$perm" != "400" ]]; then
  log "WARNING: 建议将 $SMB_CRED_FILE 权限设为 600 以保护凭据,当前权限: $perm"
fi

mkdir -p "$MOUNT_POINT"

# 确定挂载选项中的uid/gid,优先使用 mssql 用户(SQL Server on Linux 默认)
MSSQL_UID=$(id -u mssql 2>/dev/null || id -u)
MSSQL_GID=$(id -g mssql 2>/dev/null || id -g)

if ! findmnt -rn "$MOUNT_POINT" >/dev/null 2>&1; then
  log "挂载 SMB 共享: $SMB_SHARE -> $MOUNT_POINT"
  # 需要root或具备相应sudo授权
  sudo mount -t cifs "$SMB_SHARE" "$MOUNT_POINT" \
    -o "credentials=${SMB_CRED_FILE},vers=3.0,iocharset=utf8,uid=${MSSQL_UID},gid=${MSSQL_GID},file_mode=0660,dir_mode=0770,nosuid,nodev" \
    || fail "SMB 挂载失败"
else
  log "SMB 已挂载: $MOUNT_POINT"
fi

# ========= 并发控制 =========
exec 200>"$LOCK_FILE"
if ! flock -n 200; then
  fail "发现已有备份实例在运行(并发限制=1),本次退出"
fi

# ========= 备份执行 =========
TS=$(date '+%Y%m%d%H%M')
BAK_FILE="${MOUNT_POINT}/${FILE_PREFIX}_${TS}.trn"

log "开始日志备份 -> $BAK_FILE"

BACKUP_OPTS="CHECKSUM, STATS=10"
if [[ "$ENABLE_COMPRESSION" == "true" ]]; then
  BACKUP_OPTS="COMPRESSION, ${BACKUP_OPTS}"
fi

# 执行日志备份
run_sql "BACKUP LOG [${DATABASE}] TO DISK=N'${BAK_FILE}' WITH ${BACKUP_OPTS};"
log "日志备份完成"

# 备份可用性验证
if [[ "$ENABLE_VERIFY" == "true" ]]; then
  log "执行 RESTORE VERIFYONLY 校验"
  run_sql "RESTORE VERIFYONLY FROM DISK=N'${BAK_FILE}' WITH CHECKSUM;"
  log "VERIFY 校验通过"
fi

# ========= 过期清理 =========
log "清理超过 ${RETENTION_HOURS} 小时的历史日志备份"
# 只删除符合命名规范的 .trn 文件
find "$MOUNT_POINT" -maxdepth 1 -type f -name "${FILE_PREFIX}_*.trn" -mmin +$((RETENTION_HOURS*60)) -print -exec rm -f {} \; | tee -a "$LOG_FILE" || true

log "备份任务完成"
exit 0

说明与要点:

  • 连接使用 -N(加密),不使用 -C(不信任服务器证书);确保 SERVER 与证书匹配。
  • 采用 WITH CHECKSUM + RESTORE VERIFYONLY 双重保障备份可恢复性。
  • 通过 flock 保证并发=1。
  • 清理仅作用于 CRM_LOG_*.trn 文件,避免误删其他文件。
  • 脚本不输出或记录密码。
  1. 恢复脚本 请将以下脚本保存为 /usr/local/sbin/restore_logs.sh 并赋予可执行权限。支持从“完整+差异(可选)+日志链”恢复到某一时间点或最新。
#!/usr/bin/env bash
# SQL Server 2019 - 日志链恢复脚本(可选先恢复完整/差异备份,再按顺序恢复日志,支持 STOPAT)
# 适用:将数据库恢复到最新或指定时间点;需要完整备份与日志链连续
# 警告:生产环境执行前请在测试环境验证
set -euo pipefail
umask 077

# 默认参数
SERVER="tcp:10.30.4.10,1433"     # 建议改为证书匹配的FQDN(见说明)
SQLUSER="backup_svc"
DATABASE="crm_prod"

# 备份存储
SMB_SHARE="//10.1.2.50/sqlbackups/crm"
SMB_CRED_FILE="/etc/backup/smb.cred"
MOUNT_POINT="/mnt/sqlbackups/crm"
BACKUP_DIR="$MOUNT_POINT"         # 日志文件所在目录(默认挂载点)
FILE_PREFIX="CRM_LOG"

# 可选:完整/差异备份文件(提供则会先行恢复)
FULL_BAK=""                       # 例如: /mnt/sqlbackups/crm/CRM_FULL_2025xxxx.bak
DIFF_BAK=""                       # 例如: /mnt/sqlbackups/crm/CRM_DIFF_2025xxxx.bak

# 可选:停止时间点(格式:YYYY-MM-DD HH:MM:SS),为空则恢复到最新
STOPAT=""

# 可选:覆盖已存在数据库(慎用)
REPLACE="false"

# 恢复目标数据/日志目录(Linux SQL Server 默认)
TARGET_DATA_DIR="/var/opt/mssql/data"
TARGET_LOG_DIR="/var/opt/mssql/data"

LOG_DIR="/var/log/sqlbackup"
LOG_FILE="${LOG_DIR}/crm_restore.log"

log() { echo "[$(date '+%F %T%z')] $*" | tee -a "$LOG_FILE"; }
fail() { log "ERROR: $*"; exit 1; }
need_cmd() { command -v "$1" >/dev/null 2>&1 || fail "命令不存在: $1"; }

usage() {
  cat <<EOF
用法: restore_logs.sh [选项]
  --server <server>              SQL Server 地址(建议FQDN)
  --user <sqluser>               SQL 登录用户
  --db <dbname>                  目标数据库名
  --backup-dir </path>           日志备份所在目录(默认挂载点)
  --full </path/to/full.bak>     可选:完整备份文件
  --diff </path/to/diff.bak>     可选:差异备份文件
  --stopat "YYYY-MM-DD HH:MM:SS" 可选:按时间点恢复
  --replace                      可选:覆盖已存在数据库
  --data-dir </path>             目标数据文件目录
  --log-dir </path>              目标日志文件目录

示例:
  MSSQL_BACKUP_PASS=*** restore_logs.sh --server sql01.contoso.com --db crm_prod \\
    --backup-dir /mnt/sqlbackups/crm --full /mnt/sqlbackups/crm/CRM_FULL_20251118.bak \\
    --stopat "2025-11-19 13:20:00" --replace
EOF
}

# 解析参数
while [[ $# -gt 0 ]]; do
  case "$1" in
    --server) SERVER="$2"; shift 2;;
    --user) SQLUSER="$2"; shift 2;;
    --db) DATABASE="$2"; shift 2;;
    --backup-dir) BACKUP_DIR="$2"; shift 2;;
    --full) FULL_BAK="$2"; shift 2;;
    --diff) DIFF_BAK="$2"; shift 2;;
    --stopat) STOPAT="$2"; shift 2;;
    --replace) REPLACE="true"; shift 1;;
    --data-dir) TARGET_DATA_DIR="$2"; shift 2;;
    --log-dir) TARGET_LOG_DIR="$2"; shift 2;;
    -h|--help) usage; exit 0;;
    *) fail "未知参数: $1";;
  esac
done

need_cmd sqlcmd
need_cmd mount
need_cmd findmnt
mkdir -p "$LOG_DIR"
touch "$LOG_FILE"
chmod 640 "$LOG_FILE" || true

: "${MSSQL_BACKUP_PASS:?必须在环境变量 MSSQL_BACKUP_PASS 中提供 SQL 登录密码}"
export SQLCMDPASSWORD="$MSSQL_BACKUP_PASS"

# 挂载 SMB(如未挂载)
if [[ ! -f "$SMB_CRED_FILE" ]]; then
  fail "未找到 SMB 凭据文件: $SMB_CRED_FILE"
fi
mkdir -p "$MOUNT_POINT"
if ! findmnt -rn "$MOUNT_POINT" >/dev/null 2>&1; then
  log "挂载 SMB 共享: $SMB_SHARE -> $MOUNT_POINT"
  MSSQL_UID=$(id -u mssql 2>/dev/null || id -u)
  MSSQL_GID=$(id -g mssql 2>/dev/null || id -g)
  sudo mount -t cifs "$SMB_SHARE" "$MOUNT_POINT" \
    -o "credentials=${SMB_CRED_FILE},vers=3.0,iocharset=utf8,uid=${MSSQL_UID},gid=${MSSQL_GID},file_mode=0660,dir_mode=0770,nosuid,nodev" \
    || fail "SMB 挂载失败"
else
  log "SMB 已挂载: $MOUNT_POINT"
fi

run_sql() {
  sqlcmd -S "$SERVER" -U "$SQLUSER" -b -l 60 -d "master" -N -Q "$1"
}

# 若指定完整备份,先恢复完整备份(+可选差异)
if [[ -n "$FULL_BAK" ]]; then
  [[ -f "$FULL_BAK" ]] || fail "未找到完整备份文件: $FULL_BAK"
  log "准备从完整备份恢复: $FULL_BAK"

  # 生成MOVE映射并恢复到 NORECOVERY
  REPLACE_OPT=$([[ "$REPLACE" == "true" ]] && echo ", REPLACE" || echo "")
  TSQL=$(cat <<EOF
DECLARE @db sysname = N'${DATABASE}';
DECLARE @full nvarchar(4000) = N'${FULL_BAK}';
DECLARE @dataDir nvarchar(4000) = N'${TARGET_DATA_DIR}';
DECLARE @logDir nvarchar(4000) = N'${TARGET_LOG_DIR}';
DECLARE @moves nvarchar(max) = N'';
DECLARE @cmd nvarchar(max) = N'RESTORE FILELISTONLY FROM DISK=N''' + REPLACE(@full,'''','''''') + N''''';

DECLARE @FileList TABLE (LogicalName sysname, [Type] char(1), FileId int);
INSERT INTO @FileList (LogicalName,[Type],FileId)
EXEC(@cmd);

WITH cte AS (
  SELECT LogicalName, [Type], FileId,
         ROW_NUMBER() OVER (PARTITION BY [Type] ORDER BY FileId) AS rn
  FROM @FileList
)
SELECT @moves = @moves + N', MOVE N''' + LogicalName + N''' TO N''' +
  CASE WHEN [Type] = 'L'
       THEN @logDir + N'/' + @db + N'_' + LogicalName + N'.ldf'
       ELSE @dataDir + N'/' + @db + N'_' + LogicalName + CASE WHEN rn=1 THEN N'.mdf' ELSE N'.ndf' END
  END + N''''
FROM cte
ORDER BY FileId;

DECLARE @restore nvarchar(max) = N'RESTORE DATABASE ' + QUOTENAME(@db) +
  N' FROM DISK=N''' + REPLACE(@full,'''','''''') + N''' WITH NORECOVERY, CHECKSUM' + @moves ${REPLACE_OPT} + N', STATS=10;';
PRINT @restore;
EXEC(@restore);
EOF
)
  run_sql "$TSQL"
  log "完整备份已恢复到 NORECOVERY 状态"

  if [[ -n "$DIFF_BAK" ]]; then
    [[ -f "$DIFF_BAK" ]] || fail "未找到差异备份文件: $DIFF_BAK"
    log "恢复差异备份: $DIFF_BAK"
    run_sql "RESTORE DATABASE [${DATABASE}] FROM DISK=N'${DIFF_BAK}' WITH NORECOVERY, CHECKSUM, STATS=10;"
    log "差异备份已恢复到 NORECOVERY"
  fi
else
  # 未提供完整备份,要求数据库已处于 RESTORING 状态,用于继续追加日志
  STATE=$(sqlcmd -S "$SERVER" -U "$SQLUSER" -b -l 30 -N -h -1 -W -Q "SET NOCOUNT ON;SELECT state_desc FROM sys.databases WHERE name='${DATABASE}';")
  if [[ "$STATE" != "RESTORING" ]]; then
    fail "未指定完整备份,且数据库当前状态为 '${STATE}'。请先恢复完整备份到 NORECOVERY 再追加日志。"
  fi
fi

# 收集日志备份文件(按文件名时间顺序)
log "收集日志备份文件: $BACKUP_DIR/${FILE_PREFIX}_*.trn"
mapfile -t LOGFILES < <(find "$BACKUP_DIR" -maxdepth 1 -type f -name "${FILE_PREFIX}_*.trn" | sort)
[[ ${#LOGFILES[@]} -gt 0 ]] || fail "未找到任何日志备份文件"

# 逐个恢复日志
COUNT=${#LOGFILES[@]}
log "开始恢复日志链(文件数: $COUNT)"
for ((i=0; i<COUNT; i++)); do
  f="${LOGFILES[$i]}"
  if [[ $i -lt $((COUNT-1)) ]]; then
    # 中间日志:NORECOVERY
    log "RESTORE LOG -> $f (NORECOVERY)"
    run_sql "RESTORE LOG [${DATABASE}] FROM DISK=N'${f}' WITH NORECOVERY, CHECKSUM, STATS=5;"
  else
    # 最后一个日志:根据STOPAT决定
    if [[ -n "$STOPAT" ]]; then
      log "RESTORE LOG -> $f (STOPAT='${STOPAT}', RECOVERY)"
      run_sql "RESTORE LOG [${DATABASE}] FROM DISK=N'${f}' WITH STOPAT='${STOPAT}', RECOVERY, CHECKSUM, STATS=5;"
    else
      log "RESTORE LOG -> $f (RECOVERY)"
      run_sql "RESTORE LOG [${DATABASE}] FROM DISK=N'${f}' WITH RECOVERY, CHECKSUM, STATS=5;"
    fi
  fi
done

log "日志恢复完成。数据库应已联机(RECOVERY)。"
exit 0

说明与要点:

  • 如需完整恢复,请提供 FULL_BAK(和可选 DIFF_BAK)。脚本自动生成 MOVE 映射并将数据库恢复至 NORECOVERY,再串接日志。
  • 如不提供 FULL_BAK,要求数据库已在 RESTORING 状态(例如已由其他流程恢复了完整/差异备份)。
  • 日志链按文件名排序恢复,命名规则确保时间顺序;RESTORE 在链断裂时会失败并中止。
  • 最后一条日志根据是否指定 STOPAT 决定使用 STOPAT/RECOVERY。
  • 使用 WITH CHECKSUM 验证备份页校验。
  1. 配置参数 请根据实际环境修改以下关键参数:
  • SQL 连接

    • SERVER:建议使用与服务器证书 CN/SAN 匹配的 FQDN,例如 sql01.prod.example.com(满足 encrypt=True; trust_server_certificate=False)。
    • DATABASE:crm_prod
    • SQLUSER:backup_svc
    • 密码:通过环境变量 MSSQL_BACKUP_PASS 注入,脚本内自动导出 SQLCMDPASSWORD。勿将密码写入文件。
  • SMB 存储

    • SMB_SHARE:\10.1.2.50\sqlbackups\crm(脚本中以 //10.1.2.50/sqlbackups/crm 使用)
    • SMB_CRED_FILE:/etc/backup/smb.cred(权限建议 600,内容格式:

示例详情

解决的问题

把数据库备份与恢复这件高风险、强依赖专家的工作,变成一条可复用、可落地、可审计的标准化产线。通过输入数据库类型、备份频率、存储位置三项关键信息,即可一键生成贴合业务场景的备份与恢复脚本方案:

  • 直接可用的生产级脚本,自动补齐异常处理、日志记录、校验与加密等安全细节
  • 智能匹配全量与增量策略,兼顾备份窗口与恢复时效,减少停机时间
  • 提供清晰的执行手册与演练步骤,降低上手门槛,帮助团队快速验证与上线
  • 一套方案覆盖多种主流数据库与多平台环境,降低跨系统维护成本 目标是让运维、DBA、SRE 与研发团队在数分钟内拿到可部署的脚本与操作说明,显著减少人工编写与反复踩坑,稳住业务连续性,达成合规要求,并以可量化的效率收益带动试用与付费转化。

适用用户

数据库管理员(DBA)

为多类型数据库快速制定备份矩阵,生成全量/增量与恢复演练脚本,统一日志与权限,缩短停机窗口。

运维工程师/DevOps

搭建标准化备份体系,按日周月自动化执行与归档,遇到故障按指引迅速恢复与回滚,降低恢复时间。

初创公司技术负责人

无需专职DBA也能落地安全可审计的备份方案,支持云存储与成本优化,保障上线与合规审查。

特征总结

支持MySQL、Oracle、PostgreSQL等,一键生成对应脚本,省去查文档与反复试错。
根据备份频率与存储位置自动设计全量/增量方案,兼顾窗口时长与恢复目标。
内置错误处理与日志记录,关键步骤清晰提示,脚本可直接落地生产环境。
默认启用加密与访问控制建议,避免明文凭证与越权操作,满足审计与合规。
自动生成恢复与校验流程,含演练指引与回滚路径,确保数据可恢复可验证。
兼容Windows与Linux环境,一键调用即可运行,减少环境差异带来的运维成本。
提供可参数化模板与示例配置,轻松按业务分库分表定制并批量复用。
同步输出使用说明与故障排查手册,新人也能快速上手并独立完成交付。
智能建议本地与云端存储策略,自动规划留存周期与空间占用,降低成本。
为迁移与容灾场景生成专用脚本,支持快速切换与最小停机,稳定托底关键发布。

如何使用购买的提示词模板

1. 直接在外部 Chat 应用中使用

将模板生成的提示词复制粘贴到您常用的 Chat 应用(如 ChatGPT、Claude 等),即可直接对话使用,无需额外开发。适合个人快速体验和轻量使用场景。

2. 发布为 API 接口调用

把提示词模板转化为 API,您的程序可任意修改模板参数,通过接口直接调用,轻松实现自动化与批量处理。适合开发者集成与业务系统嵌入。

3. 在 MCP Client 中配置使用

在 MCP client 中配置对应的 server 地址,让您的 AI 应用自动调用提示词模板。适合高级用户和团队协作,让提示词在不同 AI 工具间无缝衔接。

AI 提示词价格
¥30.00元
先用后买,用好了再付款,超安全!

您购买后可以获得什么

获得完整提示词模板
- 共 724 tokens
- 5 个可调节参数
{ 数据库类型 } { 数据库连接信息 } { 备份方式 } { 备份频率 } { 备份存储位置说明 }
获得社区贡献内容的使用权
- 精选社区优质案例,助您快速上手提示词
使用提示词兑换券,低至 ¥ 9.9
了解兑换券 →
限时半价

不要错过!

半价获取高级提示词-优惠即将到期

17
:
23
小时
:
59
分钟
:
59