添加 incremental_backup.sh

This commit is contained in:
k
2025-12-22 15:37:06 +00:00
parent 5d25b68390
commit 0533f1fc41

564
incremental_backup.sh Normal file
View File

@@ -0,0 +1,564 @@
#!/bin/bash
# ============================================================================
# MySQL 8.0.24 增量备份脚本
# ============================================================================
# 作者: AI Assistant
# 版本: 1.0.0
# 说明: 基于 binlog 的增量备份
#
# 功能特点:
# - 基于 binlog 实现增量备份
# - 从上次全量/增量备份点开始备份
# - 支持压缩
# - 自动清理过期备份
#
# 工作原理:
# 1. 读取上次备份记录的 binlog 位置
# 2. 复制从该位置到当前位置的 binlog 文件
# 3. 记录新的 binlog 位置用于下次增量备份
#
# 使用方法:
# ./incremental_backup.sh [选项]
#
# 选项:
# -f, --full-backup <path> 指定全量备份目录 (默认使用最新的全量备份)
# -c, --compress 启用压缩 (默认: 是)
# -n, --no-compress 禁用压缩
# -h, --help 显示帮助信息
#
# 前提条件:
# - 必须先执行过全量备份
# - MySQL 必须启用 binlog
# - MySQL 用户需要 REPLICATION SLAVE, REPLICATION CLIENT 权限
# ============================================================================
set -o pipefail
# ----------------------------------------------------------------------------
# 脚本路径和配置加载
# ----------------------------------------------------------------------------
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# 加载配置文件
if [[ ! -f "${SCRIPT_DIR}/config.sh" ]]; then
echo "[ERROR] 配置文件不存在: ${SCRIPT_DIR}/config.sh"
exit 1
fi
source "${SCRIPT_DIR}/config.sh"
# 加载公共函数库
if [[ ! -f "${SCRIPT_DIR}/lib/common.sh" ]]; then
echo "[ERROR] 公共函数库不存在: ${SCRIPT_DIR}/lib/common.sh"
exit 1
fi
source "${SCRIPT_DIR}/lib/common.sh"
# ----------------------------------------------------------------------------
# 全局变量
# ----------------------------------------------------------------------------
BACKUP_TIMESTAMP=$(date +"${TIMESTAMP_FORMAT}")
BACKUP_NAME="${BACKUP_PREFIX}_incr_${BACKUP_TIMESTAMP}"
BACKUP_DIR="${INCREMENTAL_BACKUP_DIR}/${BACKUP_NAME}"
LOCK_FILE="${BACKUP_ROOT_DIR}/.incremental_backup.lock"
LOG_FILE="${LOG_DIR}/incremental_backup_${BACKUP_TIMESTAMP}.log"
ENABLE_COMPRESS=true
SPECIFIED_FULL_BACKUP=""
BASE_BACKUP_DIR="" # 基准备份目录 (全量或上次增量)
# ----------------------------------------------------------------------------
# 显示帮助信息
# ----------------------------------------------------------------------------
show_help() {
cat << EOF
MySQL 8.0.24 增量备份脚本
使用方法:
$(basename "$0") [选项]
选项:
-f, --full-backup <path> 指定全量备份基准目录
-b, --base-backup <path> 指定增量备份基准目录 (用于增量链)
-c, --compress 启用压缩 (默认: 是)
-n, --no-compress 禁用压缩
-h, --help 显示此帮助信息
示例:
$(basename "$0") # 基于最新全量备份
$(basename "$0") -f /path/to/full_backup # 指定全量备份
$(basename "$0") -b /path/to/last_incr_backup # 基于上次增量备份
说明:
增量备份基于 MySQL binlog 实现。脚本会从基准备份记录的 binlog
位置开始,复制到当前 binlog 位置的所有日志。
前提条件:
- 必须先执行过全量备份
- MySQL 必须启用 binlog (log_bin=ON)
- MySQL 用户需要 REPLICATION SLAVE, REPLICATION CLIENT 权限
EOF
}
# ----------------------------------------------------------------------------
# 解析命令行参数
# ----------------------------------------------------------------------------
parse_args() {
while [[ $# -gt 0 ]]; do
case "$1" in
-f|--full-backup)
shift
if [[ -z "$1" ]]; then
die "选项 -f/--full-backup 需要一个参数"
fi
SPECIFIED_FULL_BACKUP="$1"
shift
;;
-b|--base-backup)
shift
if [[ -z "$1" ]]; then
die "选项 -b/--base-backup 需要一个参数"
fi
BASE_BACKUP_DIR="$1"
shift
;;
-c|--compress)
ENABLE_COMPRESS=true
shift
;;
-n|--no-compress)
ENABLE_COMPRESS=false
shift
;;
-h|--help)
show_help
exit 0
;;
*)
die "未知选项: $1\n使用 --help 查看帮助"
;;
esac
done
}
# ----------------------------------------------------------------------------
# 检查 binlog 是否启用
# ----------------------------------------------------------------------------
check_binlog_enabled() {
log_info "检查 binlog 状态..."
local binlog_status=$(execute_mysql "SHOW VARIABLES LIKE 'log_bin';")
if ! echo "$binlog_status" | grep -q "ON"; then
die "MySQL binlog 未启用。请在 my.cnf 中设置 log_bin=ON"
fi
log_info "Binlog 已启用"
}
# ----------------------------------------------------------------------------
# 获取 binlog 目录
# ----------------------------------------------------------------------------
get_binlog_dir() {
local binlog_basename=$(execute_mysql "SHOW VARIABLES LIKE 'log_bin_basename';" | awk '{print $2}')
if [[ -z "$binlog_basename" ]]; then
# 尝试使用数据目录
echo "$MYSQL_DATA_DIR"
else
dirname "$binlog_basename"
fi
}
# ----------------------------------------------------------------------------
# 查找基准备份
# ----------------------------------------------------------------------------
find_base_backup() {
if [[ -n "$BASE_BACKUP_DIR" ]]; then
# 使用指定的基准备份
if [[ ! -d "$BASE_BACKUP_DIR" ]]; then
die "指定的基准备份目录不存在: $BASE_BACKUP_DIR"
fi
echo "$BASE_BACKUP_DIR"
return
fi
if [[ -n "$SPECIFIED_FULL_BACKUP" ]]; then
# 使用指定的全量备份
if [[ ! -d "$SPECIFIED_FULL_BACKUP" ]]; then
die "指定的全量备份目录不存在: $SPECIFIED_FULL_BACKUP"
fi
echo "$SPECIFIED_FULL_BACKUP"
return
fi
# 首先查找最新的增量备份
local latest_incr=$(find "$INCREMENTAL_BACKUP_DIR" -maxdepth 1 -type d -name "${BACKUP_PREFIX}_incr_*" 2>/dev/null | sort -r | head -n1)
if [[ -n "$latest_incr" && -f "${latest_incr}/binlog_position.txt" ]]; then
log_info "找到最新的增量备份作为基准: $latest_incr"
echo "$latest_incr"
return
fi
# 查找最新的全量备份
local latest_full=$(find "$FULL_BACKUP_DIR" -maxdepth 1 -type d -name "${BACKUP_PREFIX}_full_*" 2>/dev/null | sort -r | head -n1)
if [[ -z "$latest_full" ]]; then
die "没有找到可用的全量备份。请先执行全量备份"
fi
if [[ ! -f "${latest_full}/binlog_position.txt" ]]; then
die "全量备份缺少 binlog 位置信息: $latest_full"
fi
log_info "使用全量备份作为基准: $latest_full"
echo "$latest_full"
}
# ----------------------------------------------------------------------------
# 读取基准 binlog 位置
# ----------------------------------------------------------------------------
read_base_binlog_position() {
local base_dir="$1"
local position_file="${base_dir}/binlog_position.txt"
if [[ ! -f "$position_file" ]]; then
die "找不到 binlog 位置文件: $position_file"
fi
source "$position_file"
if [[ -z "$BINLOG_FILE" || -z "$BINLOG_POSITION" ]]; then
die "binlog 位置信息不完整"
fi
echo "${BINLOG_FILE}:${BINLOG_POSITION}"
}
# ----------------------------------------------------------------------------
# 获取 binlog 文件列表
# ----------------------------------------------------------------------------
get_binlog_files_to_backup() {
local start_file="$1"
local start_pos="$2"
local binlog_dir="$3"
# 获取当前 binlog 位置
local current_pos=$(get_binlog_position)
local current_file=$(echo "$current_pos" | cut -d: -f1)
local current_position=$(echo "$current_pos" | cut -d: -f2)
log_info "起始 binlog: $start_file:$start_pos"
log_info "当前 binlog: $current_file:$current_position"
# 获取 binlog 索引文件
local binlog_index=$(execute_mysql "SHOW VARIABLES LIKE 'log_bin_index';" | awk '{print $2}')
# 获取需要备份的文件列表
local in_range=false
local files_to_backup=()
# 使用 SHOW BINARY LOGS 获取文件列表
local log_list=$(execute_mysql "SHOW BINARY LOGS;" | awk '{print $1}')
while IFS= read -r log_file; do
if [[ "$log_file" == "$start_file" ]]; then
in_range=true
fi
if [[ "$in_range" == true ]]; then
files_to_backup+=("$log_file")
fi
if [[ "$log_file" == "$current_file" ]]; then
break
fi
done <<< "$log_list"
echo "${files_to_backup[@]}"
}
# ----------------------------------------------------------------------------
# 初始化备份环境
# ----------------------------------------------------------------------------
init_backup() {
log_info "============================================================"
log_info "MySQL 增量备份开始"
log_info "============================================================"
log_info "备份时间: $BACKUP_TIMESTAMP"
log_info "备份目录: $BACKUP_DIR"
log_info "日志文件: $LOG_FILE"
log_info "============================================================"
# 设置错误处理
setup_error_trap
# 检查必要命令
check_commands "$MYSQL_PATH" "$MYSQLBINLOG_PATH" "gzip"
# 检查 MySQL 连接
check_mysql_connection
# 检查 binlog 是否启用
check_binlog_enabled
# 创建必要目录
ensure_dir "$BACKUP_DIR"
ensure_dir "$LOG_DIR"
# 检查磁盘空间
check_disk_space "$BACKUP_DIR" 2048
# 获取锁
acquire_lock "$LOCK_FILE" "增量备份"
# 设置清理 trap
trap cleanup EXIT
}
# ----------------------------------------------------------------------------
# 清理函数
# ----------------------------------------------------------------------------
cleanup() {
local exit_code=$?
# 释放锁
release_lock "$LOCK_FILE"
if [[ $exit_code -ne 0 ]]; then
log_error "备份过程中发生错误,退出码: $exit_code"
# 清理不完整的备份
if [[ -d "$BACKUP_DIR" ]]; then
log_warn "清理不完整的备份目录: $BACKUP_DIR"
rm -rf "$BACKUP_DIR"
fi
send_notification "[失败] MySQL 增量备份" "备份失败,请检查日志: $LOG_FILE"
fi
}
# ----------------------------------------------------------------------------
# 执行 binlog 备份
# ----------------------------------------------------------------------------
backup_binlogs() {
local base_dir="$1"
local binlog_dir=$(get_binlog_dir)
log_info "Binlog 目录: $binlog_dir"
# 读取基准位置
local base_pos=$(read_base_binlog_position "$base_dir")
local start_file=$(echo "$base_pos" | cut -d: -f1)
local start_position=$(echo "$base_pos" | cut -d: -f2)
# 获取当前位置
local current_pos=$(get_binlog_position)
local end_file=$(echo "$current_pos" | cut -d: -f1)
local end_position=$(echo "$current_pos" | cut -d: -f2)
# 检查是否有新的数据需要备份
if [[ "$start_file" == "$end_file" && "$start_position" == "$end_position" ]]; then
log_warn "没有新的 binlog 数据需要备份"
log_warn "起始位置和当前位置相同: $start_file:$start_position"
# 创建空的增量备份记录
save_binlog_position "$end_file" "$end_position"
return 0
fi
# 获取需要备份的 binlog 文件列表
local files=$(get_binlog_files_to_backup "$start_file" "$start_position" "$binlog_dir")
if [[ -z "$files" ]]; then
log_warn "没有需要备份的 binlog 文件"
save_binlog_position "$end_file" "$end_position"
return 0
fi
log_info "需要备份的 binlog 文件: $files"
local backup_count=0
local is_first_file=true
for binlog_file in $files; do
log_info "备份 binlog: $binlog_file"
local binlog_path="${binlog_dir}/${binlog_file}"
local output_file="${BACKUP_DIR}/${binlog_file}"
if [[ ! -f "$binlog_path" ]]; then
# binlog 可能已被清理,尝试从 MySQL 读取
log_warn "本地 binlog 文件不存在,尝试使用 mysqlbinlog 远程读取"
local mysqlbinlog_args="-h${MYSQL_HOST} -P${MYSQL_PORT} -u${MYSQL_USER}"
if [[ -n "$MYSQL_PASSWORD" ]]; then
mysqlbinlog_args="$mysqlbinlog_args -p${MYSQL_PASSWORD}"
fi
# 对于第一个文件,从指定位置开始
if [[ "$is_first_file" == true && "$binlog_file" == "$start_file" ]]; then
mysqlbinlog_args="$mysqlbinlog_args --start-position=$start_position"
fi
# 对于最后一个文件,在指定位置停止
if [[ "$binlog_file" == "$end_file" ]]; then
mysqlbinlog_args="$mysqlbinlog_args --stop-position=$end_position"
fi
mysqlbinlog_args="$mysqlbinlog_args --read-from-remote-server"
if ! $MYSQLBINLOG_PATH $mysqlbinlog_args "$binlog_file" > "$output_file" 2>> "$LOG_FILE"; then
die "备份 binlog 失败: $binlog_file"
fi
else
# 使用本地 binlog 文件
local mysqlbinlog_args=""
# 对于第一个文件,从指定位置开始
if [[ "$is_first_file" == true && "$binlog_file" == "$start_file" ]]; then
mysqlbinlog_args="--start-position=$start_position"
fi
# 对于最后一个文件,在指定位置停止
if [[ "$binlog_file" == "$end_file" ]]; then
mysqlbinlog_args="$mysqlbinlog_args --stop-position=$end_position"
fi
if ! $MYSQLBINLOG_PATH $mysqlbinlog_args "$binlog_path" > "$output_file" 2>> "$LOG_FILE"; then
die "备份 binlog 失败: $binlog_file"
fi
fi
# 压缩
if [[ "$ENABLE_COMPRESS" == "true" ]]; then
gzip -f "$output_file" || die "压缩失败: $output_file"
output_file="${output_file}.gz"
fi
((backup_count++))
is_first_file=false
log_info "备份完成: $(get_file_size $output_file)"
done
log_info "共备份 $backup_count 个 binlog 文件"
# 记录新的 binlog 位置
save_binlog_position "$end_file" "$end_position"
}
# ----------------------------------------------------------------------------
# 保存 binlog 位置
# ----------------------------------------------------------------------------
save_binlog_position() {
local binlog_file="$1"
local binlog_pos="$2"
local position_file="${BACKUP_DIR}/binlog_position.txt"
log_info "记录 binlog 位置: $binlog_file:$binlog_pos"
cat > "$position_file" << EOF
# MySQL Binlog Position
# 备份时间: $BACKUP_TIMESTAMP
# 用于下次增量备份的起始位置
BINLOG_FILE=$binlog_file
BINLOG_POSITION=$binlog_pos
EOF
}
# ----------------------------------------------------------------------------
# 保存备份元数据
# ----------------------------------------------------------------------------
save_metadata() {
local base_dir="$1"
local metadata_file="${BACKUP_DIR}/metadata.txt"
log_info "保存备份元数据"
local end_time=$(date +%s)
local duration=$(calculate_duration "$START_TIME" "$end_time")
local backup_size=$(get_file_size "$BACKUP_DIR")
cat > "$metadata_file" << EOF
# MySQL 增量备份元数据
# ========================================
备份类型: 增量备份 (Incremental Backup)
备份时间: $BACKUP_TIMESTAMP
备份目录: $BACKUP_NAME
备份大小: $backup_size
执行耗时: $duration
基准备份: $base_dir
MySQL 服务器: ${MYSQL_HOST}:${MYSQL_PORT}
MySQL 用户: $MYSQL_USER
备份选项:
- 压缩: $ENABLE_COMPRESS
- 压缩工具: $COMPRESS_TOOL
备份状态: 成功
EOF
# 记录基准备份路径 (用于恢复时的依赖链)
echo "$base_dir" > "${BACKUP_DIR}/base_backup_path.txt"
log_info "元数据已保存"
}
# ----------------------------------------------------------------------------
# 清理过期备份
# ----------------------------------------------------------------------------
cleanup_expired_backups() {
log_info "检查过期备份..."
cleanup_old_backups "$INCREMENTAL_BACKUP_DIR" "$INCREMENTAL_BACKUP_RETENTION_DAYS"
}
# ----------------------------------------------------------------------------
# 主函数
# ----------------------------------------------------------------------------
main() {
# 记录开始时间
START_TIME=$(date +%s)
# 解析参数
parse_args "$@"
# 初始化
init_backup
# 查找基准备份
local base_backup=$(find_base_backup)
log_info "基准备份: $base_backup"
# 执行 binlog 备份
backup_binlogs "$base_backup"
# 保存元数据
save_metadata "$base_backup"
# 清理过期备份
cleanup_expired_backups
# 计算总耗时
local end_time=$(date +%s)
local duration=$(calculate_duration "$START_TIME" "$end_time")
local backup_size=$(get_file_size "$BACKUP_DIR")
log_info "============================================================"
log_info "MySQL 增量备份完成"
log_info "============================================================"
log_info "备份目录: $BACKUP_DIR"
log_info "备份大小: $backup_size"
log_info "执行耗时: $duration"
log_info "基准备份: $base_backup"
log_info "============================================================"
# 发送成功通知
send_notification "[成功] MySQL 增量备份" "备份完成\n目录: $BACKUP_DIR\n大小: $backup_size\n耗时: $duration"
return 0
}
# 执行主函数
main "$@"