#!/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 指定全量备份目录 (默认使用最新的全量备份) # -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 指定全量备份基准目录 -b, --base-backup 指定增量备份基准目录 (用于增量链) -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 "$@"