本篇为历史文档的升级版本,支持瀚高,支持动态配置信息,傻瓜式使用。本文撰写大量参考AI给出的建议。
本文脚本放在最后,如果您想直接阅读脚本,请直接翻阅至最后。
1. 方案概述
本套脚本旨在为 MySQL 和 HighGo (瀚高) 数据库提供简单、低成本的全量冷备方案。支持物理机直接部署和 Docker 容器化部署,并可灵活选择单机本地备份或双机异地容灾。
1.1 核心流程图
+----------------+ +----------------+
| 主服务器 (A) | | 从服务器 (B) |
| (Script: Master)| | (Script: Slave)|
+--------+-------+ +--------+-------+
| |
1. 导出数据 (mysqldump/pg_dump) |
| |
2. 流式压缩 (.sql.gz) |
| |
3. 传输文件 (SSH/SCP) [可选步骤] -----------> 4. 接收文件
(若配置了 SLAVE_IP 则传输,否则跳过) |
| |
5. 清理 A 本地旧备份 6. 清理 B 本地旧备份
(基于文件名日期的精准清理) (基于文件名日期的精准清理)
1.2 功能亮点
- 灵活部署:支持单机模式! 如果不配置从服务器 IP,脚本仅执行本地备份和清理。
- 零依赖:支持 Docker 模式,宿主机无需安装任何数据库客户端工具(如
mysql或psql),脚本直接穿透容器执行。 - 通用性强:一套脚本同时支持 MySQL 和 HighGo,通过配置切换。
- 安全性高:若启用双机模式,支持 SSH 免密互信,脚本中无需明文存储 Linux 系统密码。
- 节省空间:导出过程直接经过 gzip 压缩,不占用临时磁盘空间。
2. 部署前置条件 (仅双机模式需要)
⚠️ 如果您只进行单机本地备份,可直接跳过本节。
若需要将备份传输到异地从服务器,必须配置 SSH 免密登录。
2.1 环境假设
- 主服务器 (Master) IP:
192.168.1.10 - 从服务器 (Slave) IP:
192.168.1.200
2.2 配置免密互信 (二选一)
方案 A:自动配置 (推荐,需知晓从机密码)
在 主服务器 上执行:
- 生成密钥(如果已经生成可以跳过):
ssh-keygen -t rsa(一路回车) - 下发公钥:
ssh-copy-id root@192.168.1.200 - 验证:
ssh root@192.168.1.200(无需密码即成功)
方案 B:手动配置 (无需密码,适合云平台)
- 主服务器:生成密钥(如果已经生成可以跳过)
ssh-keygen -t rsa(一路回车) - 主服务器:
cat ~/.ssh/id_rsa.pub并复制内容。 - 从服务器:编辑
~/.ssh/authorized_keys,粘贴公钥内容。 - 从服务器:执行
chmod 600 ~/.ssh/authorized_keys(至关重要)。
3. 安装与目录结构
建议在服务器上创建统一目录:
-
创建目录:
mkdir -p /opt/db_backup -
上传文件:将
db_backup.sh和db_backup.conf上传至该目录。 -
赋予权限:
chmod +x /opt/db_backup/db_backup.sh chmod 600 /opt/db_backup/db_backup.conf
4. 配置文件详解 (db_backup.conf)
4.1 基础参数 (所有场景通用)
| 参数 | 说明 | 示例 |
|---|---|---|
SCRIPT_ROLE | 脚本角色。 MASTER: 执行备份 (+传输) + 清理 SLAVE: 仅执行清理 (从库用) | "MASTER" |
DB_TYPE | 数据库类型。支持 MYSQL 或 HIGHGO | "MYSQL" |
BACKUP_DIR | 宿主机备份文件存储路径 | "/data/backup" |
LOG_FILE | 日志文件路径 | "/var/log/db_backup.log" |
4.2 场景化配置 (重点:Docker 设置)
🐳 场景一:数据库运行在 Docker 容器中
这是目前最常见的部署方式。
| 参数 | 配置说明 |
|---|---|
DOCKER_CONTAINER_NAME | 填写容器名称或容器 ID (例如 "mysql_prod")。 ⚠️ 只要此项不为空,脚本就会启用 Docker 模式。 |
DB_HOST | 必须填 127.0.0.1 (因为命令是在容器内部执行的)。 |
💻 场景二:数据库直接安装在宿主机
| 参数 | 配置说明 |
|---|---|
DOCKER_CONTAINER_NAME | 必须留空 ("")。 |
DB_HOST | 填写 127.0.0.1 或本机 IP。 |
4.3 传输配置 (仅 Master 需要)
⚠️ 单机/双机模式切换开关
| 参数 | 说明 |
|---|---|
SLAVE_IP | 关键开关。 1. 开启双机备份:填写从服务器 IP (如 "192.168.1.200")。 2. 仅单机备份:留空 (即 SLAVE_IP=""),脚本将跳过传输步骤。 |
SLAVE_USER | SSH 用户名 (通常 root) |
SLAVE_PORT | SSH 端口 (通常 22) |
SLAVE_DEST_DIR | 从服务器接收文件的路径 |
5. 配置示例速查
示例 A:MySQL Docker 版 (单机备份)
SCRIPT_ROLE="MASTER"
DB_TYPE="MYSQL"
DOCKER_CONTAINER_NAME="mysql-container-v1" # Docker 模式
DB_HOST="127.0.0.1"
DB_PORT="3306"
DB_USER="root"
DB_PASS="123456"
DB_LIST="order_db"
SLAVE_IP="" # <--- 留空,表示不传输,仅本地备份
MASTER_KEEP_DAYS=7
示例 B:HighGo (瀚高) 物理机版 (双机互备)
SCRIPT_ROLE="MASTER"
DB_TYPE="HIGHGO"
DOCKER_CONTAINER_NAME="" # 物理机模式
DB_HOST="localhost"
DB_PORT="5866"
DB_USER="sysdba"
DB_PASS="Highgo@123"
DB_LIST="highgo_test"
SLAVE_IP="192.168.1.200" # <--- 配置 IP,开启传输
SLAVE_DEST_DIR="/data/backup"
6. 定时任务 (Crontab)
使用 crontab -e 添加计划任务。
主服务器 (Master)
# 每天凌晨 02:00 执行全量备份
0 2 * * * /bin/bash /opt/db_backup/db_backup.sh
从服务器 (Slave - 仅双机模式需要)
# 每天凌晨 03:00 清理过期备份
0 3 * * * /bin/bash /opt/db_backup/db_backup.sh
7. 灾难恢复指南
当需要恢复数据时,请先将 .sql.gz 文件解压,然后导入。
7.1 MySQL 恢复
Docker 环境:
# 1. 解压数据流 -> 2. 传入容器 -> 3. 执行导入
gunzip < backup_file.sql.gz | docker exec -i <容器名> mysql -u root -p<密码> <数据库名>
物理机环境:
gunzip < backup_file.sql.gz | mysql -u root -p<密码> <数据库名>
7.2 HighGo (瀚高) 恢复
Docker 环境:
# 注意: HighGo 容器内通常使用 psql 工具
gunzip < backup_file.sql.gz | docker exec -i <容器名> psql -h 127.0.0.1 -p 5866 -U sysdba -d <数据库名>
物理机环境:
gunzip < backup_file.sql.gz | psql -h localhost -p 5866 -U sysdba -d <数据库名>
8. 常见问题 (FAQ)
Q1: 脚本执行报错 /db_backup/db_backup.conf: line 2: $'\r': command not found
- 原因: 脚本文件 (
db_backup.sh) 或配置文件 (db_backup.conf) 是在 Windows 环境下编辑或保存的。 - 解决: 执行命令:
sed -i 's/\r$//' db_backup.sh db_backup.conf
Q2: 脚本执行报错 Permission denied (publickey)
- 原因: 双机模式下 SSH 免密互信未配置成功。
- 解决: 参考第 2 节重新配置。如果是单机模式,请确保
SLAVE_IP已置空。
Q3: 提示 mysqldump: command not found
- 原因: 物理机模式下宿主机没装客户端,或者 Docker 模式下
DOCKER_CONTAINER_NAME忘填了。 - 解决: 检查
DOCKER_CONTAINER_NAME配置。
Q4: 为什么旧文件没被删除?
- 原因: 脚本现在使用文件名中的日期 (
YYYYMMDD_HHMMSS) 来判断文件年龄。 - 解决: 请检查旧文件的命名格式是否为
库名_YYYYMMDD_HHMMSS.sql.gz。如果命名不符合此规范,脚本为防止误删会跳过该文件。
9. 脚本内容
9.1 db_backup.conf (配置文件)
# ================= 核心模式配置 =================
# 数据库类型: MYSQL 或 HIGHGO
DB_TYPE="MYSQL"
# ⚠️ [Docker 支持]
# 填写容器名称(如 "mysql_prod")开启 Docker 模式;留空则为宿主机模式
DOCKER_CONTAINER_NAME="mysql_prod"
# 脚本角色: MASTER (备份+传输+清理) 或 SLAVE (仅清理)
SCRIPT_ROLE="MASTER"
# ================= 基础路径 =================
BACKUP_DIR="/data/backup/database"
LOG_FILE="/var/log/db_backup.log"
# ================= 数据库连接配置 (Master 填写) =================
DB_HOST="127.0.0.1"
DB_PORT="3306"
DB_USER="root"
DB_PASS="你的数据库密码"
# 多个数据库使用空格隔开
DB_LIST="db_project_a db_project_b"
# ================= 传输配置 (Master 填写) =================
# ⚠️ [可选功能] ⚠️
# 如果需要双机异地备份,请填写从服务器信息(需配置 SSH 免密互信)。
# 如果只需要单机本地备份,请将 SLAVE_IP 留空 (""),脚本将自动跳过传输步骤。
SLAVE_IP="192.168.1.200"
SLAVE_PORT="22"
SLAVE_USER="root"
SLAVE_DEST_DIR="/data/backup/database"
# ================= 清理策略 =================
MASTER_KEEP_DAYS=7
SLAVE_KEEP_DAYS=30
9.2 db_backup.sh (脚本)
#!/bin/bash
# ================= 解决 Crontab 环境变量问题 (新增) =================
# 1. 强制加载系统级环境变量
source /etc/profile
[ -f ~/.bash_profile ] && source ~/.bash_profile
[ -f ~/.bashrc ] && source ~/.bashrc
# 2. 显式补充常用路径 (防止 source 没生效)
export PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin:/root/bin:$PATH
# ================= 环境准备 =================
SCRIPT_DIR=$(cd $(dirname $0); pwd)
CONF_FILE="$SCRIPT_DIR/db_backup.conf"
if [ -f "$CONF_FILE" ]; then
source "$CONF_FILE"
else
echo "Error: 配置文件 $CONF_FILE 不存在!"
exit 1
fi
log() {
echo "[$(date +'%Y-%m-%d %H:%M:%S')] [$DB_TYPE] $1" | tee -a "$LOG_FILE"
}
if [ ! -d "$BACKUP_DIR" ]; then
mkdir -p "$BACKUP_DIR"
fi
DATE_STR=$(date +%Y%m%d_%H%M%S)
# ================= 辅助函数:根据文件名清理过期备份 =================
# 原理:提取文件名中的 YYYYMMDD_HHMMSS,格式化为标准日期后对比时间戳
cleanup_by_filename() {
local target_dir=$1
local keep_days=$2
local current_timestamp=$(date +%s)
local threshold_seconds=$(($keep_days * 86400))
local cutoff_timestamp=$(($current_timestamp - $threshold_seconds))
local cutoff_date_str=$(date -d @$cutoff_timestamp "+%Y-%m-%d %H:%M:%S")
log ">>> 执行清理检查: 保留天数=$keep_days (截止时间: $cutoff_date_str)"
shopt -s nullglob
for file in "$target_dir"/*.sql.gz; do
filename=$(basename "$file")
# 匹配格式:任意前缀_YYYYMMDD_HHMMSS.sql.gz
if [[ $filename =~ _([0-9]{8})_([0-9]{6})\.sql\.gz$ ]]; then
d_part="${BASH_REMATCH[1]}"
t_part="${BASH_REMATCH[2]}"
# 手动拼接成 ISO 格式: YYYY-MM-DD HH:MM:SS
year=${d_part:0:4}
month=${d_part:4:2}
day=${d_part:6:2}
hour=${t_part:0:2}
minute=${t_part:2:2}
second=${t_part:4:2}
formatted_date="$year-$month-$day $hour:$minute:$second"
file_timestamp=$(date -d "$formatted_date" +%s 2>/dev/null)
if [ $? -eq 0 ]; then
if [ "$file_timestamp" -lt "$cutoff_timestamp" ]; then
log "🗑️ [删除] 过期文件: $filename (日期: $formatted_date)"
rm -f "$file"
else
:
fi
else
log "⚠️ [跳过] 日期解析失败: $filename"
fi
else
log "⚠️ [跳过] 命名格式不符: $filename"
fi
done
shopt -u nullglob
log ">>> 清理检查完成."
}
# ================= 核心逻辑 =================
if [ "$SCRIPT_ROLE" == "MASTER" ]; then
log "=== 开始执行 MASTER 备份任务 ==="
if [ -n "$DOCKER_CONTAINER_NAME" ]; then
log "检测到 Docker 模式,容器名称: $DOCKER_CONTAINER_NAME"
fi
for DB_NAME in $DB_LIST; do
FILE_NAME="${DB_NAME}_${DATE_STR}.sql.gz"
FILE_PATH="$BACKUP_DIR/$FILE_NAME"
log "正在备份数据库: $DB_NAME ..."
# >>>>>> 备份逻辑 <<<<<<
BACKUP_STATUS=1
if [ "$DB_TYPE" == "MYSQL" ]; then
if [ -n "$DOCKER_CONTAINER_NAME" ]; then
docker exec "$DOCKER_CONTAINER_NAME" mysqldump -h"$DB_HOST" -P"$DB_PORT" -u"$DB_USER" -p"$DB_PASS" \
--single-transaction --set-gtid-purged=OFF "$DB_NAME" 2>/dev/null | gzip > "$FILE_PATH"
BACKUP_STATUS=${PIPESTATUS[0]}
else
mysqldump -h"$DB_HOST" -P"$DB_PORT" -u"$DB_USER" -p"$DB_PASS" \
--single-transaction --set-gtid-purged=OFF "$DB_NAME" 2>/dev/null | gzip > "$FILE_PATH"
BACKUP_STATUS=${PIPESTATUS[0]}
fi
elif [ "$DB_TYPE" == "HIGHGO" ]; then
if [ -n "$DOCKER_CONTAINER_NAME" ]; then
docker exec -e PGPASSWORD="$DB_PASS" "$DOCKER_CONTAINER_NAME" \
pg_dump -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" | gzip > "$FILE_PATH"
BACKUP_STATUS=${PIPESTATUS[0]}
else
export PGPASSWORD="$DB_PASS"
pg_dump -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" | gzip > "$FILE_PATH"
BACKUP_STATUS=${PIPESTATUS[0]}
unset PGPASSWORD
fi
else
log "❌ 错误: 未知的 DB_TYPE ($DB_TYPE)"
exit 1
fi
if [ $BACKUP_STATUS -eq 0 ]; then
FILE_SIZE=$(stat -c%s "$FILE_PATH" 2>/dev/null || echo 0)
if [ "$FILE_SIZE" -gt 20 ]; then
log "备份成功: $FILE_NAME (大小: $(du -h "$FILE_PATH" | cut -f1))"
# >>>>>> 传输逻辑 (关键修改) <<<<<<
if [ -n "$SLAVE_IP" ]; then
log "正在传输到从服务器 ($SLAVE_IP) ..."
# 1. 先尝试在远程创建目录 (防止目录不存在导致scp失败)
# -o StrictHostKeyChecking=no: 禁止交互式询问指纹
ssh -p "$SLAVE_PORT" -o StrictHostKeyChecking=no "$SLAVE_USER@$SLAVE_IP" "mkdir -p $SLAVE_DEST_DIR" >> "$LOG_FILE" 2>&1
# 2. 执行 SCP 传输
scp -P "$SLAVE_PORT" -o StrictHostKeyChecking=no "$FILE_PATH" "$SLAVE_USER@$SLAVE_IP:$SLAVE_DEST_DIR" >> "$LOG_FILE" 2>&1
if [ $? -eq 0 ]; then
log "传输成功."
else
log "❌ 传输失败! 请检查日志详情 (可能是网络、互信或磁盘满)。"
fi
else
log "ℹ️ 跳过传输: 未配置从服务器 IP (单机模式)"
fi
else
log "❌ 备份失败: 文件大小异常。"
rm -f "$FILE_PATH"
fi
else
log "❌ 备份失败: 命令执行错误。"
rm -f "$FILE_PATH"
fi
done
cleanup_by_filename "$BACKUP_DIR" "$MASTER_KEEP_DAYS"
elif [ "$SCRIPT_ROLE" == "SLAVE" ]; then
log "=== 开始执行 SLAVE 清理任务 ==="
if [ -d "$BACKUP_DIR" ]; then
cleanup_by_filename "$BACKUP_DIR" "$SLAVE_KEEP_DAYS"
else
log "❌ 备份目录不存在。"
fi
else
log "❌ SCRIPT_ROLE 错误"
exit 1
fi