MariaDB全量|增量备份脚本

温馨提醒

脚本功能:

  • 支持全量 | 增量备份(备份时指定 full | incr);
  • 支持备份到华为云对象存储中;
  • 流式上传到对象存储中,本地不落盘;
  • 备份完成后,会验证备份的有效性;
  • 备份失败发送企业微信告警;
  • 配合定时任务,每周六全量备份,周日至周五增量备份;
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
#!/bin/bash

DB_NAME=$1
BACKUP_TYPE=$2
TS=$(date "+%Y%m%d")
MYSQL_USER="backup"
MYSQL_PASSWORD="abcd1234"
CONF_FOO_PATH="/etc"
CONF_FILES=("my.cnf" "my.cnf.d")
_result_socket=$(grep -s '^socket' $CONF_FOO_PATH/${CONF_FILES[1]}/*.cnf | awk -F[=\ ] '{print $NF}' | sort -u)
PORT=$(grep -s '^port' $CONF_FOO_PATH/${CONF_FILES[1]}/*.cnf | awk -F[=\ ] '{print $NF}' | sort -u)
MYSQL_SOCKET=${_result_socket:-'/var/lib/mysql/mysql.sock'}
LOCAL_BACKUP_DIR="/data/backup"
RCLONE="/usr/local/bin/rclone"
RCLONE_REMOTE="huawei"
RCLONE_BUCKET="backup-bucket/mysql"
WECHAT_WEBHOOK_KEY="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

[[ $# != 2 ]] && echo -e "Usage:\n\t$0 (库名) (full|incr)" && exit 1

raise_error() {
  msg="### 备份失败! \n#### $DB_NAME $TS $BACKUP_TYPE\n#### $1"
  for i in {1..3}; do
    curl -s "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=$WECHAT_WEBHOOK_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "msgtype": "markdown",
        "markdown": {
          "content": "'"$msg"'"
        }
      }'
    [[ $? == 0 ]] && break || sleep $i
  done
  echo "$msg"
  exit 1
}

[[ -z "$MYSQL_SOCKET" ]] && MYSQL_SOCKET="/var/lib/mysql/mysql.sock"

mysql -u"$MYSQL_USER" -p"$MYSQL_PASSWORD" -S "$MYSQL_SOCKET" -e "select 1;" >/dev/null 2>&1 || raise_error "数据库账号或Socket连接失败"

ulimit -n 1024000
backup_start_sec=$(date +%s)

mkdir -p $LOCAL_BACKUP_DIR/{lastTimeCheckPoint,thisTimeCheckPoint,tmpDir}

# 备份版本和配置上传
mysql -u"$MYSQL_USER" -p"$MYSQL_PASSWORD" -S "$MYSQL_SOCKET" -e "select version()\G" | \
awk '/version/{print $2}' | grep -o '[0-9\.]*' > $LOCAL_BACKUP_DIR/${DB_NAME}-${TS}.version
$RCLONE copy $LOCAL_BACKUP_DIR/${DB_NAME}-${TS}.version ${RCLONE_REMOTE}:${RCLONE_BUCKET}/ || raise_error "版本文件上传失败"
rm -f $LOCAL_BACKUP_DIR/${DB_NAME}-${TS}.version

tar -zcf $LOCAL_BACKUP_DIR/${DB_NAME}-${TS}-conf.tgz -C $CONF_FOO_PATH my.cnf my.cnf.d
$RCLONE copy $LOCAL_BACKUP_DIR/${DB_NAME}-${TS}-conf.tgz ${RCLONE_REMOTE}:${RCLONE_BUCKET}/ || raise_error "配置文件上传失败"
rm -f $LOCAL_BACKUP_DIR/${DB_NAME}-${TS}-conf.tgz

BACKUP_OBJECT="${DB_NAME}-${TS}-${BACKUP_TYPE}.tar.gz"

if [[ $BACKUP_TYPE == "full" ]]; then
  mariabackup --backup --slave-info --tmpdir=$LOCAL_BACKUP_DIR/tmpDir \
    --user="$MYSQL_USER" --password="$MYSQL_PASSWORD" --socket="$MYSQL_SOCKET" \
    --extra-lsndir=$LOCAL_BACKUP_DIR/thisTimeCheckPoint \
    --stream=mbstream | gzip -c | $RCLONE rcat --s3-upload-cutoff 64M --s3-chunk-size 128M ${RCLONE_REMOTE}:${RCLONE_BUCKET}/${BACKUP_OBJECT} || raise_error "全量备份或上传失败"

  ### >>>>>>>>>>> 恢复测试 START <<<<<<<<<<<<<<
  echo "开始恢复测试..."
  RESTORE_TEST_DIR="$LOCAL_BACKUP_DIR/restore_test/$PORT"
  rm -rf "$RESTORE_TEST_DIR" && mkdir -p "$RESTORE_TEST_DIR"

  file_list=()
  full_ts=""
  for ((i=0;i<=7;i++)); do
    ts_check=$(date -d "$TS -$i day" +%Y%m%d)
    if $RCLONE lsf ${RCLONE_REMOTE}:${RCLONE_BUCKET}/ | grep -q "^${DB_NAME}-${ts_check}-full.tar.gz$"; then
      full_ts="$ts_check"
      file_list+=("${DB_NAME}-${ts_check}-full.tar.gz")
      break
    fi
  done

  if [[ -z "$full_ts" ]]; then
    raise_error "恢复测试失败:未找到全量备份"
  fi

  for ((i=0;i<=7;i++)); do
    ts_check=$(date -d "$full_ts +$i day" +%Y%m%d)
    if $RCLONE lsf ${RCLONE_REMOTE}:${RCLONE_BUCKET}/ | grep -q "^${DB_NAME}-${ts_check}-incr.tar.gz$"; then
      file_list+=("${DB_NAME}-${ts_check}-incr.tar.gz")
    fi
  done

  echo "下载恢复测试文件:${file_list[@]}"
  for f in "${file_list[@]}"; do
    $RCLONE copy ${RCLONE_REMOTE}:${RCLONE_BUCKET}/$f $RESTORE_TEST_DIR/ || raise_error "恢复测试失败:下载文件 $f 失败"
  done

  echo "开始测试解压和prepare..."
  full_dir="$RESTORE_TEST_DIR/full"
  mkdir -p "$full_dir"
  gzip -dc $RESTORE_TEST_DIR/${file_list[0]} | mbstream -x -C "$full_dir" || raise_error "全量解压失败"
  mariabackup --prepare --target-dir="$full_dir" --apply-log-only || raise_error "全量prepare失败"

  prev_lsn=$(awk -F'= ' '/to_lsn/ {print $2}' $full_dir/xtrabackup_checkpoints)

  for ((i=1;i<${#file_list[@]};i++)); do
    incr_dir="$RESTORE_TEST_DIR/incr_$i"
    mkdir -p "$incr_dir"
    gzip -dc $RESTORE_TEST_DIR/${file_list[$i]} | mbstream -x -C "$incr_dir" || raise_error "增量 $i 解压失败"

    from_lsn=$(awk -F'= ' '/from_lsn/ {print $2}' $incr_dir/xtrabackup_checkpoints)

    if [[ "$from_lsn" != "$prev_lsn" ]]; then
      echo "增量 $i from_lsn ($from_lsn) 与前一个 to_lsn ($prev_lsn) 不一致,跳过"
      continue
    fi

    mariabackup --prepare --target-dir="$full_dir" --incremental-dir="$incr_dir" --apply-log-only || raise_error "增量 $i prepare失败"
    prev_lsn=$(awk -F'= ' '/to_lsn/ {print $2}' $incr_dir/xtrabackup_checkpoints)
  done

  mariabackup --prepare --target-dir="$full_dir" || raise_error "最后apply-log失败"
  echo "恢复测试通过"
  rm -rf "$RESTORE_TEST_DIR"
  ### >>>>>>>>>>> 恢复测试 END <<<<<<<<<<<<<<

elif [[ $BACKUP_TYPE == "incr" ]]; then
  mariabackup --backup --slave-info --tmpdir=$LOCAL_BACKUP_DIR/tmpDir \
    --user="$MYSQL_USER" --password="$MYSQL_PASSWORD" --socket="$MYSQL_SOCKET" \
    --extra-lsndir=$LOCAL_BACKUP_DIR/thisTimeCheckPoint \
    --incremental-basedir=$LOCAL_BACKUP_DIR/lastTimeCheckPoint \
    --stream=mbstream | gzip -c | $RCLONE rcat ${RCLONE_REMOTE}:${RCLONE_BUCKET}/${BACKUP_OBJECT} || raise_error "增量备份或上传失败"
else
  raise_error "参数错误: 仅支持 full 或 incr"
fi

backup_end_sec=$(date +%s)
backup_spent_sec=$[ $backup_end_sec - $backup_start_sec ]
START=$(date -d@$backup_start_sec +"%Y-%m-%d %H:%M:%S")
END=$(date -d@$backup_end_sec +"%Y-%m-%d %H:%M:%S")
SPENT="$((backup_spent_sec / 3600))小时 $(((backup_spent_sec / 60) % 60))分钟 $((backup_spent_sec % 60))秒"
SIZE=$(/usr/local/bin/rclone lsf ${RCLONE_REMOTE}:${RCLONE_BUCKET}/ --format "ps" | grep "^${DB_NAME}-${TS}-${BACKUP_TYPE}.tar.gz;" | awk -F';' '{ printf "%d", $2 / 1024 / 1024 / 1024 }')

rm -rf $LOCAL_BACKUP_DIR/lastTimeCheckPoint/*
cp -a $LOCAL_BACKUP_DIR/thisTimeCheckPoint/* $LOCAL_BACKUP_DIR/lastTimeCheckPoint/

echo "备份成功: $DB_NAME-$TS-$BACKUP_TYPE 已上传到OBS"
echo -e "备份成功!\n Size: $SIZE GB \nStart: $START\n  End: $END\nSpent: $SPENT"

设置定时任务

1
2
3
4
#Ansible: 全量备份 每周六
25 3 * * 6 bash -x /usr/local/scripts/mysql-backup.sh DB_NAME full > /var/log/mysql/DB_NAME-$(date "+\%Y\%m\%d")-full.log 2>&1
#Ansible: 增量备份 每周日至周五
25 3 * * 0-5 bash -x /usr/local/scripts/mysql-backup.sh DB_NAME incr > /var/log/mysql/DB_NAME-$(date "+\%Y\%m\%d")-incr.log 2>&1