diff --git a/incremental_backup.sh b/incremental_backup.sh new file mode 100644 index 0000000..1af3bda --- /dev/null +++ b/incremental_backup.sh @@ -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 指定全量备份目录 (默认使用最新的全量备份) +# -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 "$@"