前言
那阵子好几个项目要从老机房迁到阿里云,redis 直接用了阿里云带密码认证的实例。开发不想丢数据,又不愿意自己导,最后落到运维头上。麻烦在于:阿里云的 redis 实例只给访问地址、端口、密码,不支持 slaveof,主从同步这条路走不通,只能自己写个 python 脚本把源 redis 的数据取出来再写进去。
脚本基于开源工具(作者 JaesonCheng,见文末参考)整理改造,实测 7.7 万个 key 同步只用了 6 秒。
1、环境与造数
三个测试实例:
- redis1:localhost:4500(不带认证)
- redis2:localhost:4600(不带认证)
- redis3:localhost:4700(带认证,密码 redistest)
先往 redis1 写 10 万个 key 当测试数据:
1
|
/usr/bin/redis-benchmark -h localhost -p 4500 -t set -r 100000 -n 1000000
|
2、脚本设计思路
migrate_redis.py 的几个关键设计:
- pipeline 批量同步,不走 for 循环一条条 get/set,速度差几个量级
- 处理 key 过期、value 为空、多源合并时的 key 冲突,各自计数
- 冲突阈值策略:目标里已存在的冲突 key 少于 50 个就直接跳过这些继续同步;超过 50 个直接退出不同步——冲突太多说明目标不是空的,大概率是跑错了环境,宁可停
- 开始前打印源 key 数量、内存占用,结束后打印总耗时和三类异常计数
核心同步逻辑用的是 DUMP + RESTORE 而不是 GET/SET:
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
|
def pipe_restore(self, keys):
src_len = 0
keylist = []
for key in keys:
keylist.append(key)
self.src_pipe.dump(key) # 序列化整个 value(保留类型和编码)
self.src_pipe.ttl(key) # 顺带取 TTL
if src_len < self.pipesize:
src_len += 1
else:
keyttlList = self.src_pipe.execute()
for (k, t, v) in zip(keylist, keyttlList[1::2], keyttlList[0::2]):
if t == None or t == -1: # 永不过期
if v != None:
self.dst_pipe.restore(k, 0, v)
else:
self.addvaluenil() # value 为空计数
elif t == -2: # 已过期
self.addkeyoverdue()
else: # 有 TTL,原样保留过期时间
if v != None:
self.dst_pipe.restore(k, t, v)
else:
self.addvaluenil()
self.dst_pipe.execute()
src_len = 0
keylist = []
|
用 DUMP/RESTORE 的好处:序列化格式原样搬,数据类型(hash/list/zset)、编码、TTL 全都保真,GET/SET 只能搬 string 类型。
pipesize 定 1000,注释里写清楚了原因:源是线上 redis 时 pipeline 太大会产生阻断,影响正常请求。
冲突检查也是 pipeline 批量 exists:
1
2
3
4
5
6
7
8
9
10
11
12
|
def checkeyexist(self):
exkeyList = []
i = -1
srckeys = self.src_redis.keys()
for key in srckeys:
self.dst_pipe.exists(key)
for st in self.dst_pipe.execute():
i = i + 1
if st:
self.addkeyexist()
exkeyList.append(srckeys[i])
return exkeyList
|
3、使用方法
参数格式 ip:port[:db][:passwd],冒号分隔:
1
2
3
4
5
6
7
8
9
|
$ python migrate_redis.py
Usage:
python migrate_redis.py SRC DEST 同步源 Redis 所有 key 到目标 Redis
example:
1. python migrate_redis.py 192.168.1.1:4500 192.168.1.5:4500
2. python migrate_redis.py 192.168.1.1:4500:0 192.168.1.5:4500:1
3. python migrate_redis.py 192.168.1.1:4500 192.168.1.5:4500::passwd
4. python migrate_redis.py 192.168.1.1:4501:0:passwd 192.168.1.5:4500:1:passwd
|
同步实测:
1
2
3
4
5
6
7
8
9
10
11
|
$ python migrate_redis.py 127.0.0.1:4500 127.0.0.1:4600
************************************************************
redis src total keys: 77161 used memory: 7 Mb
redis dst total keys: 77161
value is nil: 0
key overdue : 0
key is exist: 0
Start at 2017-05-16 17:17:05 , End at 2017-05-16 17:17:12 , Usetime: 6.000 s
************************************************************
|
77161 个 key,6 秒同步完。
4、跨机房同步:iptables 端口转发
真实环境的问题:业务从一个机房迁另一个机房,redis 默认都只监听内网,跨机房网络不通。
解决办法:在源机器上做端口转发,把外网端口伪装成内网地址。环境模拟:
- 旧业务 redis-4500,外网 eth0:198.51.100.10,内网 eth1:192.168.5.10
- 新业务阿里云 redis-6379,内网:10.0.0.5,密码 redis123
在旧业务 redis 机器上加三条防火墙规则:
1
2
3
4
5
6
|
# 允许新业务机器访问本机 4500-4530 端口
iptables -A INPUT -s 203.0.113.10/32 -p tcp -m tcp --dport 4500:4530 -j ACCEPT
# 目标地址转换:外网 IP 的 4500-4530 包转给内网 redis
iptables -t nat -A PREROUTING -d 198.51.100.10/32 -p tcp -m tcp --dport 4500:4530 -j DNAT --to-destination 192.168.5.10
# 内网卡上做源伪装
iptables -t nat -A POSTROUTING -d 192.168.5.10/32 -p tcp -m tcp --dport 4500:4530 -o eth1 -j MASQUERADE
|
先 telnet 验证连通:
1
2
3
4
|
$ telnet 198.51.100.10 4500
Trying 198.51.100.10...
Connected to 198.51.100.10.
Escape character is '^]'.
|
通了之后就可以跨机房同步了:
1
|
python migrate_redis.py 198.51.100.10:4500 10.0.0.5:6379:0:redis123
|
总结
这个方案适合「目标 redis 不支持 slaveof、量又不至于上 rump 之类工具」的场景。要点三个:DUMP/RESTORE 保真搬数据、pipeline 控制批量大小(线上源别超 1000)、冲突阈值兜底防跑错环境。跨机房打不通网络就用 iptables DNAT + MASQUERADE 伪装内网。如果目标也是自建 redis 且网络可达,直接 slaveof 同步完 slaveof no one 更省事。
参考
- 脚本原作者:JaesonCheng(migrate_redis.py v0.3)