目 录CONTENT

文章目录

redis-cluster集群数据备份、同步与恢复(k8s环境)

Seven
2025-03-25 / 0 评论 / 0 点赞 / 14 阅读 / 22510 字 / 正在检测是否收录...

前言:

之前redis的部署一直是一主二从三哨兵,备份和恢复相对简单,用工具redis-dump基本能搞定,但是最近这个项目使用k8s自建redis,使用helm部署的redis-cluster三主三从在使用和同步数据就遇到了问题。网上也有很多工具,比如redis-shake是比较好用的工具,不过导出来的数据,在新集群死活导不进去,各种格式错误,sync模式使用不了,因为不在一个局域网中。然后就没办法只有想到了手动备份和数据迁移,其中也踩了不少坑,下面进入正题。

环境:

服务器

环境

网络

名称

存储

命名空间

阿里国际

ack(k8s)

cluster

redis-cluster

nas-动态存储(未挂载到ecs)

prod-db

阿里云

ack(k8s)

cluster

redis-cluster

nas (已经挂载到ecs)

pre-db

上面的环境都只能在集群内部访问,而且在不同的网络环境,如果通过暴露外网端口的方式,只能拿到暴露节点的数据。但是redis集群是分片式存储,除非把所有master节点都暴露出来,显然是不可能的。就算你能把所有节点爆出来,如果使用工具redis-shake,你会发现,你暴露出来的节点,是公网,而全拿到的节点ip是集群内部ip,所以无法进行连接。如果通过redis配置将ip宣讲配置cluster-announce-ip为公网ip,那么集群就会失效。而且也不可能把这个改为公网ip。

方案确定

最终通过几天的测试备份,恢复,同步。最终还是选择了通过备份快照导入,进行数据恢复,同步。

如果在同一局域网,或者是云端的同一pvc下,选redis-shake同步数据。

不在同一网络环境,就不行。

操作步骤

一、备份源reids数据

因为是在k8s环境里,首先要拿到master节点的ip。(因为三主三从,master节点不是固定的,如果有节点退出slave会接手master节点的工作)

kubectl exec -itn prod-db redis-cluster-0 -- redis-cli -a <passwd> cluster nodes
# 如果你在redis客户端可以直接使用
redis-cli -c -h ip -a password cluster nodes

拿到master 节点ip后,就去每一个master里去执行“save”命令可以用脚本跑

#用脚本:
HOSTS=$(kubectl exec -in boss-db redis-cluster-0 -- redis-cli -a "$passwd" cluster nodes | grep master | awk '{print $2}' | cut -d: -f1) // 获取master ip到数组 HOSTS 因为使用脚本不需要tty所以参数里不用“-t”
#遍历master进行save
for host in ${HOSTS[@]}; do
        echo "----------------------------${host} save begin-----------------------------" >> ${log_dir}/redis_backup_${DATE}.logs
        redis-cli -h ${host} -a "$passwd" save
        echo "----------------------------${host} save over------------------------------" >> ${log_dir}/redis_backup_${DATE}.logs
done

这样每一个master节点完成save,如果下一步将master节点的dump.rdb下载到目标集群。

二、还原redis数据

1、redis 集群配置

目标集群优先得更改配置文件:

关闭自动快照,关闭aof。

save ""
appendonly no
rdb-aof-use-rdb-preamble no  

重启redis 集群

然后检查配置:

# 检查 RDB 状态
redis-cli CONFIG GET save  
输出:save ""
# 检查 AOF 状态  
redis-cli CONFIG GET appendonly
输出:appendonly "no"

2、将dump文件导入到目标集群

2.1 由于目前k8s使用的存储是动态存储可以使用
kubectl get pvc -n 命名空间 |grep redis
redis-data-redis-cluster-0   Bound    nas-dbd93212-3f27-466f-b083-50331ce9c22a   10Gi       RWO            alicloud-nas-subpath   4d17h
redis-data-redis-cluster-1   Bound    nas-a128389c-d49c-42f0-9303-0f4741f9d738   10Gi       RWO            alicloud-nas-subpath   4d17h
redis-data-redis-cluster-2   Bound    nas-a12ade42-d5f0-4c05-a512-666a29072eb8   10Gi       RWO            alicloud-nas-subpath   4d17h
redis-data-redis-cluster-3   Bound    nas-814550f3-731c-463b-95c6-a0fbbc2677af   10Gi       RWO            alicloud-nas-subpath   4d17h
redis-data-redis-cluster-4   Bound    nas-960596fe-46ac-4bc1-be40-d9ba69ce8b3b   10Gi       RWO            alicloud-nas-subpath   4d17h
redis-data-redis-cluster-5   Bound    nas-11ac8577-9743-460c-9ddb-bc059367e8e9   10Gi       RWO            alicloud-nas-subpath   4d17h

你得去所有的存储里面删除dump.rdb文件和appendonly的文件夹。虽然说slave不用删除但是最好还是删除,因为有可能你的slave会变为master。这样你导入的dump.rdb会不生效。

2.2获取master节点ip以及存储所在的nas目录。
HOSTS=$(kubectl exec -in pre-db redis-cluster-0 -- redis-cli -a ${DB_PASSWD} cluster nodes | grep master | awk '{print $2}' | cut -d: -f1) #获取master ip 地址。
pod_name=(kubectl get pods -n pre-db -o jsonpath='{range .items[*]}{.status.podIP}{"\t"}{.metadata.name}{"\n"}{end}' | grep -w -f <(echo "$HOSTS")) # 这个可以获取到master节点的pod_name,通过ip地址比对,将pod_name存到数组里面。
while read -r ip pod_name; do
    # 获取 PVC 名称,添加错误处理
    pvc_name=$(kubectl get pod "$pod_name" -n pre-db -o jsonpath='{.spec.volumes[?(@.persistentVolumeClaim)].persistentVolumeClaim.claimName}' 2>/dev/null)
    if [ -z "$pvc_name" ]; then
        echo "错误: 无法获取 Pod $pod_name 的 PVC 名称"
        continue
    fi

    # 获取 NAS 路径,检查空值
    nas_tmp=$(kubectl get pvc -n pre-db "$pvc_name" -o jsonpath='{.spec.volumeName}' 2>/dev/null)
    if [ -n "$nas_tmp" ]; then
        nas_dir+=("$nas_tmp")  # 正确追加到数组
    else
        echo "警告: PVC $pvc_name 无 volumeName"
    fi
done < <$pod_name
#这个是通过pod_name先拿到每个master节点的pvc名称,再通过pvc名称拿到nas名称

上面这一步需要和你具体的环境对比一下。比如你是用的pv,pvc是手动挂载的。你可以不用这一步。如果你是用的动态挂载,这样就能拿到nas的实际目录。当然前提是你需要将nas挂到一台服务器上,并且你可以访问。

2.3 将新的dump.rdb文件导入到nas目录里并重新启动集群,让其加载文件。

这里可以手动操作,如果你是要加入流水线之类的。我见意还是写脚本来完成。

i=1
for tmpdir in ${nas_dir[@]}; do
        echo -e "----------------------${tmpdir} backup begin and update rdb----------------------" >> ${FILE_LOG}
        cp ${nas}${tmpdir}/dump.rdb ${DIR_BACKUP}/${DATE}_${i}_dump.rdb
        echo "${nas}${tmpdir} save to ${DIR_BACKUP}/${DATE}_${i}_dump.rdb" >> ${FILE_LOG}
        cp ${sync_dir}/dump-${i}.rdb ${nas}${tmpdir}/dump.rdb
        echo "${sync_dir}/dump-${i}.rdb update to ${nas}${tmpdir}/dump.rdb" >> ${FILE_LOG}
        ((i++))
        echo -e "------------------------------${tmpdir} backup over--------------------------------" >> ${FILE_LOG}
done

这样就完成了目标redis集群的rdb文件备份,并把源集群的rdb导入到了目标集群的data里面。

重启redis cluster集群

可以在k8s里,直接删除redis cluster的容器,让它重新生成。还有就是柔和的重启服务(其实还是重建,k8s里面说是重启,核心就是删除,重建。)

kubectl rollout restart -n pre-db sts/redis-cluster

3、检查导入数据的完整性

其实目前你的业务已经可以正常访问数据了,导入的数据也在,但是他们的分片槽位是错乱的,数据是处于一个import状态。

你可以使用

redis-cli -a <passwd> --cluster check <masterIP>:<端口>

对集群的数据进行检查,可能槽位和数据全都有问题。《我这边由于已经修复过了看不到》

你可能遇到的问题

cluster setslot <槽位> stable

个别槽位偏移:

[OK] All nodes agree about slots configuration.

>>> Check for open slots...

[WARNING] Node 192.168.1.100:7000 has slots in importing state (5798,11479).

[WARNING] Node 192.168.1.100:7001 has slots in importing state (1734,5798).

[WARNING] Node 192.168.1.101:7002 has slots in importing state (11479).

[WARNING] The following slots are open: 5798,11479,1734

>>> Check slots coverage...

[OK] All 16384 slots covered.

解决办法:

一定要登录到告警信息中的节点和对应的端口上进行操作。

执行"cluster setslot <slot> stable"命令,表示取消对槽slot 的导入( import)或者迁移( migrate)。执行后,这部分slot槽的数据就没了。

# 以节点172.20.32.7为例,其他节点需同样操作
redis-cli -a <passwd> -h 192.168.1.100 -p 6379 \
    CLUSTER SETSLOT 5798 STABLE

# 重新声明槽归属至目标节点(e554a779...)
redis-cli -a <passwd> -h 192.168.1.100 -p 6379 \
    CLUSTER SETSLOT 5798 NODE e554a779384187eea814226f43967784c280dd52 //master节点id

如果大量槽位偏移:

使用 redis-cli --cluster fix 强制修复槽分配:

redis-cli -a <passwd> --cluster fix <masterIP>:6379 \
    --cluster-search-multiple-owners \
    --cluster-fix-with-unreachable-masters

如果数据恢复后,槽位混乱,你可能需要重新分配所有槽位,我不知道有什么简单的方法,我是先将所有槽位划到一个master,这样会导至其它两个master变为slave,然后删除两个slave,再加入集群成为master,再将槽位重新划给新的master,现在三个slave,三个master,但是你会发现,所有slave都是指向一个master,你又需要将slave删除掉重新加入集群,每个slave指向不同的master。

槽位迁移相关命令:

使用 --cluster reshard 并指定正确的主节点地址和 ID:
redis-cli --cluster reshard <目标主节点IP:端口> \  
  --cluster-from <源主节点ID> \  
  --cluster-to <目标主节点ID> \  
  --cluster-slots <迁移槽位数> \  
  --cluster-yes \  
  -a <密码>  
验证集群状态:
redis-cli --cluster check <任意主节点IP:端口> -a <密码>
# 清除所有槽位和集群配置  
FLUSHALL  
CLUSTER RESET HARD  

下面是通过 所有槽位重新分配的一个流程:

把所有槽位分配给a服务器。等分配完成后,再通过重新分配槽位给b和c服务器。
执行全局槽位迁移‌:
redis-cli --cluster reshard <A_IP>:<端口> \  
  --cluster-from all \  
  --cluster-to <A_ID> \  
  --cluster-slots 16384 \  
  --cluster-yes \  
  -a <密码>  

关键参数‌:

  • --cluster-from all:从所有节点迁移槽位。

  • --cluster-slots 16384:覆盖全部槽位(0-16383)。

redis-cli -h <A_IP> -p <端口> -a <密码> CLUSTER SLOTS  

输出应显示 A 节点持有所有槽位(0-16383).

清理 B/C 节点残留配置‌:
# 分别登录 B 和 C 节点执行  
redis-cli -h <节点IP> -p <端口> -a <密码>  
FLUSHALL  
CLUSTER RESET HARD  

重启 B/C 节点并确认角色为 master

  • 如果B/C节点变为了slave(当迁移数据很多的情况下就会出现)

解除 B/C 与 A 的主从关系,使其成为独立主节点。

# 登录 B 节点执行  
redis-cli -h <B_IP> -p <端口> -a <密码> CLUSTER RESET HARD  

# 登录 C 节点执行  
redis-cli -h <C_IP> -p <端口> -a <密码> CLUSTER RESET HARD  
#说明:CLUSTER RESET HARD 清除节点历史配置,使其脱离从节点角色‌

将 B/C 以主节点身份重新加入集群(不分配槽位)。

# 添加 B 为主节点  
redis-cli --cluster add-node <B_IP>:<端口> <A_IP>:<端口> -a <密码>  

# 添加 C 为主节点  
redis-cli --cluster add-node <C_IP>:<端口> <A_IP>:<端口> -a <密码>  
# 验证:执行 CLUSTER NODES 确认 B/C 角色为 master 且无槽位‌

然后继续分配槽位-->下一步

分配槽位给 B‌(示例:0-5460):
redis-cli --cluster reshard <B_IP>:<端口> \  
  --cluster-from <A_ID> \  
  --cluster-to <B_ID> \  
  --cluster-slots 5461 \  
  --cluster-yes \  
  -a <密码>  
分配槽位给 C‌(示例:5461-10922):
redis-cli --cluster reshard <C_IP>:<端口> \  
  --cluster-from <A_ID> \  
  --cluster-to <C_ID> \  
  --cluster-slots 5462 \  
  --cluster-yes \  
  -a <密码>  
分配从节点给 B/C
# 将 D 设为 B 的从节点  
redis-cli --cluster add-node <D_IP>:<端口> <B_IP>:<端口> \  
  --cluster-slave \  
  --cluster-master-id <B_ID> \  
  -a <密码>  

# 将 E 设为 C 的从节点  
redis-cli --cluster add-node <E_IP>:<端口> <C_IP>:<端口> \  
  --cluster-slave \  
  --cluster-master-id <C_ID> \  
  -a <密码>  
验证最终分配‌:
redis-cli --cluster check <A_IP>:<端口> -a <密码> 
输出应为 [OK] All 16384 slots covered,且 A/B/C 的槽位范围互不重叠。

全自动化脚本:

源redis cluster:(k8s环境)

#!/bin/bash
set -euo pipefail
passwd=$1
log_dir=/data/redis-backup/logs
backup_dir=/data/redis-backup/backup
container_dir=bitnami/redis/data
sync_dir=/data/redis-backup/syncdb
DATE=$(date +%Y%m%d-%T)

# 定义空数组
POD_NAME=()
# 文件保留天数
DAY=15
DAY_LOG=$(expr ${DAY} + 7)

# 移除 -t 参数避免 TTY 错误,使用 --no-raw 禁用交互式模式
HOSTS=$(kubectl exec -in boss-db redis-redis-cluster-0 -- redis-cli -a "$passwd" cluster nodes | grep master | awk '{print $2}' | cut -d: -f1)


POD_NAME=$(kubectl get pods -n boss-db -o jsonpath='{range .items[*]}{.status.podIP}{"\t"}{.metadata.name}{"\n"}{end}' | grep -w -f <(echo "$HOSTS") | awk '{print $2}' )


echo -e "\n=============================$(date +%F\ %T)=============================" >> ${log_dir}/redis_backup_${DATE}.logs

for host in ${HOSTS[@]}; do
        echo "----------------------------${host} save begin-----------------------------" >> ${log_dir}/redis_backup_${DATE}.logs
        redis-cli -h ${host} -a "$passwd" save
        echo "----------------------------${host} save over------------------------------" >> ${log_dir}/redis_backup_${DATE}.logs
done
i=1
for rdb in ${POD_NAME[@]}; do
        echo "-----------------------------${rdb} backup begin-------------------------------" >> ${log_dir}/redis_backup_${DATE}.logs
        kubectl cp boss-db/${rdb}:${container_dir}/dump.rdb ${sync_dir}/dump-${i}.rdb
        echo "${rdb} save to ${sync_dir}/dump-${i}.rdb" >> ${log_dir}/redis_backup_${DATE}.logs
        cp ${sync_dir}/dump-${i}.rdb ${backup_dir}/${rdb}_${DATE}_dump.rdb
        echo "${rdb} save to ${backup_dir}/dump-${rdb}.rdb" >> ${log_dir}/redis_backup_${DATE}.logs
        ((i++))
        echo "------------------------------${rdb} backup over--------------------------------" >> ${log_dir}/redis_backup_${DATE}.logs
done

echo -e "\n================================$(date +%F\ %T) save seccuess===============================" >> ${log_dir}/redis_backup_${DATE}.logs

OLD_BACKUP=$(find ${backup_dir} -type f -mtime +${DAY} -name '*_dump.rdb')
OLD_LOGS=$(find ${log_dir} -type f -mtime +${DAY_LOG} -name 'redis_backup_*.logs')

# 遍历旧备份文件
for bak in "${OLD_BACKUP[@]}"; do
    # 删除旧备份
    rm -f ${bak}
    echo "------------------- Deleted old bak files -------------------" >> ${log_dir}/redis_backup_${DATE}.logs
    echo "${bak}" >> ${log_dir}/redis_backup_${DATE}.logs
done
# 遍历旧日志
for log in "${OLD_LOGS[@]}"; do
    # 删除旧日志
    rm -f ${log}
    echo "------------------- Deleted old log files -------------------" >> ${log_dir}/redis_backup_${DATE}.logs
    echo "${log}" >> ${log_dir}/redis_backup_${DATE}.logs
done
echo -e "------------------ $(date +%F\ %T) End ------------------n"
echo -e "================== $(date +%F\ %T) End ==================n" >> ${log_dir}/redis_backup_${DATE}.logs

目标redis cluster

#!/bin/bash
# 数据库连接信息
set -euo pipefail
DB_PASSWD=$1
nas=/nas/data/k8s/
# 时间格式化,如 20211216
DATE=$(date +%Y%m%d-%T)
# 备份文件目录
DIR_BACKUP="/data/redis-backup/backup"
# 日志目录: ${HOME}/data/db-backup/logs
DIR_LOG="${DIR_BACKUP}/../logs"
# 日志文件: ${HOME}/data/db-backup/logs/db_backup.INFO.2021-12-30.log
FILE_LOG="${DIR_LOG}/redis_backup.`date +%F`.log"
sync_dir="/data/redis-backup/syncdb"
# 文件保留天数
DAY=15  
DAY_LOG=$(expr ${DAY} + 7)
        
# 定义空数组
nas_dir=()

# 移除 -t 参数避免 TTY 错误,使用 --no-raw 禁用交互式模式
HOSTS=$(kubectl exec -in pre-db redis-cluster-0 -- redis-cli -a ${DB_PASSWD} cluster nodes | grep master | awk '{print $2}' | cut -d: -f1)

echo "$HOSTS"
# 避免管道导致子 Shell,使用进程替换保持作用域
while read -r ip pod_name; do
    # 获取 PVC 名称,添加错误处理
    pvc_name=$(kubectl get pod "$pod_name" -n pre-db -o jsonpath='{.spec.volumes[?(@.persistentVolumeClaim)].persistentVolumeClaim.claimName}' 2>/dev/null)
    if [ -z "$pvc_name" ]; then
        echo "错误: 无法获取 Pod $pod_name 的 PVC 名称"
        continue
    fi

    # 获取 NAS 路径,检查空值
    nas_tmp=$(kubectl get pvc -n pre-db "$pvc_name" -o jsonpath='{.spec.volumeName}' 2>/dev/null)
    if [ -n "$nas_tmp" ]; then
        nas_dir+=("$nas_tmp")  # 正确追加到数组
    else
        echo "警告: PVC $pvc_name 无 volumeName"
    fi
done < <(kubectl get pods -n pre-db -o jsonpath='{range .items[*]}{.status.podIP}{"\t"}{.metadata.name}{"\n"}{end}' | grep -w -f <(echo "$HOSTS"))

# 保存快照镜像
echo -e "n----------------- $(date +%F\ %T) Start -----------------"
echo -e "n================= $(date +%F\ %T) Start =================" >> ${FILE_LOG}

for rdb in ${HOSTS[@]}; do
    echo "------------------------Backed-up host:[ ${rdb} ] ------------------" >> ${FILE_LOG}
    redis-cli -h ${rdb} -a ${DB_PASSWD} save
    echo "----------------------Save rdb file over ---------------------------" >> ${FILE_LOG}
done

echo -e "n=====================begin copy rdb file to backup_dir======================" >> ${FILE_LOG}

#备份及更新生产rdb到预发布环境
i=1
for tmpdir in ${nas_dir[@]}; do
        echo -e "----------------------${tmpdir} backup begin and update rdb----------------------" >> ${FILE_LOG}
        cp ${nas}${tmpdir}/dump.rdb ${DIR_BACKUP}/${DATE}_${i}_dump.rdb
        echo "${nas}${tmpdir} save to ${DIR_BACKUP}/${DATE}_${i}_dump.rdb" >> ${FILE_LOG}
        cp ${sync_dir}/dump-${i}.rdb ${nas}${tmpdir}/dump.rdb
        echo "${sync_dir}/dump-${i}.rdb update to ${nas}${tmpdir}/dump.rdb" >> ${FILE_LOG}
        ((i++))
        echo -e "------------------------------${tmpdir} backup over--------------------------------" >> ${FILE_LOG}
done


kubectl rollout restart -n pre-db sts/redis-cluster

if [ $? -eq 0 ];then
        echo "wait redis server restart !!!"  >> ${FILE_LOG}
else
        echo "redis sever restart faile ****" >> ${FILE_LOG}
        exit 1
fi

# 至此, 备份已完成, 下面是清理备份的旧文件, 释放磁盘空间


OLD_BACKUP=$(find ${DIR_BACKUP} -type f -mtime +${DAY} -name '*_dump.rdb')
OLD_LOGS=$(find ${DIR_LOG} -type f -mtime +${DAY_LOG} -name 'redis_backup.*.log')

# 遍历旧备份文件
for bak in "${OLD_BACKUP[@]}"; do
    # 删除旧备份
    rm -f ${bak}
    echo "------------------- Deleted old bak files -------------------" >> ${FILE_LOG}
    echo "${bak}" >> ${FILE_LOG}
done
# 遍历旧日志
for log in "${OLD_LOGS[@]}"; do
    # 删除旧日志
    rm -f ${log}
    echo "------------------- Deleted old log files -------------------" >> ${FILE_LOG}
    echo "${log}" >> ${FILE_LOG}
done
echo -e "------------------ $(date +%F\ %T) End ------------------n"
echo -e "================== $(date +%F\ %T) End ==================n" >> ${FILE_LOG}

中间却一个下载dump文件的步骤,大家可以自已添加到目标脚本里面。(我的环境是在jenkins里面配置了自动下载,就没有体现在脚本里面。)

到此为止,这次reids cluster的踩坑结束,如果大家有什么方便的方法欢迎留言告之。(提示我使用了redis-shake还原rdb文件,测试没有成功,各种格式错误,可能是我使用错误)

0

评论区