数据库账号为:backup,密码保存在:/etc/my.cnf.d/backup.conf ,文件内容为:
[client]
user=backup
password="123456"
host=127.0.0.1
mysql备份账号权限:
REVOKE ALL PRIVILEGES ON *.* FROM 'backup'@'localhost'; REVOKE GRANT OPTION ON *.* FROM 'backup'@'localhost'; GRANT SELECT, RELOAD, PROCESS, SHOW DATABASES, LOCK TABLES, REPLICATION CLIENT, EVENT, SHOW VIEW ON *.* TO 'backup'@'localhost' REQUIRE NONE WITH MAX_QUERIES_PER_HOUR 0 MAX_CONNECTIONS_PER_HOUR 0 MAX_UPDATES_PER_HOUR 0 MAX_USER_CONNECTIONS 0;
每天定时备份一次 crontab 脚本:
2 3 * * * /data/backup/backup_full.sh >> /var/log/backup_full.log 1>&1
备份脚本:backup_full.h
#!/bin/bash
set -euo pipefail
set -o pipefail
# ==================== 配置区 ====================
BACKUP_BASE="/data/backup"
KEEP_DAYS=7
LOG_RETENTION_DAYS=30
MIN_FREE_SPACE_MB=2048 # 保底值,实际会动态计算
MYSQL_BACKUP_CONFIG="/etc/my.cnf.d/backup.conf"
MYSQL_BACKUP_OPTS="--defaults-extra-file=$MYSQL_BACKUP_CONFIG --host=127.0.0.1 --user=backup"
WEB_SITES=(
"weather_ical:/data/www/w.mdeve.com/"
"default:/data/www/default/"
"platform:/data/www/platform/"
"smsfilter:/data/www/smsfilter.mdeve.com/"
)
CONFIG_DIRS=(
"etc:/etc/"
"php.etc:/usr/local/php/etc/"
"etc.nginx:/usr/local/etc/"
"frp:/usr/local/frp/"
)
LOG_DIR="/var/log/backup"
# ==================== 初始化 ====================
BACKUP_DATETIME=$(date +%Y%m%d_%H%M%S) # 加入时间戳,避免同天覆盖
TEMP_DIR="$BACKUP_BASE/$BACKUP_DATETIME"
FINAL_TARBALL="$BACKUP_BASE/$BACKUP_DATETIME.tar.gz"
ENCRYPTED_FILE="$BACKUP_BASE/$BACKUP_DATETIME.tar.gz.enc"
INFO_FILE="$BACKUP_BASE/${BACKUP_DATETIME}_info.txt"
LOG_FILE="$LOG_DIR/backup_${BACKUP_DATETIME}.log"
umask 077
mkdir -p "$LOG_DIR"
# ==================== 日志函数(同时输出终端和日志文件) ====================
log_info() {
local msg="[$(date '+%Y-%m-%d %H:%M:%S')] [信息] $*"
echo "$msg" | tee -a "$LOG_FILE"
}
log_error() {
local msg="[$(date '+%Y-%m-%d %H:%M:%S')] [错误] $*"
echo "$msg" | tee -a "$LOG_FILE" >&2
}
log_success() {
local msg="[$(date '+%Y-%m-%d %H:%M:%S')] [成功] $*"
echo "$msg" | tee -a "$LOG_FILE"
}
# ==================== 清理函数 ====================
cleanup() {
local exit_code=$?
if [ -n "${TEMP_DIR:-}" ] && [ -d "$TEMP_DIR" ] && [ "$TEMP_DIR" != "/" ]; then
log_info "正在清理临时目录: $TEMP_DIR"
rm -rf "$TEMP_DIR"
fi
# 如果加密成功,删除未加密的中间文件
if [ -f "$FINAL_TARBALL" ] && [ -f "$ENCRYPTED_FILE" ]; then
rm -f "$FINAL_TARBALL"
log_info "已删除未加密的临时压缩包"
fi
if [ $exit_code -eq 0 ]; then
log_success "脚本执行成功"
else
log_error "脚本异常终止,退出码 $exit_code(详见日志 $LOG_FILE)"
fi
exit $exit_code
}
trap cleanup EXIT
trap 'log_error "命令在第 $LINENO 行失败"' ERR
# ==================== 前置检查 ====================
check_mysql_config_perms() {
if [ ! -f "$MYSQL_BACKUP_CONFIG" ]; then
log_error "MySQL 配置文件 $MYSQL_BACKUP_CONFIG 不存在"
exit 1
fi
local perms
perms=$(stat -c %a "$MYSQL_BACKUP_CONFIG" 2>/dev/null || stat -f %Lp "$MYSQL_BACKUP_CONFIG" 2>/dev/null)
if [ "$perms" != "600" ]; then
log_error "配置文件权限为 $perms(应为 600)"
exit 1
fi
log_info "MySQL 配置文件权限检查通过"
}
check_mariadb_backup() {
if ! command -v mariadb-backup &>/dev/null; then
log_error "未找到 mariadb-backup 命令,请安装 mariadb-backup 软件包"
exit 1
fi
log_info "mariadb-backup 已安装"
}
check_mysql_client() {
if ! command -v mysql &>/dev/null; then
log_error "未找到 mysql 客户端命令"
exit 1
fi
log_info "mysql 客户端已安装"
}
check_binlog_privilege() {
log_info "检查 backup 用户是否具备 BINLOG MONITOR 权限..."
if mysql $MYSQL_BACKUP_OPTS -e "SHOW MASTER STATUS" >/dev/null 2>&1; then
log_success "权限检查通过"
else
log_error "backup 用户缺少执行 SHOW MASTER STATUS 的权限"
log_error "请以 root 登录 MariaDB 执行:"
log_error " GRANT BINLOG MONITOR ON *.* TO 'backup'@'localhost';"
log_error " (若版本较旧,使用 GRANT SUPER ON *.* TO 'backup'@'localhost';)"
exit 1
fi
}
check_disk_space() {
# 估算所需空间:数据库数据目录大小 + 网站目录大小 + 10% 余量
local estimated=0
# 获取数据库数据目录大小(假设在 /var/lib/mysql)
if [ -d /var/lib/mysql ]; then
local db_size=$(du -sb /var/lib/mysql 2>/dev/null | cut -f1 || echo 0)
estimated=$((estimated + db_size))
fi
# 网站目录大小
for entry in "${WEB_SITES[@]}"; do
local src_dir="${entry#*:}"
if [ -d "$src_dir" ]; then
local size=$(du -sb "$src_dir" 2>/dev/null | cut -f1 || echo 0)
estimated=$((estimated + size))
fi
done
# 加上配置目录等
estimated=$((estimated + 100*1024*1024)) # 预留 100M
estimated=$((estimated * 120 / 100)) # 加 20% 余量
local required_mb=$((estimated / 1024 / 1024))
[ $required_mb -lt $MIN_FREE_SPACE_MB ] && required_mb=$MIN_FREE_SPACE_MB
local available_kb=$(df --output=avail "$BACKUP_BASE" | tail -1)
local available_mb=$((available_kb / 1024))
if [ $available_mb -lt $required_mb ]; then
log_error "磁盘空间不足:可用 ${available_mb}MB,需要约 ${required_mb}MB"
exit 1
fi
log_info "磁盘空间检查通过:可用 ${available_mb}MB,估算需要 ${required_mb}MB"
}
# ==================== 清理旧文件(基于日期前缀) ====================
clean_old_backups() {
log_info "开始清理 ${KEEP_DAYS} 天前的旧备份..."
local cutoff=$(date -d "$KEEP_DAYS days ago" +%Y%m%d)
local count=0
for f in "$BACKUP_BASE"/*.tar.gz.enc; do
[ -f "$f" ] || continue
local fname=$(basename "$f")
# 提取日期部分(YYYYMMDD)
local fdate=${fname%%_*} # 取第一个下划线前的部分
if [[ $fdate =~ ^[0-9]{8}$ ]] && [ "$fdate" -lt "$cutoff" ]; then
log_info "删除旧备份: $f"
rm -f "$f"
((count++))
fi
done
for f in "$BACKUP_BASE"/*_info.txt; do
[ -f "$f" ] || continue
local fname=$(basename "$f")
local fdate=${fname%%_*} # 同样取日期
if [[ $fdate =~ ^[0-9]{8}$ ]] && [ "$fdate" -lt "$cutoff" ]; then
log_info "删除旧信息文件: $f"
rm -f "$f"
fi
done
log_info "共删除 $count 个旧备份文件"
}
clean_old_logs() {
log_info "开始清理 ${LOG_RETENTION_DAYS} 天前的旧日志..."
local cutoff=$(date -d "$LOG_RETENTION_DAYS days ago" +%Y%m%d)
local count=0
for f in "$LOG_DIR"/backup_*.log; do
[ -f "$f" ] || continue
local fname=$(basename "$f")
if [[ $fname =~ backup_([0-9]{8})_ ]]; then
local fdate=${BASH_REMATCH[1]}
if [ "$fdate" -lt "$cutoff" ]; then
log_info "删除旧日志: $f"
rm -f "$f"
((count++))
fi
fi
done
log_info "共删除 $count 个旧日志文件"
}
# ==================== 生成随机密码(通过环境变量传递) ====================
generate_password() {
local len=$((RANDOM % 13 + 21))
export BACKUP_PASS=$(tr -dc 'A-Za-z0-9!@#$%^&*()_+-=' < /dev/urandom | head -c "$len")
}
# ==================== 生成信息文件 ====================
generate_info_file() {
cat > "$INFO_FILE" <<EOF
============================================================
备份信息文件
生成时间: $(date '+%Y-%m-%d %H:%M:%S')
备份标识: $BACKUP_DATETIME
============================================================
一、备份内容
-------------
- MariaDB 全量物理备份(目录: mysql_backup/)
- 网站目录(各 .tar 文件)
- 系统配置文件(各 .tar 文件)
打包为 ${BACKUP_DATETIME}.tar.gz,并 AES-256 加密为 ${BACKUP_DATETIME}.tar.gz.enc。
二、加密密码(请妥善保管,丢失无法恢复)
----------------------------------------
${BACKUP_PASS}
三、SHA-256 校验值(加密前压缩包内文件)
-----------------------------------------
EOF
cd "$TEMP_DIR"
find . -type f -exec sha256sum {} \; >> "$INFO_FILE"
cd - >/dev/null
cat >> "$INFO_FILE" <<EOF
四、恢复步骤
------------
1. 解密:
export PASS="${BACKUP_PASS}"
openssl enc -d -aes-256-cbc -salt -pbkdf2 -in ${BACKUP_DATETIME}.tar.gz.enc -out ${BACKUP_DATETIME}.tar.gz -pass env:PASS
2. 解压:
tar -xzf ${BACKUP_DATETIME}.tar.gz
3. 恢复 MariaDB:
systemctl stop mariadb
rm -rf /var/lib/mysql/*
mariadb-backup --copy-back --target-dir=./${BACKUP_DATETIME}/mysql_backup
chown -R mysql:mysql /var/lib/mysql
# 若启用 SELinux,执行:restorecon -R /var/lib/mysql
systemctl start mariadb
4. 恢复网站和配置:
根据各 .tar 文件解压至对应目录。
⚠️ 安全提示:本文件包含明文密码,请立即移至安全介质并加密存储。
============================================================
EOF
log_info "信息文件已生成: $INFO_FILE"
}
# ==================== 主流程 ====================
log_info "==================== 备份开始 ===================="
log_info "备份标识: $BACKUP_DATETIME"
log_info "日志文件: $LOG_FILE"
check_mysql_config_perms
check_mariadb_backup
check_mysql_client
check_binlog_privilege
mkdir -p "$BACKUP_BASE"
cd "$BACKUP_BASE"
check_disk_space
[ -d "$TEMP_DIR" ] && rm -rf "$TEMP_DIR"
mkdir -p "$TEMP_DIR"
# 记录开始时间
start_time=$(date +%s)
# ==================== 1. MariaDB 全量物理备份 ====================
log_info "开始使用 mariadb-backup 进行全量备份..."
BACKUP_DATA_DIR="$TEMP_DIR/mysql_backup"
mkdir -p "$BACKUP_DATA_DIR"
if mariadb-backup $MYSQL_BACKUP_OPTS --backup --target-dir="$BACKUP_DATA_DIR" --no-version-check; then
log_success "MariaDB 全量备份完成 -> $BACKUP_DATA_DIR"
else
log_error "mariadb-backup 备份失败"
exit 1
fi
# ==================== 2. 备份网站目录 ====================
log_info "开始打包网站目录..."
for entry in "${WEB_SITES[@]}"; do
tar_name="${entry%%:*}.tar"
src_dir="${entry#*:}"
if [ ! -d "$src_dir" ]; then
log_error "源目录 $src_dir 不存在"
exit 1
fi
log_info "正在创建 $tar_name(来源 $src_dir)"
if tar --exclude='*.log*' --warning=no-file-changed -cf "$TEMP_DIR/$tar_name" -C "$src_dir" . >/dev/null 2>&1; then
log_success "打包完成: $tar_name"
else
log_error "打包 $src_dir 失败"
exit 1
fi
done
# ==================== 3. 备份配置文件 ====================
log_info "开始打包配置文件..."
for entry in "${CONFIG_DIRS[@]}"; do
tar_name="${entry%%:*}.tar"
src_dir="${entry#*:}"
if [ ! -d "$src_dir" ]; then
log_error "源目录 $src_dir 不存在"
exit 1
fi
log_info "正在创建 $tar_name(来源 $src_dir)"
if tar --warning=no-file-changed -cf "$TEMP_DIR/$tar_name" -C "$src_dir" . >/dev/null 2>&1; then
log_success "打包完成: $tar_name"
else
log_error "打包 $src_dir 失败"
exit 1
fi
done
# ==================== 4. 打包整个临时目录 ====================
log_info "正在打包为 $FINAL_TARBALL ..."
if tar czf "$FINAL_TARBALL" -C "$BACKUP_BASE" "$BACKUP_DATETIME" >/dev/null 2>&1; then
log_success "打包完成"
else
log_error "创建压缩包失败"
exit 1
fi
# ==================== 5. 生成随机密码并加密(密码通过环境变量传递) ====================
log_info "正在生成随机密码(21~33位)..."
generate_password
log_info "随机密码已生成(不显示)"
log_info "正在使用 AES-256 加密压缩包..."
if openssl enc -aes-256-cbc -salt -pbkdf2 -in "$FINAL_TARBALL" -out "$ENCRYPTED_FILE" -pass env:BACKUP_PASS; then
log_success "加密完成: $ENCRYPTED_FILE"
else
log_error "加密失败"
exit 1
fi
# ==================== 6. 生成信息文件 ====================
generate_info_file
# ==================== 7. 清理中间文件 ====================
rm -rf "$TEMP_DIR"
rm -f "$FINAL_TARBALL"
log_info "已清理临时文件和未加密压缩包"
# ==================== 8. 清理旧备份和日志 ====================
clean_old_backups
clean_old_logs
# 耗时统计
end_time=$(date +%s)
duration=$((end_time - start_time))
log_info "备份总耗时: $((duration / 60)) 分 $((duration % 60)) 秒"
log_success "备份流程全部完成!"
log_info "最终加密备份: $ENCRYPTED_FILE"
log_info "信息文件: $INFO_FILE"
log_info "==================== 备份结束 ===================="
exit 0
:bi...
文章评论