一、实现目标
生产环境中,MySQL 从库每天执行一次全量物理备份,备份完成后自动压缩,并通过 rsync 传输到异地备份服务器。
整体流程:
MySQL 从库
↓
XtraBackup 全量备份
↓
tar.gz 压缩
↓
本地保留 7 天
↓
rsync 异地传输
↓
异地保留 30 天
示例环境:
MySQL 主库:192.168.10.31
MySQL 从库:192.168.10.32
MySQL 端口:13307
异地服务器:192.168.20.15
本地备份目录:
/data/backup/mysql/13307
异地备份目录:
/home/192.168.10.32_13307
如果后续新增 13308,传输脚本会自动创建:
/home/192.168.10.32_13308
二、整体实现逻辑
只配置一个定时任务:
cron
↓
backup.sh
↓
XtraBackup 成功
↓
tar.gz 压缩成功
↓
调用 transfer_mysql_backup.sh
↓
rsync 到异地服务器
不再单独给传输脚本设置固定时间。
这样做的好处是:不需要猜数据库什么时候备份完成。
例如数据库增长以后,备份时间可能从 2 小时变成 3 小时甚至更久,但只要压缩没有完成,就不会触发异地传输。
三、备份脚本
脚本路径:
/app/tools/backup.sh
完整脚本:
#!/bin/bash
set -eo pipefail
source /etc/profile
###########################################################
# 全局配置区
###########################################################
MYSQL_BASE_DIR="/data/mysql"
XTRABACKUP_BIN="/app/tools/xtrabackup/bin/xtrabackup"
BK_BASE_ROOT="/data/backup/mysql"
BK_KEEP_DAYS=7
MYSQL_USER="root"
# 修改为/app/tools/pwd
PWD_STORE_DIR="/app/tools/pwd"
###########################################################
# 函数定义
###########################################################
# 带端口日志输出
log() {
local port="$1"
local msg="$2"
echo "[$(date '+%Y-%m-%d %H:%M:%S')] [端口${port}] ${msg}" >> "${LOG_FILE}"
echo "[$(date '+%Y-%m-%d %H:%M:%S')] [端口${port}] ${msg}"
}
# 清理单个实例过期压缩包
clean_expire_backup() {
local bk_path="$1"
find "${bk_path}" -maxdepth 1 -name "*.tar.gz" -type f -mtime +${BK_KEEP_DAYS} -delete
}
###########################################################
# 全局前置校验
###########################################################
# 1. 检查xtrabackup程序
if [ ! -x "${XTRABACKUP_BIN}" ]; then
echo "ERROR: xtrabackup 不存在或无执行权限 -> ${XTRABACKUP_BIN}"
exit 1
fi
# 2. 密码存储目录不存在则创建并加固权限
mkdir -p "${PWD_STORE_DIR}"
chmod 700 "${PWD_STORE_DIR}"
# 3. 获取/data/mysql下所有纯数字端口文件夹
PORT_LIST=$(ls -d "${MYSQL_BASE_DIR}"/[0-9]* 2>/dev/null | xargs -I {} basename {})
if [ -z "${PORT_LIST}" ]; then
echo "未找到任何数字命名的MySQL实例目录,脚本退出"
exit 0
fi
echo "==================== 待备份端口列表:${PORT_LIST} ===================="
###########################################################
# 循环逐个端口备份
###########################################################
for DB_PORT in ${PORT_LIST}; do
echo -e "\n>>>>>>>>>> 开始处理端口 ${DB_PORT} <<<<<<<<<<"
# 1. 定义当前实例所有路径
PWD_FILE="${PWD_STORE_DIR}/${DB_PORT}.pwd"
INSTANCE_CONF="${MYSQL_BASE_DIR}/${DB_PORT}/conf/my.cnf"
INSTANCE_BK_ROOT="${BK_BASE_ROOT}/${DB_PORT}"
BK_TIME=$(date +%Y%m%d_%H%M%S)
BK_FULL_DIR="${INSTANCE_BK_ROOT}/${BK_TIME}"
LOG_DIR="${INSTANCE_BK_ROOT}/log"
LOG_FILE="${LOG_DIR}/mysql_backup_$(date +%Y%m%d).log"
# 创建备份&日志目录
mkdir -p "${BK_FULL_DIR}"
mkdir -p "${LOG_DIR}"
# 2. 校验密码文件是否存在
if [ ! -f "${PWD_FILE}" ]; then
log "${DB_PORT}" "ERROR: 密码文件 ${PWD_FILE} 不存在,跳过本实例"
continue
fi
# 校验权限是否600,防止泄露
PWD_PERM=$(stat -c "%a" "${PWD_FILE}")
if [ "${PWD_PERM}" != "600" ]; then
log "${DB_PORT}" "WARN: 密码文件权限非600,自动修正"
chmod 600 "${PWD_FILE}"
fi
# 读取当前端口密码
MYSQL_PASSWORD=$(cat "${PWD_FILE}")
# 3. 校验my.cnf配置文件
if [ ! -f "${INSTANCE_CONF}" ]; then
log "${DB_PORT}" "ERROR: ${INSTANCE_CONF} 不存在,跳过"
continue
fi
log "${DB_PORT}" "===== ${DB_PORT} 全量物理备份开始 ====="
log "${DB_PORT}" "配置文件: ${INSTANCE_CONF}"
log "${DB_PORT}" "密码文件: ${PWD_FILE}"
# 4. 执行xtrabackup备份
if ${XTRABACKUP_BIN} \
--defaults-file="${INSTANCE_CONF}" \
--user="${MYSQL_USER}" \
--password="${MYSQL_PASSWORD}" \
--backup \
--target-dir="${BK_FULL_DIR}"; then
log "${DB_PORT}" "xtrabackup原始备份生成成功"
else
log "${DB_PORT}" "ERROR: xtrabackup备份失败,跳过压缩"
continue
fi
# 5. tar打包压缩
cd "${INSTANCE_BK_ROOT}" || {
log "${DB_PORT}" "ERROR: 进入备份目录失败"
continue
}
TAR_NAME="${BK_TIME}.tar.gz"
tar -zcf "${TAR_NAME}" "${BK_TIME}"
TAR_EXIT=$?
cd - >/dev/null
if [ ${TAR_EXIT} -eq 0 ]; then
log "${DB_PORT}" "压缩包已生成: ${INSTANCE_BK_ROOT}/${TAR_NAME}"
rm -rf "${BK_FULL_DIR}"
log "${DB_PORT}" "原始备份目录已删除"
# 异地备份:备份成功后执行异地传输
if /bin/bash /app/tools/transfer_mysql_backup.sh \
"${DB_PORT}" \
"${INSTANCE_BK_ROOT}/${TAR_NAME}"; then
log "${DB_PORT}" "异地备份传输成功"
else
log "${DB_PORT}" "WARN: 本地备份成功,但异地备份传输失败"
fi
else
log "${DB_PORT}" "ERROR: tar压缩失败,保留源目录排查"
continue
fi
# 6. 清理7天前过期备份
clean_expire_backup "${INSTANCE_BK_ROOT}"
log "${DB_PORT}" "端口${DB_PORT}备份完成"
done
echo -e "\n==================== 全部实例备份循环结束 ===================="
exit 0
主要功能:
1. 自动扫描 /data/mysql 下的数字端口目录
2. 使用 XtraBackup 执行全量物理备份
3. 将备份目录压缩成 tar.gz
4. 压缩成功后删除原始备份目录
5. 自动调用异地传输脚本
6. 本地备份保留 7 天
四、异地传输脚本
脚本路径:
/app/tools/transfer_mysql_backup.sh
完整脚本:
#!/bin/bash
# 接收 backup.sh 传入的端口和备份文件
DB_PORT="$1"
BACKUP_FILE="$2"
# 异地备份服务器
REMOTE_USER="root"
REMOTE_HOST="192.168.20.15"
# 根据源服务器IP和实例端口自动生成异地目录
REMOTE_DIR="/home/192.168.10.32_${DB_PORT}"
# 传输日志
LOG_DIR="/opt/mysql_backup/log"
LOG_FILE="${LOG_DIR}/transfer_${DB_PORT}_$(date +%Y%m%d).log"
# 异地备份及传输日志保留30天
KEEP_DAYS=30
mkdir -p "${LOG_DIR}"
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >> "${LOG_FILE}"
}
# 检查端口和备份文件
if [ -z "${DB_PORT}" ] || [ -z "${BACKUP_FILE}" ] || [ ! -f "${BACKUP_FILE}" ]; then
log "ERROR: 端口或备份文件无效:DB_PORT=${DB_PORT} FILE=${BACKUP_FILE}"
exit 1
fi
log "确认备份文件已生成:${BACKUP_FILE}"
# 自动创建对应实例的异地目录
if ! ssh "${REMOTE_USER}@${REMOTE_HOST}" \
"mkdir -p '${REMOTE_DIR}'"; then
log "ERROR: 异地目录创建失败:${REMOTE_DIR}"
exit 1
fi
log "开始传输到 ${REMOTE_HOST}:${REMOTE_DIR}"
# rsync传输备份文件
if rsync -av "${BACKUP_FILE}" \
"${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_DIR}/" \
>> "${LOG_FILE}" 2>&1; then
log "备份文件传输成功"
else
log "ERROR: 备份文件传输失败"
exit 1
fi
# 清理异地30天前备份
ssh "${REMOTE_USER}@${REMOTE_HOST}" \
"find '${REMOTE_DIR}' -maxdepth 1 -type f -name '*.tar.gz' -mtime +${KEEP_DAYS} -delete"
# 清理30天前传输日志
find "${LOG_DIR}" \
-maxdepth 1 \
-type f \
-name 'transfer_*_*.log' \
-mtime +${KEEP_DAYS} \
-delete
log "异地备份任务完成"
exit 0
传输脚本接收两个参数:
参数1:MySQL 实例端口
参数2:本次生成的备份文件路径
例如:
/bin/bash /app/tools/transfer_mysql_backup.sh \
13307 \
/data/backup/mysql/13307/20260816_020001.tar.gz
脚本根据端口自动生成异地路径:
13307 → /home/192.168.10.32_13307
13308 → /home/192.168.10.32_13308
13309 → /home/192.168.10.32_13309
因此以后新增 MySQL 实例,不需要重新写传输脚本。
五、配置 SSH 免密
在 MySQL 从库执行:
ssh-keygen -t rsa -b 4096
复制公钥到异地服务器:
ssh-copy-id root@192.168.20.15
测试:
ssh root@192.168.20.15 "hostname"
如果无需输入密码即可返回,说明免密配置成功。
六、定时任务
例如每天凌晨 02:00 执行:
0 2 * * * /bin/bash /app/tools/backup.sh >> /app/tools/backup_run.log 2>&1 &
传输脚本无需单独配置 cron。
因为 backup.sh 会在压缩成功后直接调用:
transfer_mysql_backup.sh
七、备份保留策略
当前策略:
数据库服务器本地备份:7 天
异地备份:30 天
传输日志:30 天
本地和异地保留周期是独立的。
因此把异地备份调整为 30 天,不会导致数据库服务器本地磁盘保留 30 天数据。
八、手工测试
1. 检查脚本语法
bash -n /app/tools/backup.sh
echo $?
bash -n /app/tools/transfer_mysql_backup.sh
echo $?
返回:
0
说明语法正常。
2. 测试异地传输
可以使用已有备份文件测试:
/bin/bash /app/tools/transfer_mysql_backup.sh \
13307 \
/data/backup/mysql/13307/20260815_020001.tar.gz
查看日志:
tail -f /app/tools/log/transfer_13307_$(date +%Y%m%d).log
正常应看到:
备份文件传输成功
异地备份任务完成
检查异地文件:
ssh root@192.168.20.15 \
"ls -lh /home/192.168.10.32_13307"
九、备份前检查从库状态
建议执行:
SHOW REPLICA STATUS\G;
重点确认:
Replica_IO_Running: Yes
Replica_SQL_Running: Yes
Seconds_Behind_Source: 0
还可以检查连接:
SHOW PROCESSLIST;
优先在从库执行全量物理备份,可以降低对主库业务的直接影响。
十、实际测试结果
一次完整测试中,大致耗时:
XtraBackup:约 11 分钟
tar.gz 压缩:约 1 小时 45 分钟
rsync 异地传输:约 5 分钟
完整流程成功:
XtraBackup completed OK
↓
tar.gz 生成成功
↓
删除原始备份目录
↓
自动执行异地传输
↓
异地备份成功
可以看出,当前主要耗时并不是 XtraBackup,也不是网络传输,而是:
tar -zcf
十一、压缩耗时优化
不一定数据量大就必须压缩,因为压缩耗时太长;空间够、网络够快、强调快速备份和快速恢复时,不压缩是一种合理的设计。 甚至对于大数据库,如果网络很快,不压缩反而可能更好。
tar -zcf 使用的是 gzip。
gzip 主要是单线程压缩,因此在多核服务器上无法充分利用 CPU。
后续如果需要优化,可以测试:
pigz
即多线程 gzip。
生产环境建议先验证:
压缩耗时
CPU 使用率
磁盘 IO
压缩率
恢复兼容性
确认无问题后再替换。
十二、日常巡检命令
查看本地备份:
ls -lh /data/backup/mysql/13307/
查看备份日志:
tail -100 /data/backup/mysql/13307/log/mysql_backup_$(date +%Y%m%d).log
查看异地传输日志:
tail -100 /app/tools/log/transfer_13307_$(date +%Y%m%d).log
查看异地备份:
ssh root@192.168.20.15 \
"ls -lh /home/192.168.10.32_13307"
十三、总结
最终架构:
MySQL 主库
↓
主从复制
↓
MySQL 从库
↓
XtraBackup
↓
tar.gz
↓
本地保留 7 天
↓
rsync
↓
异地保留 30 天
这套方案最关键的一点是:
不通过固定时间判断备份是否完成,而是在压缩成功后直接触发异地传输。
这样即使后续数据库变大、备份耗时增加,也不需要频繁调整传输时间。
同时,传输脚本通过实例端口动态生成异地目录,后续新增实例时也能直接复用。