阿里云 Redis 2.8 迁移到 5.0 的坑与解决方案

以下是对 redis 2.8 迁移到 redis 5.0 的操作实践,主要用到了下面两个工具

缘起

我们很多服务使用了阿里云的云数据库 Redis 产品,当时是 2.8 版本,使用后一直没有动过。2024年1月31日阿里云发送了一封邮件,告知用户从 2024年7月31日 起,对 2.8 版本的云数据库 Redis 停止全面支持 EOFS (End of Full Support):

尊敬的阿里云Redis用户,
云数据库 Redis 版2.8版本实计划于2024年07月31日起停止全面支持,进入EOFS(End of Ful Support) 阶段,此阶段我们将停止功能送代升级、停止续费、停止升降配和扩容、停止售后服务,请尽快升级实例至高版本。详情请看公告https://click.aliyun.com/m/1000389479/

在阿里云的文档里关于如何进行兼容性测试的环节里说:“您可以在原实例中,通过数据恢复功能,将当前Redis实例的备份数据克隆至一个新的高版本实例中,并进行测试、验证,更多信息请参见从备份集恢复至新实例。”。于是我们开始尝试对线上 Redis 实例进行升级测试。

问题

  • 阿里云官方的 Redis 2.8 实例已经不能再购买了,所以不能买个新的 2.8 实例做测试
  • 阿里云官方的数据导出任务,从 2.8 到 5.0 实例,尝试失败,报不支持的版本错误
  • 从 2.8 的落盘备份 rdb 文件导入到阿里云 5.0 实例,使用阿里云推荐的 redis-shake 导入失败,redis-shake 进程在没导入完成就自杀掉了……

尝试

本地导入

我在本地的一台服务器上搭建了 redis 5.0.13 服务器(注:阿里云上的 5.0 实例小版本为 5.2.6,为阿里云自行基于官方 5.0.13 修改后的版本),将 2.8 实例上的备份 rdb 文件下载(约 3 GB)后,使用 redis-port 导入。第一次导入失败,报超出 maxmemory 限制。于是修改了 redis.conf ,设定 5G 最大内存 maxmemory 5368709120

daemonize yes
pidfile /var/run/redisi_6379.pid
port 6379
bind 0.0.0.0
timeout 600
loglevel verbose
logfile /var/log/redis/redis_6379.log
databases 16
appendonly yes
appendfsync everysec
appendfilename appendonly.aof
dir /home/redisdata
maxclients 1000
maxmemory 5368709120
slowlog-log-slower-than 20000
slowlog-max-len 500
client-output-buffer-limit normal 0 0 0
client-output-buffer-limit slave 256mb 64mb 60
client-output-buffer-limit pubsub 32mb 8mb 60
requirepass password

再次使用 redis-port 导入成功:

#!/bin/bash

REDIS_INSTANCE=r-abcdef1234567
DATE=$(date -d "yesterday" '+%Y-%m-%d')
LOG=$(pwd)/logs/redis_sync_$DATE.log

# Download yesterday's backup file
#cd /root/aliyun_client
#python3 -u alibabacloud_sample/sample.py $REDIS_INSTANCE $DATE true |& tee -a $LOG 2>&1

cd /root
./redis-port restore -i /root/aliyun_client/$REDIS_INSTANCE.rdb -t 127.0.0.1:6379 -A 'password' --redis |& tee -a $LOG 2>&1

echo "Restore to local redis instance DONE!"

尝试使用 redis-shake 导入报错,错误信息乱码,也就没有再尝试。

阿里云服上尝试

因为使用阿里云内网速度更快,所以在一台测试用的阿里云服务器上尝试使用同样的办法将 2.8 的 rdb 备份导入到新购买的一个 5.0 实例(r-1234567abcdef.redis.rds.aliyuncs.com:6379)上。

redis-port

这次尝试 redis-port 执行到了 60% 到 85% 就会中断。

./redis-port restore -i ./r-abcdef1234567.rdb -t r-1234567abcdef.redis.rds.aliyuncs.com:6379 -A 'password' --redis

redis-port 中断

与阿里云技术支持沟通,对方建议先恢复到本地自建的 5.0,再导出本地 5.0 rdb 备份恢复到云实例 5.0。

经尝试,redis-port 导入 5.0 以后版本导出的 rdb 文件会报错(新版 rdb 文件头部为REDIS0009 开头):

2024/02/20 18:35:28 utils.go:361: [PANIC] parse rdb header error
[error]: verify version, invalid RDB version number 9
    1   /home/travis/gopath/src/github.com/CodisLabs/redis-port/pkg/rdb/loader.go:44
            github.com/CodisLabs/redis-port/pkg/rdb.(*Loader).Header
    0   /home/travis/gopath/src/github.com/CodisLabs/redis-port/cmd/utils.go:360
            main.newRDBLoader.func1
        ... ...
[stack]: 
    0   /home/travis/gopath/src/github.com/CodisLabs/redis-port/cmd/utils.go:361
            main.newRDBLoader.func1
        ... ...

5.0 rdb 文件:
某rdb文件

redis-shake

使用 redis-shake 工具,跑到快一半百分比进程也自动死掉了。

redis-shake中断

这里用到的 shake.toml

function = ""

[rdb_reader]
filepath = "/home/service/redis/aliyun_client/r-abcdef1234567.rdb"

[redis_writer]
cluster = false            # set to true if target is a redis cluster
address = "r-1234567abcdef.redis.rds.aliyuncs.com:6379" # when cluster is true, set address to one of the cluster node
username = ""              # keep empty if not using ACL
password = "password"              # keep empty if no authentication is required
tls = false


[advanced]
dir = "data"
ncpu = 0        # runtime.GOMAXPROCS, 0 means use runtime.NumCPU() cpu cores
pprof_port = 0  # pprof port, 0 means disable
status_port = 0 # status port, 0 means disable

# log
log_file = "shake.log"
log_level = "info"     # debug, info or warn
log_interval = 5       # in seconds

rdb_restore_command_behavior = "rewrite" # panic, rewrite or skip

pipeline_count_limit = 1024

target_redis_client_max_querybuf_len = 1024_000_000

target_redis_proto_max_bulk_len = 512_000_000

aws_psync = "" # example: aws_psync = "10.0.0.1:6379@nmfu2sl5osync,10.0.0.1:6379@xhma21xfkssync"

[module]
# The data format for BF.LOADCHUNK is not compatible in different versions. v2.6.3 <=> 20603
#target_mbbloom_version = 20603

定位问题

因为在公司服务器上我成功的使用 redis-port 将 2.8 的 rdb 备份导入到了 5.0.13 Redis 服务器里,所以我尝试在阿里云测试服务器上搭建一个同样的 5.0.13 本地 Redis 服务来测试。

服务器搭建完成后,使用同样的 redis-port 脚本尝试导入 2.8 的 rdb 备份,发现也是导入到了一半左右,进程就自杀了。

这时我们可以判断不是阿里云 Redis 实例的问题,而是阿里云服务器的问题。经过与本地服务器配置比较,我发现虽然两者都有 16 GB 内存,但阿里云服务器上没有 swap(阿里云主机创建时默认不包含 swap 分区),而本地服务器有 16 GB swap。

使用下面过程给阿里云测试服添加了 16 GB swap,再次运行 redis-port 脚本,成功了!尝试使用 redis-shake 来导入 2.8 rdb 到新的 5.0 实例,也成功!

因为我使用的阿里云测试服务器上还运行了不少其它程序,系统内存一直被占用得比较多。所以在没有 swap 的情况下,go 语言编写的两个工具都因为内存耗尽而自动退出了。

添加 swap
# 查看 swap
swapon -s

# 创建 swap 文件 /swap
dd if=/dev/zero of=/swap bs=1M count=16384

# 修改 swap 文件权限
chmod 600 /swap

# 创建 swap
mkswap /swap 

# 启用 swap
swapon /swap

# 再次查看 swap 情况
swapon -s

# 修改 /etc/fstab,保证系统启动时启用 swap

/swap    none    swap    sw    0 0

结论

阿里云 2.8 Redis 实例的 rdb 备份可以使用 redis-port 或 redis-shake 导入到 5.0 实例,前提是运行这两个脚本的阿里云服务器有足够的内存或 swap

这两个工具使用 go 语言开发,在因为缺少内存而崩溃时没有足够的错误信息输出,其实是需要改进的。

自动下载备份脚本

前述的 redis-port 脚本里注释掉了自动下载前一天 Redis 实例备份的代码,是我在阿里云发布的 Python工程示例 上略做了点修改。你也可以访问 https://next.api.aliyun.com/api/R-kvstore/2015-01-01/DescribeBackups 来在线查看接口的调用方式。

# -*- coding: utf-8 -*-
import os
import sys
import json
import requests

from typing import List

from alibabacloud_r_kvstore20150101.client import Client as R_kvstore20150101Client
from alibabacloud_tea_openapi import models as open_api_models
from alibabacloud_r_kvstore20150101 import models as r_kvstore_20150101_models
from alibabacloud_tea_util import models as util_models
from alibabacloud_tea_console.client import Client as ConsoleClient
from alibabacloud_tea_util.client import Client as UtilClient


class Sample:
    def __init__(self):
        pass

    @staticmethod
    def create_client(
        access_key_id: str,
        access_key_secret: str,
    ) -> R_kvstore20150101Client:
        """
        使用AK&SK初始化账号Client
        @param access_key_id:
        @param access_key_secret:
        @return: Client
        @throws Exception
        """
        config = open_api_models.Config(
            # 必填,您的 AccessKey ID,
            access_key_id=access_key_id,
            # 必填,您的 AccessKey Secret,
            access_key_secret=access_key_secret,
            # 这里写死了使用青岛区域服务器,可自行修改
            region_id = f'cn-qingdao'
        )
        # Endpoint 请参考 https://api.aliyun.com/product/R-kvstore
        config.endpoint = f'r-kvstore.aliyuncs.com'
        return R_kvstore20150101Client(config)

    @staticmethod
    def main(
        args: List[str],
    ) -> None:
        ConsoleClient.log('Start to query redis backups with args: ' + UtilClient.to_jsonstring(args))
        if len(args) > 3 or len(args) < 2:
            ConsoleClient.log('Please input instance_id, date, is_intranet(optional, default false), for example: \nr-bp123456789yw3lp4n 2023-11-06 true')
            sys.exit(1)

        is_intranet = False
        if len(args) == 3:
            is_intranet = (args[2].lower() == 'true')

        # 请确保代码运行环境设置了环境变量 ALIBABA_CLOUD_ACCESS_KEY_ID 和 ALIBABA_CLOUD_ACCESS_KEY_SECRET。
        # 工程代码泄露可能会导致 AccessKey 泄露,并威胁账号下所有资源的安全性。以下代码示例仅供参考,建议使用更安全的 STS 方式,更多鉴权访问方式请参见:https://help.aliyun.com/document_detail/378659.html
        client = Sample.create_client(os.environ['ALIBABA_CLOUD_ACCESS_KEY_ID'], os.environ['ALIBABA_CLOUD_ACCESS_KEY_SECRET'])
        describe_backups_request = r_kvstore_20150101_models.DescribeBackupsRequest(
            instance_id=args[0],
            start_time=args[1] + 'T00:00Z',
            end_time=args[1] + 'T23:59Z
        )
        runtime = util_models.RuntimeOptions()
        resp = client.describe_backups_with_options(describe_backups_request, runtime)
        ConsoleClient.log(UtilClient.to_jsonstring(resp))
        robj = json.loads(UtilClient.to_jsonstring(resp))

        for backup in robj['body']['Backups']['Backup']:
            url = backup['BackupDownloadURL']

            # 是否使用内网下载 URL
            if is_intranet:
                url = backup['BackupIntranetDownloadURL']

            ConsoleClient.log(backup['NodeInstanceId'] + ' Download URL: ' + url)

            r = requests.get(url, stream=True)

            fname = './' + backup['NodeInstanceId'] + '.rdb'

            if os.path.isfile(fname):
                os.remove(fname)

            with open(fname, 'wb') as f:
                for ch in r:
                    f.write(ch)
                f.close()

if __name__ == '__main__':
    Sample.main(sys.argv[1:])
  • 19
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值