一、业务备份中的常规需求
在业务备份中,通常我们需要指定某些业务库进行周期性的全量备份,并且在其他时间需要增量备份。在业务库出现问题,或者误删时,能够尽可能的恢复到事故前的一个时间点,减少数据丢失。
然而在tidb使用中,官方给我们了很好的解决方案,就是快照备份和日志备份的方式。
具体的说明请参考官网:https://docs.pingcap.com/zh/tidb/stable/br-use-overview/
但是官方给了这种方案,我们怎么能优雅的集成使用呢,比如我想定期在周一和周四凌晨1点进行快照备份,其他时间进行日志备份。保留最近10天内的快照备份(3次快照备份),日志备份保留最久一次的快照备份之后的数据。最近因为业务需求,所以整理了一个shell脚本的方式来提供上述的备份方式。本来想着用go或者python写,但是感觉go对不同平台有兼容性要求,python呢还得安装环境和依赖,比较麻烦。shell脚本直接在linux上执行就好。
二、脚本的依赖组件
脚本中需要两个组件来支持运行:
第一个是br的执行包,这个在官网中下载tools包中可以得到。
第二个是操作s3存储的客户端,这里我选择使用minio的客户端mc,可以在minio的官网下载:https://dl.min.io/aistor/mc/release/,直接下载如下的文件mc即可,该脚本使用该组件来来删除全量备份数据。
三、脚本的说明
执行脚本后会看到如下提示:
支持如下参数:
logstart:仅开启日志备份
logstatus:查看日志备份的状态
snapshot:仅执行一次快照备份。
task:任务备份,会自动判断是否开启日志备份,没有的话,会自动开启;根据配置的周期,定期快照备份;根据配置的保留天数,定时删除快照备份和日志备份数据。
运行实例:
# 仅执行一直快照备份,会备份到指定bucket下的snapshot-yyyyMMdd目录下。
./br-backup.sh snapshot
具体脚本如下:
#!/bin/bash
######参数#########
# s3对象存储的秘钥key
s3_access_key=xljtCAbfSIWeKJHBv3az
# s3对象存储的秘钥value
s3_secret_access_key=r9KwGrIii26aB23sKTQZajCYAHAcqDRd7XfuXY02
# s3对象存储的bucket
s3_bucket_name=backup
# s3对象存储的地址 http://host:port
s3_server=http://192.168.0.188:9000
# minio客户端地址,结尾带/
minio_client_path=/opt/soft/
# br执行文件的路径,结尾带/
br_path=/opt/soft/
# 执行log地址,结尾带/
log_path=/opt/soft/
# tidb的pd地址ip:port,多个端口用英文逗号分隔
pd_path=192.168.0.175:2379
# 快照备份周期,0周日、 1周一、 2周二、 3周三、 4周四、 5周五、 6周六, 多个日期用英文逗号分隔
backup_period=0,3,4
# 保留日志的时长,单位天
keep_days=1
# 需要备份的数据库,比如foo.*,表示备份foo库下的所有表,多个条件用英文逗号分隔,设置为空时则不做过滤条件
filters="db_monitor.*,kb_info.*,rag_test.*,zhql.*"
# 备份数据加密方法:aes128-ctr、aes192-ctr 和 aes256-ctr,日志加密仅在v8.4.0后版本才支持,当设置为空时,则不需要加密
crypt_method=aes128-ctr
# 加密密钥,十六进制字符串格式,aes128-ctr 对应 128 位(16 字节)密钥长度,aes192-ctr 为 24 字节,aes256-ctr 为 32 字节
crypt_key=fc125f1d7ca3a51d1a02eefe3941d86f
# log备份的任务名称
log_task_name=pitr
mc_host_name=s3-host
# 判断今天是否在配置的周期里
is_backup_day() {
# 如果配置为空,直接返回成功
if [ -z "$backup_period" ]; then
return 0
fi
local today
today=$(date +%w) # 获取今天周几
IFS=',' read -ra days <<< "$backup_period"
for d in "${days[@]}"; do
if [ "$today" -eq "$d" ]; then
return 0 # 匹配成功
fi
done
return 1 # 没匹配到
}
# 记录日志的方法
log_file() {
local msg="$1"
# 获取当前时间,格式:YYYY-MM-DD HH:MM:SS.mmm
local ts
ts=$(date +"%Y-%m-%d %H:%M:%S.%3N")
# 输出到终端和日志文件
echo "${ts} ${msg}" | tee -a "${log_path}backup.log"
}
# 快照备份方法
backup_snapshot(){
# 获取日期格式串
dateT=$(date +%Y%m%d)
cmd="${br_path}br backup full --pd=\"${pd_path}\" --storage=\"s3://${s3_bucket_name}/snapshot-${dateT}?access-key=${s3_access_key}&secret-access-key=${s3_secret_access_key}\" --ratelimit 128 --s3.endpoint \"${s3_server}\" --log-file \"${log_path}backup_snapshot_${dateT}.log\""
if [ -n "${crypt_method}" ]; then
cmd="$cmd --crypter.method ${crypt_method} --crypter.key ${crypt_key} "
fi
# 用逗号分隔,逐个拼接 -f 参数
if [ -n "${filters}" ]; then
IFS=',' read -ra arr <<< "$filters"
for f in "${arr[@]}"; do
cmd="$cmd --filter '$f'"
done
fi
# 拼接存储参数
log_file "执行命令:$cmd"
eval "$cmd"
log_file "全量备份结束...."
}
# 获取快照的ts时间戳
log_snapshot_ts(){
# 获取该次全量备份的时间戳
log_file "获取快照备份时间戳"
# 构造可选参数
extra_args=""
if [ -n "${crypt_method}" ]; then
extra_args="--crypter.method ${crypt_method} --crypter.key ${crypt_key}"
fi
FULL_BACKUP_TS=`${br_path}br validate decode --field="end-version" --storage "s3://${s3_bucket_name}/snapshot-${dateT}?access-key=${s3_access_key}&secret-access-key=${s3_secret_access_key}" --s3.endpoint "${s3_server}" ${extra_args} | tail -n1`
echo "${dateT},${FULL_BACKUP_TS}" >> "${log_path}full_backup_ts"
log_file "快照备份时间戳为:${FULL_BACKUP_TS}"
}
# log备份方法
backup_log(){
# 判断log备份的状态
output=`${br_path}/br log status --task-name=${log_task_name} --pd ${pd_path}`
if echo "$output" | grep -q "NORMAL"; then
log_file "log 备份已开启"
else
# 构造可选加密参数
local extra_args_log=""
if [ -n "${crypt_method}" ]; then
extra_args_log="--log.crypter.method ${crypt_method} --log.crypter.key ${crypt_key}"
fi
# 构造表过滤条件
local extra_filters=""
if [ -n "${filters}" ]; then
# 用逗号分隔,逐个拼接 -f 参数
IFS=',' read -ra arr <<< "$filters"
for f in "${arr[@]}"; do
extra_filters="$extra_filters --filter '$f'"
done
fi
# 备份命令
# 拼接完整命令
cmd="${br_path}br log start \
--task-name=${log_task_name} --pd \"${pd_path}\" \
--storage \"s3://${s3_bucket_name}/log-backup?access-key=${s3_access_key}&secret-access-key=${s3_secret_access_key}\" \
--s3.endpoint \"${s3_server}\" \
${extra_args_log} ${extra_filters} \
--log-file \"${log_path}backup_log.log\""
log_file "开启日志备份的命令:$cmd"
eval "$cmd"
#
sleep 1
# 判断log备份的状态
output=`${br_path}/br log status --task-name=${log_task_name} --pd ${pd_path}`
if echo "$output" | grep -q "NORMAL"; then
log_file "log 备份已开启"
else
log_file "log 开启失败,异常信息:${output}"
exit 1
fi
fi
}
# 获取log备份的状态
get_log_backup_status(){
log_file "获取log备份状态"
output=`${br_path}/br log status --task-name=${log_task_name} --pd ${pd_path}`
log_file "$output"
}
# 清除日志备份,清除上次全量备份之前的日志备份
delete_log_backup() {
local ts1="$1"
log_file "删除日志备份的时间戳为:$ts1"
# 清除上次全量备份之前的日志
${br_path}br log truncate --until=${ts1} \
--storage "s3://${s3_bucket_name}/log-backup?access-key=${s3_access_key}&secret-access-key=${s3_secret_access_key}" \
--s3.endpoint "${s3_server}" \
--yes
}
# 获取最老的一次备份记录
get_oldest_ts() {
# 获取文件中第一行的数据
local line
line=$(head -n 1 "${log_path}full_backup_ts")
# 拆分出日期和时间戳
IFS=',' read -r date_val ts_val <<< "$line"
#获取需要删除的日期
day=$(date -d "${keep_days} days ago" +"%Y%m%d")
# 如果记录的日期小于等于需要删除的日期,则删除记录里的数据,否则忽略
if (( date_val <= day )); then
log_file "删除最旧的一条记录:${line}"
sed -i '1d' "${log_path}full_backup_ts"
fi
echo "$line"
}
# 先初始化本地mc的参数
init_mc_local(){
# 老版本minio客户端mc为 mc config host list
output=`${minio_client_path}mc alias list`
if echo "$output" | grep -q "${mc_host_name}"; then
log_file "mc host已存在...."
return 0
else
# 老版本minio客户端mc为 mc config host add
${minio_client_path}mc alias set ${mc_host_name} ${s3_server} ${s3_access_key} ${s3_secret_access_key} --api s3v4
# 老版本minio客户端mc为 mc config host list
output=`${minio_client_path}mc alias list`
if echo "$output" | grep -q "${mc_host_name}"; then
log_file "mc host创建成功"
else
log_file "mc host创建失败,失败信息:${output}"
exit 1
fi
fi
}
# 清除日志备份,清除上次全量备份之前的日志备份
delete_full_backup(){
day=$(date -d "${keep_days} days ago" +"%Y%m%d")
if init_mc_local; then
log_file "开始删除快照备份....."
# 获取日期列表
output=`${minio_client_path}mc ls ${mc_host_name}/${s3_bucket_name}/`
if echo "$output" | grep -q "snapshot-${day}"; then
${minio_client_path}mc rm --recursive --force ${mc_host_name}/${s3_bucket_name}/snapshot-${day}/
log_file "备份删除成功....."
else
log_file "没有相关的全量备份....,忽略删除....."
fi
else
log_file "ERROR 初始化mc host失败....."
exit 1
fi
}
# 检查log备份的状态
task_backup(){
log_file "............任务备份开始............"
# 先判断log备份是否正常
output=`${br_path}/br log status --task-name=${log_task_name} --pd ${pd_path}`
if echo "$output" | grep -q "NORMAL"; then
log_file "log 备份检查正常...."
else
log_file "${log_task_name} 没有在运行中,创建一个log任务"
backup_log
fi
# 运行快照备份,先检查是否在设定的时间点内
if is_backup_day; then
log_file "快照备份开始...."
backup_snapshot
log_file "快照备份结束...."
log_snapshot_ts
else
log_file "今天不是快照备份日,忽略快照备份......."
fi
# 获取最老的一次备份记录
local data_line="$(get_oldest_ts)"
log_file "获取文件的最后数据:${data_line}"
IFS=',' read -r v1 v2 <<< "$data_line"
log_file "获取文件的日期,日期:${v1},时间戳:${v2}"
# 删除日志备份
delete_log_backup "${v2}"
# 删除快照备份
delete_full_backup
}
# 读取第一个参数
action=$1
# 判断执行哪个函数
case ${action} in
logstart)
backup_log
;;
logstatus)
get_log_backup_status
;;
snapshot)
backup_snapshot
;;
task)
task_backup
;;
*)
echo "用法: $0 {logstart|logstatus|snapshot|task}"
exit 1
;;
esac
在task备份方式执行后会生成如下文件:
-rw-r--r-- 1 root root 41294 Sep 10 14:33 backup.log
-rw------- 1 root root 27667 Sep 10 14:28 backup_log.log
-rw------- 1 root root 197774 Sep 10 14:33 backup_snapshot_20250910.log
-rw-r--r-- 1 root root 28 Sep 10 14:33 full_backup_ts
backup.log:是各种命令的执行日志,记录相关的时间和执行步骤。
backup_log.log:日志备份的日志
backup_snapshot_20250910.log:某次快照备份的日志。
full_backup_ts:快照备份的记录,分段式的yyyyMMdd,ts;yyyyMMdd为备份的日期,ts为某个快照备份的时间戳
四、脚本定时执行
脚本定时需要依赖linux的定时器,直接crontab -e
# 每天凌晨1点定时执行。
00 01 * * * sh /opt/soft/br-backup.sh task
五、总结
br是tidb备份的一个利器,在备份和恢复上起到了很大的作用,而且采用物理备份的方式增加了备份的速度,而且还提供了不同的加密方式。同样的自动化运维也是节省人力的方式,希望本次分享能给大家有所帮助。