热门角色不仅是灵感来源,更是你的效率助手。通过精挑细选的角色提示词,你可以快速生成高质量内容、提升创作灵感,并找到最契合你需求的解决方案。让创作更轻松,让价值更直接!
我们根据不同用户需求,持续更新角色库,让你总能找到合适的灵感入口。
本提示词用于根据用户提供的数据库类型、备份频率、存储位置等关键参数,生成可直接部署的数据库备份与恢复脚本。所有逻辑均基于用户显式输入,不依赖外部数据推断,确保脚本安全、可控与可复现。适用于企业运维、灾备设计和自动化流程构建,可提供完整脚本、配置信息、执行方法与故障排查要点,帮助提升数据库运维效率与数据保护可靠性。
以下为基于 MySQL 8.0 的企业级全量备份/恢复脚本方案,支持:
注意:
文件名建议:/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 连接
导出选项
S3 兼容存储
暂存和保留
恢复脚本
无法连接 MySQL(SSL 或认证失败)
备份文件过小或为空
S3 上传失败或慢
校验失败(SHA256 不一致)
保留清理未生效
恢复阻塞或耗时过长
权限建议
以上脚本遵循安全最佳实践:不在脚本中保存明文密码;使用 TLS 校验;使用 SSE 加密;包含错误处理、日志与可恢复性验证。可直接在生产环境部署,按需调整参数与策略。
本方案面向 PostgreSQL 14,采用“持续WAL日志归档 + 定期全量基准备份”的组合策略,满足“日志备份、每小时频率、NFS远端存储、Zstandard压缩、SHA256校验、SSL加密连接、pgpass认证、备份可验证”的要求。
目录约定(均可在脚本中调整):
以下为三个备份相关脚本,均为完整可执行脚本:
另外提供 systemd 单元文件保持 WAL 归档常驻运行。
注意:请先阅读“配置参数”章节并按需修改脚本头部配置变量。
#!/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
[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/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/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/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
请根据实际环境调整以下参数(均在脚本头部):
安全注意:
01_wal_archiver.sh 启动失败
WAL 未生成 .zst 或数量过少
03_base_backup.sh 报错
校验失败(sha256 对不上)
恢复失败:启动后无法找到 WAL
PITR 到达目标时间但未恢复到期望状态
性能建议
如需进一步适配(例如以 archive_command 方式压缩推送而非 pg_receivewal 拉取、基备使用并行、多仓库冗余等),可在现有脚本基础上进行扩展。
以下方案基于“SQL Server 2019 + SMB 共享存储 + 每小时日志备份 + 48 小时保留 + 备份压缩 + VERIFY 验证 + 并发=1”的需求设计,脚本适配 Linux 上的 SQL Server(使用 sqlcmd),通过 CIFS 挂载 SMB 共享,符合加密连接要求且不在脚本中存储明文密码。备份脚本包含完善的错误处理、日志记录、并发控制和备份可用性验证;恢复脚本支持在已有完整备份(可选差异备份)基础上按时间点恢复日志链或恢复至最新。
重要安全提示:
备份脚本(backup_log.sh)
恢复脚本(restore_logs.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
说明与要点:
#!/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
说明与要点:
SQL 连接
SMB 存储
把数据库备份与恢复这件高风险、强依赖专家的工作,变成一条可复用、可落地、可审计的标准化产线。通过输入数据库类型、备份频率、存储位置三项关键信息,即可一键生成贴合业务场景的备份与恢复脚本方案:
为多类型数据库快速制定备份矩阵,生成全量/增量与恢复演练脚本,统一日志与权限,缩短停机窗口。
搭建标准化备份体系,按日周月自动化执行与归档,遇到故障按指引迅速恢复与回滚,降低恢复时间。
无需专职DBA也能落地安全可审计的备份方案,支持云存储与成本优化,保障上线与合规审查。
将模板生成的提示词复制粘贴到您常用的 Chat 应用(如 ChatGPT、Claude 等),即可直接对话使用,无需额外开发。适合个人快速体验和轻量使用场景。
把提示词模板转化为 API,您的程序可任意修改模板参数,通过接口直接调用,轻松实现自动化与批量处理。适合开发者集成与业务系统嵌入。
在 MCP client 中配置对应的 server 地址,让您的 AI 应用自动调用提示词模板。适合高级用户和团队协作,让提示词在不同 AI 工具间无缝衔接。
半价获取高级提示词-优惠即将到期