以下是对 redis 2.8 迁移到 redis 5.0 的操作实践,主要用到了下面两个工具
- redis-port: https://github.com/CodisLabs/redis-port
- redis-shake: https://github.com/alibaba/RedisShake
缘起
我们很多服务使用了阿里云的云数据库 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
与阿里云技术支持沟通,对方建议先恢复到本地自建的 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 文件:
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:])