MySQL/瀚高双机冷备Shell脚本(支持docker容器)

58 阅读8分钟

本篇为历史文档的升级版本,支持瀚高,支持动态配置信息,傻瓜式使用。本文撰写大量参考AI给出的建议。

本文脚本放在最后,如果您想直接阅读脚本,请直接翻阅至最后。

shell脚本实现mysql数据库双机定时备份

1. 方案概述

本套脚本旨在为 MySQLHighGo (瀚高) 数据库提供简单、低成本的全量冷备方案。支持物理机直接部署和 Docker 容器化部署,并可灵活选择单机本地备份双机异地容灾

1.1 核心流程图

 +----------------+                          +----------------+
 |  主服务器 (A)   |                          |  从服务器 (B)   |
 | (Script: Master)|                          | (Script: Slave)|
 +--------+-------+                          +--------+-------+
          |                                           |
     1. 导出数据 (mysqldump/pg_dump)                  |
          |                                           |
     2. 流式压缩 (.sql.gz)                            |
          |                                           |
     3. 传输文件 (SSH/SCP) [可选步骤] ----------->  4. 接收文件
     (若配置了 SLAVE_IP 则传输,否则跳过)                |
          |                                           |
     5. 清理 A 本地旧备份                              6. 清理 B 本地旧备份
     (基于文件名日期的精准清理)                         (基于文件名日期的精准清理)

1.2 功能亮点

  • 灵活部署支持单机模式! 如果不配置从服务器 IP,脚本仅执行本地备份和清理。
  • 零依赖:支持 Docker 模式,宿主机无需安装任何数据库客户端工具(如 mysqlpsql),脚本直接穿透容器执行。
  • 通用性强:一套脚本同时支持 MySQL 和 HighGo,通过配置切换。
  • 安全性高:若启用双机模式,支持 SSH 免密互信,脚本中无需明文存储 Linux 系统密码。
  • 节省空间:导出过程直接经过 gzip 压缩,不占用临时磁盘空间。

2. 部署前置条件 (仅双机模式需要)

⚠️ 如果您只进行单机本地备份,可直接跳过本节。

若需要将备份传输到异地从服务器,必须配置 SSH 免密登录。

2.1 环境假设

  • 主服务器 (Master) IP: 192.168.1.10
  • 从服务器 (Slave) IP: 192.168.1.200

2.2 配置免密互信 (二选一)

方案 A:自动配置 (推荐,需知晓从机密码)

主服务器 上执行:

  1. 生成密钥(如果已经生成可以跳过):ssh-keygen -t rsa (一路回车)
  2. 下发公钥:ssh-copy-id root@192.168.1.200
  3. 验证ssh root@192.168.1.200 (无需密码即成功)

方案 B:手动配置 (无需密码,适合云平台)

  1. 主服务器:生成密钥(如果已经生成可以跳过)ssh-keygen -t rsa (一路回车)
  2. 主服务器cat ~/.ssh/id_rsa.pub 并复制内容。
  3. 从服务器:编辑 ~/.ssh/authorized_keys,粘贴公钥内容。
  4. 从服务器:执行 chmod 600 ~/.ssh/authorized_keys (至关重要)。

3. 安装与目录结构

建议在服务器上创建统一目录:

  1. 创建目录mkdir -p /opt/db_backup

  2. 上传文件:将 db_backup.shdb_backup.conf 上传至该目录。

  3. 赋予权限

     chmod +x /opt/db_backup/db_backup.sh
     chmod 600 /opt/db_backup/db_backup.conf
    

4. 配置文件详解 (db_backup.conf)

4.1 基础参数 (所有场景通用)

参数说明示例
SCRIPT_ROLE脚本角色MASTER: 执行备份 (+传输) + 清理 SLAVE: 仅执行清理 (从库用)"MASTER"
DB_TYPE数据库类型。支持 MYSQLHIGHGO"MYSQL"
BACKUP_DIR宿主机备份文件存储路径"/data/backup"
LOG_FILE日志文件路径"/var/log/db_backup.log"

4.2 场景化配置 (重点:Docker 设置)

🐳 场景一:数据库运行在 Docker 容器中

这是目前最常见的部署方式。

参数配置说明
DOCKER_CONTAINER_NAME填写容器名称或容器 ID (例如 "mysql_prod")。 ⚠️ 只要此项不为空,脚本就会启用 Docker 模式。
DB_HOST必须填 127.0.0.1 (因为命令是在容器内部执行的)。

💻 场景二:数据库直接安装在宿主机

参数配置说明
DOCKER_CONTAINER_NAME必须留空 ("")。
DB_HOST填写 127.0.0.1 或本机 IP。

4.3 传输配置 (仅 Master 需要)

⚠️ 单机/双机模式切换开关

参数说明
SLAVE_IP关键开关。 1. 开启双机备份:填写从服务器 IP (如 "192.168.1.200")。 2. 仅单机备份留空 (即 SLAVE_IP=""),脚本将跳过传输步骤。
SLAVE_USERSSH 用户名 (通常 root)
SLAVE_PORTSSH 端口 (通常 22)
SLAVE_DEST_DIR从服务器接收文件的路径

5. 配置示例速查

示例 A:MySQL Docker 版 (单机备份)

 SCRIPT_ROLE="MASTER"
 DB_TYPE="MYSQL"
 DOCKER_CONTAINER_NAME="mysql-container-v1" # Docker 模式
 ​
 DB_HOST="127.0.0.1"
 DB_PORT="3306"
 DB_USER="root"
 DB_PASS="123456"
 DB_LIST="order_db"
 ​
 SLAVE_IP=""  # <--- 留空,表示不传输,仅本地备份
 MASTER_KEEP_DAYS=7

示例 B:HighGo (瀚高) 物理机版 (双机互备)

 SCRIPT_ROLE="MASTER"
 DB_TYPE="HIGHGO"
 DOCKER_CONTAINER_NAME="" # 物理机模式
 ​
 DB_HOST="localhost"
 DB_PORT="5866"
 DB_USER="sysdba"
 DB_PASS="Highgo@123"
 DB_LIST="highgo_test"
 ​
 SLAVE_IP="192.168.1.200" # <--- 配置 IP,开启传输
 SLAVE_DEST_DIR="/data/backup"

6. 定时任务 (Crontab)

使用 crontab -e 添加计划任务。

主服务器 (Master)

 # 每天凌晨 02:00 执行全量备份
 0 2 * * * /bin/bash /opt/db_backup/db_backup.sh

从服务器 (Slave - 仅双机模式需要)

 # 每天凌晨 03:00 清理过期备份
 0 3 * * * /bin/bash /opt/db_backup/db_backup.sh

7. 灾难恢复指南

当需要恢复数据时,请先将 .sql.gz 文件解压,然后导入。

7.1 MySQL 恢复

Docker 环境:

 # 1. 解压数据流 -> 2. 传入容器 -> 3. 执行导入
 gunzip < backup_file.sql.gz | docker exec -i <容器名> mysql -u root -p<密码> <数据库名>

物理机环境:

 gunzip < backup_file.sql.gz | mysql -u root -p<密码> <数据库名>

7.2 HighGo (瀚高) 恢复

Docker 环境:

 # 注意: HighGo 容器内通常使用 psql 工具
 gunzip < backup_file.sql.gz | docker exec -i <容器名> psql -h 127.0.0.1 -p 5866 -U sysdba -d <数据库名>

物理机环境:

 gunzip < backup_file.sql.gz | psql -h localhost -p 5866 -U sysdba -d <数据库名>

8. 常见问题 (FAQ)

Q1: 脚本执行报错 /db_backup/db_backup.conf: line 2: $'\r': command not found

  • 原因: 脚本文件 (db_backup.sh) 或配置文件 (db_backup.conf) 是在 Windows 环境下编辑或保存的。
  • 解决: 执行命令:sed -i 's/\r$//' db_backup.sh db_backup.conf

Q2: 脚本执行报错 Permission denied (publickey)

  • 原因: 双机模式下 SSH 免密互信未配置成功。
  • 解决: 参考第 2 节重新配置。如果是单机模式,请确保 SLAVE_IP 已置空。

Q3: 提示 mysqldump: command not found

  • 原因: 物理机模式下宿主机没装客户端,或者 Docker 模式下 DOCKER_CONTAINER_NAME 忘填了。
  • 解决: 检查 DOCKER_CONTAINER_NAME 配置。

Q4: 为什么旧文件没被删除?

  • 原因: 脚本现在使用文件名中的日期 (YYYYMMDD_HHMMSS) 来判断文件年龄。
  • 解决: 请检查旧文件的命名格式是否为 库名_YYYYMMDD_HHMMSS.sql.gz。如果命名不符合此规范,脚本为防止误删会跳过该文件。

9. 脚本内容

9.1 db_backup.conf (配置文件)

# ================= 核心模式配置 =================
# 数据库类型: MYSQL 或 HIGHGO
DB_TYPE="MYSQL"

# ⚠️ [Docker 支持]
# 填写容器名称(如 "mysql_prod")开启 Docker 模式;留空则为宿主机模式
DOCKER_CONTAINER_NAME="mysql_prod"

# 脚本角色: MASTER (备份+传输+清理) 或 SLAVE (仅清理)
SCRIPT_ROLE="MASTER"

# ================= 基础路径 =================
BACKUP_DIR="/data/backup/database"
LOG_FILE="/var/log/db_backup.log"

# ================= 数据库连接配置 (Master 填写) =================
DB_HOST="127.0.0.1"
DB_PORT="3306" 
DB_USER="root"
DB_PASS="你的数据库密码"
# 多个数据库使用空格隔开
DB_LIST="db_project_a db_project_b"

# ================= 传输配置 (Master 填写) =================
# ⚠️ [可选功能] ⚠️
# 如果需要双机异地备份,请填写从服务器信息(需配置 SSH 免密互信)。
# 如果只需要单机本地备份,请将 SLAVE_IP 留空 (""),脚本将自动跳过传输步骤。
SLAVE_IP="192.168.1.200"

SLAVE_PORT="22"
SLAVE_USER="root"
SLAVE_DEST_DIR="/data/backup/database"

# ================= 清理策略 =================
MASTER_KEEP_DAYS=7
SLAVE_KEEP_DAYS=30

9.2 db_backup.sh (脚本)

#!/bin/bash

# ================= 解决 Crontab 环境变量问题 (新增) =================
# 1. 强制加载系统级环境变量
source /etc/profile
[ -f ~/.bash_profile ] && source ~/.bash_profile
[ -f ~/.bashrc ] && source ~/.bashrc

# 2. 显式补充常用路径 (防止 source 没生效)
export PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin:/root/bin:$PATH

# ================= 环境准备 =================
SCRIPT_DIR=$(cd $(dirname $0); pwd)
CONF_FILE="$SCRIPT_DIR/db_backup.conf"

if [ -f "$CONF_FILE" ]; then
    source "$CONF_FILE"
else
    echo "Error: 配置文件 $CONF_FILE 不存在!"
    exit 1
fi

log() {
    echo "[$(date +'%Y-%m-%d %H:%M:%S')] [$DB_TYPE] $1" | tee -a "$LOG_FILE"
}

if [ ! -d "$BACKUP_DIR" ]; then
    mkdir -p "$BACKUP_DIR"
fi

DATE_STR=$(date +%Y%m%d_%H%M%S)

# ================= 辅助函数:根据文件名清理过期备份 =================
# 原理:提取文件名中的 YYYYMMDD_HHMMSS,格式化为标准日期后对比时间戳
cleanup_by_filename() {
    local target_dir=$1
    local keep_days=$2
    
    local current_timestamp=$(date +%s)
    local threshold_seconds=$(($keep_days * 86400))
    local cutoff_timestamp=$(($current_timestamp - $threshold_seconds))
    
    local cutoff_date_str=$(date -d @$cutoff_timestamp "+%Y-%m-%d %H:%M:%S")

    log ">>> 执行清理检查: 保留天数=$keep_days (截止时间: $cutoff_date_str)"

    shopt -s nullglob
    for file in "$target_dir"/*.sql.gz; do
        filename=$(basename "$file")
        
        # 匹配格式:任意前缀_YYYYMMDD_HHMMSS.sql.gz
        if [[ $filename =~ _([0-9]{8})_([0-9]{6})\.sql\.gz$ ]]; then
            d_part="${BASH_REMATCH[1]}"
            t_part="${BASH_REMATCH[2]}"
            
            # 手动拼接成 ISO 格式: YYYY-MM-DD HH:MM:SS
            year=${d_part:0:4}
            month=${d_part:4:2}
            day=${d_part:6:2}
            hour=${t_part:0:2}
            minute=${t_part:2:2}
            second=${t_part:4:2}
            
            formatted_date="$year-$month-$day $hour:$minute:$second"
            
            file_timestamp=$(date -d "$formatted_date" +%s 2>/dev/null)
            
            if [ $? -eq 0 ]; then
                if [ "$file_timestamp" -lt "$cutoff_timestamp" ]; then
                    log "🗑️ [删除] 过期文件: $filename (日期: $formatted_date)"
                    rm -f "$file"
                else
                    :
                fi
            else
                log "⚠️ [跳过] 日期解析失败: $filename"
            fi
        else
            log "⚠️ [跳过] 命名格式不符: $filename"
        fi
    done
    shopt -u nullglob
    log ">>> 清理检查完成."
}

# ================= 核心逻辑 =================

if [ "$SCRIPT_ROLE" == "MASTER" ]; then
    log "=== 开始执行 MASTER 备份任务 ==="
    
    if [ -n "$DOCKER_CONTAINER_NAME" ]; then
        log "检测到 Docker 模式,容器名称: $DOCKER_CONTAINER_NAME"
    fi

    for DB_NAME in $DB_LIST; do
        FILE_NAME="${DB_NAME}_${DATE_STR}.sql.gz"
        FILE_PATH="$BACKUP_DIR/$FILE_NAME"
        
        log "正在备份数据库: $DB_NAME ..."
        
        # >>>>>> 备份逻辑 <<<<<<
        BACKUP_STATUS=1

        if [ "$DB_TYPE" == "MYSQL" ]; then
            if [ -n "$DOCKER_CONTAINER_NAME" ]; then
                docker exec "$DOCKER_CONTAINER_NAME" mysqldump -h"$DB_HOST" -P"$DB_PORT" -u"$DB_USER" -p"$DB_PASS" \
                    --single-transaction --set-gtid-purged=OFF "$DB_NAME" 2>/dev/null | gzip > "$FILE_PATH"
                BACKUP_STATUS=${PIPESTATUS[0]}
            else
                mysqldump -h"$DB_HOST" -P"$DB_PORT" -u"$DB_USER" -p"$DB_PASS" \
                    --single-transaction --set-gtid-purged=OFF "$DB_NAME" 2>/dev/null | gzip > "$FILE_PATH"
                BACKUP_STATUS=${PIPESTATUS[0]}
            fi

        elif [ "$DB_TYPE" == "HIGHGO" ]; then
            if [ -n "$DOCKER_CONTAINER_NAME" ]; then
                docker exec -e PGPASSWORD="$DB_PASS" "$DOCKER_CONTAINER_NAME" \
                    pg_dump -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" | gzip > "$FILE_PATH"
                BACKUP_STATUS=${PIPESTATUS[0]}
            else
                export PGPASSWORD="$DB_PASS"
                pg_dump -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" | gzip > "$FILE_PATH"
                BACKUP_STATUS=${PIPESTATUS[0]}
                unset PGPASSWORD
            fi
            
        else
            log "❌ 错误: 未知的 DB_TYPE ($DB_TYPE)"
            exit 1
        fi

        if [ $BACKUP_STATUS -eq 0 ]; then
            FILE_SIZE=$(stat -c%s "$FILE_PATH" 2>/dev/null || echo 0)
            if [ "$FILE_SIZE" -gt 20 ]; then
                log "备份成功: $FILE_NAME (大小: $(du -h "$FILE_PATH" | cut -f1))"
                
                # >>>>>> 传输逻辑 (关键修改) <<<<<<
                if [ -n "$SLAVE_IP" ]; then
                    log "正在传输到从服务器 ($SLAVE_IP) ..."
                    
                    # 1. 先尝试在远程创建目录 (防止目录不存在导致scp失败)
                    # -o StrictHostKeyChecking=no: 禁止交互式询问指纹
                    ssh -p "$SLAVE_PORT" -o StrictHostKeyChecking=no "$SLAVE_USER@$SLAVE_IP" "mkdir -p $SLAVE_DEST_DIR" >> "$LOG_FILE" 2>&1
                    
                    # 2. 执行 SCP 传输
                    scp -P "$SLAVE_PORT" -o StrictHostKeyChecking=no "$FILE_PATH" "$SLAVE_USER@$SLAVE_IP:$SLAVE_DEST_DIR" >> "$LOG_FILE" 2>&1
                    
                    if [ $? -eq 0 ]; then
                        log "传输成功."
                    else
                        log "❌ 传输失败! 请检查日志详情 (可能是网络、互信或磁盘满)。"
                    fi
                else
                    log "ℹ️  跳过传输: 未配置从服务器 IP (单机模式)"
                fi
            else
                 log "❌ 备份失败: 文件大小异常。"
                 rm -f "$FILE_PATH"
            fi
        else
            log "❌ 备份失败: 命令执行错误。"
            rm -f "$FILE_PATH"
        fi
    done

    cleanup_by_filename "$BACKUP_DIR" "$MASTER_KEEP_DAYS"

elif [ "$SCRIPT_ROLE" == "SLAVE" ]; then
    log "=== 开始执行 SLAVE 清理任务 ==="
    if [ -d "$BACKUP_DIR" ]; then
        cleanup_by_filename "$BACKUP_DIR" "$SLAVE_KEEP_DAYS"
    else
        log "❌ 备份目录不存在。"
    fi
else
    log "❌ SCRIPT_ROLE 错误"
    exit 1
fi