redis cluster 学习与梳理
参考:Redis Cluster 原理相关说明!!!
参考:Redis Cluster 集群一致性原理及slot迁移测试 !!!
参考:REDIS CLUSTER 搭建,扩容缩容基本原理!!!
参考:redis实战第十四篇 redis cluster ask重定向
参考:redis实战第十二篇 redis cluster请求重定向
参考:【RedisCluster】redis-cluster原理概述(虚拟槽+move/ask+cluster集群故障发现/恢复)
客户端路由
moved重定向
1.每个节点通过通信都会共享Redis Cluster中槽和集群中对应节点的关系
2.客户端向Redis Cluster的任意节点发送命令,接收命令的节点会根据CRC16规则进行hash运算与16383取余,计算自己的槽和对应节点
3.如果保存数据的槽被分配给当前节点,则去槽中执行命令,并把命令执行结果返回给客户端
4.如果保存数据的槽不在当前节点的管理范围内,则向客户端返回moved重定向异常
5.客户端接收到节点返回的结果,如果是moved异常,则从moved异常中获取目标节点的信息
6.客户端向目标节点发送命令,获取命令执行结果
在集群模式下,redis在接收到键任何命令时会先计算该键所在的槽,如果改键所在的槽位于当前节点,则直接执行命令,如果改键位于其它节点,则不执行该命令,返回重定向信息。 比如hello这个键槽866上,而曹866位于31节点上,假设在32上执行get hello ,则会返回重定向信息。
127.0.0.1:6380> get hello
(error) MOVED 866 192.168.0.31:6380
ask重定向
在对集群进行扩容和缩容时,需要对槽及槽中数据进行迁移
当客户端向某个节点发送命令,节点向客户端返回moved异常,告诉客户端数据对应的槽的节点信息
如果此时正在进行集群扩展或者缩空操作,当客户端向正确的节点发送命令时,槽及槽中数据已经被迁移到别的节点了,就会返回ask,这就是ask重定向机制
1.客户端向目标节点发送命令,目标节点中的槽已经迁移支别的节点上了,此时目标节点会返回ask转向给客户端
2.客户端向新的节点发送Asking命令给新的节点,然后再次向新节点发送命令
3.新节点执行命令,把命令执行结果返回给客户端
moved异常与ask异常的相同点和不同点:
两者都是客户端重定向
moved异常:槽已经确定迁移,即槽已经不在当前节点
ask异常:槽还在迁移中
通过cluster keyslot [key]
可以计算出key对用的槽。
通过./redis-cli -c -p [port]
可以以集群模式访问,它会自动的帮助我们捕获moved异常,自动跳转到新的节点并执行命令。
redis-cli 原始方式连接
[root@iz2zechwdfwvcm1rr3upjvz redis]# ./redis-cli -p 8000
127.0.0.1:8000> set zhangsan 100
# 返回moved异常,需要转到8002节点执行
(error) MOVED 12767 127.0.0.1:8002
# 退出
127.0.0.1:8000> exit
#手动连接8002节点执行set命令
[root@iz2zechwdfwvcm1rr3upjvz redis]# ./redis-cli -p 8002
127.0.0.1:8002> set zhangsan 100
OK
redis-cli 集群模式连接
[root@iz2zechwdfwvcm1rr3upjvz redis]# ./redis-cli -c -p 8000
#发送set命令
127.0.0.1:8000> set zhangsan 100
# 跳转到8002节点,并执行set命令
-> Redirected to slot [12767] located at 127.0.0.1:8002
OK
127.0.0.1:8002>
多节点命令实现
1.串行mget
定义for循环,遍历所有的key,分别去所有的Redis节点中获取值并进行汇总,简单,但是效率不高,需要n次网络时间
2.串行IO
对串行mget进行优化,在客户端本地做内聚,对每个key进行CRC16hash,然后与16383取余,就可以知道哪个key对应的是哪个槽
本地已经缓存了槽与节点的对应关系,然后对key按节点进行分组,成立子集,然后使用pipeline把命令发送到对应的node,需要nodes次网络时间,大大减少了网络时间开销
3.并行IO
并行IO是对串行IO的一个优化,把key分组之后,根据节点数量启动对应的线程数,根据多线程模式并行向node节点请求数据,只需要1次网络时间
4.hash_tag
将key进行hash_tag的包装,然后把tag用大括号括起来,保证所有的key只向一个node请求数据,这样执行类似mget命令只需要去一个节点获取数据即可,效率更高
Redis Cluster 集群一致性原理及slot迁移测试
集群信息一致性问题
主从和slot的一致性是由epoch来管理的. epoch就像Raft中的term, 但仅仅是像. 每个节点有一个自己独特的epoch和整个集群的epoch, 为简化下面都称为node epoch和cluster epoch. node epoch一直递增, 其表示某节点最后一次变成主节点或获取新slot所有权的逻辑时间. cluster epoch则是整个集群中最大的那个node epoch. 我们称递增node epoch为bump epoch, 它会用当前的cluster epoch加一来更新自己的node epoch.
在使用gossip协议中, 如果多个节点声称不同的集群信息, 那对于某个节点来说究竟要相信谁呢? Redis Cluster规定了每个主节点的epoch都不可以相同. 而一个节点只会去相信拥有更大node epoch的节点声称的信息, 因为更大的epoch代表更新的集群信息.
原则上:
(1)如果epoch不变, 集群就不应该有变更(包括选举和迁移槽位)
(2)每个节点的node epoch都是独一无二的
(3)拥有越高epoch的节点, 集群信息越新
集群所有槽的指派信息
通过将所有槽的指派信息保存在clusterState.slots数组里面,程序要检查槽i是否已经被指派,又或者取得负责处理槽i的节点,只需要访问clusterState.slots[i]的值即可,复杂度仅为O(1)。 (PS:这里有个问题,clusterState是前面的那个结构吗,另外这是个二进制数组,怎么处理槽i的节点?)
请求重定向
由于每个节点只负责部分slot,以及slot可能从一个节点迁移到另一节点,造成客户端有可能会向错误的节点发起请求。因此需要有一种机制来对其进行发现和修正,这就是请求重定向。有两种不同的重定向场景:
a) MOVED错误
-
请求的key对应的槽不在该节点上,节点将查看自身内部所保存的哈希槽到节点 ID 的映射记录,节点回复一个 MOVED 错误。
-
需要客户端进行再次重试。
b) ASK错误
-
请求的key对应的槽目前的状态属于MIGRATING状态,并且当前节点找不到这个key了,节点回复ASK错误。ASK会把对应槽的IMPORTING节点返回给你,告诉你去IMPORTING的节点查找。
-
客户端进行重试 首先发送ASKING命令,节点将为客户端设置一个一次性的标志(flag),使得客户端可以执行一次针对 IMPORTING 状态的槽的命令请求,然后再发送真正的命令请求。
-
不必更新客户端所记录的槽至节点的映射。
四、数据迁移
当槽x从Node A向Node B迁移时,Node A和Node B都会有这个槽x,Node A上槽x的状态设置为MIGRATING,Node B上槽x的状态被设置为IMPORTING。
MIGRATING状态
-
如果key存在则成功处理
-
如果key不存在,则返回客户端ASK,客户端根据ASK首先发送ASKING命令到目标节点,然后发送请求的命令到目标节点
-
当key包含多个:
-
如果都存在则成功处理
-
如果都不存在,则返回客户端ASK
-
如果一部分存在,则返回客户端TRYAGAIN,通知客户端稍后重试,这样当所有的key都迁移完毕的时候客户端重试请求的时候回得到ASK,然后经过一次重定向就可以获取这批键
-
此时不刷新客户端中node的映射关系
IMPORTING状态
-
如果key不在该节点上,会被MOVED重定向,刷新客户端中node的映射关系
-
如果是ASKING则命令会被执行,key不在迁移的节点已经被迁移到目标的节点
-
Key不存在则新建
Key迁移的命令:
1 DUMP:在源(migrate)上执行
2 RESTORE:在目标(importing)上执行
3 DEL:在源(migrate)上执行
经过上面三步可以将键迁移,然后再将处于MIGRATING和IMPORTING状态的槽变为常态,完成整个重新分片的过程,具体的信息可见Redis Cluster部署、管理和测试 。
4.1 读写请求
槽里面的key还未迁移,并且槽属于迁移中。
假如槽x在Node A,需要迁移到Node B上,槽x的状态为migrating,其中的key1还没轮到迁移。此时访问key1则先计算key1所在的Slot,存在key1则直接返回。
4.2 MOVED请求
槽里面的key已经迁移过去,并且槽属于迁移完。
假如槽x在Node A,需要迁移到Node B上,迁移完成。此时访问key1则先计算key1所在的Slot,因为已经迁移至Node B上,Node A上不存在,则返回 moved slotid IP:PORT,再根据返回的信息去Node B访问key1,此时更新slot和node的映射。
4.3 ASK请求
槽里面的key已经迁移完,并且槽属于迁移中的状态。
假如槽x在Node A,需要迁移到Node B上,迁移完成,但槽x的状态为migrating。此时访问key1则先计算key1所在的Slot,不存在key1则返回ask slotid IP:PORT,再根据ask返回的信息发送asking请求到Node B,没问题后则最后再去Node B上访问key1,此时不更新slot和node的映射。
- meet: 某个节点发送meet给新加入的节点,让新节点加入集群中,然后新节点就会开始与其他节点进行通信
- redis-trib.rb add-node 其实内部就是发送了一个gossip meet消息,给新加入的节点,通知那个节点去加入我们的集群
- ping: 每个节点都会频繁给其他节点发送ping,其中包含自己的状态还有自己维护的集群元数据,互相通过ping交换元数据
- 每个节点每秒都会频繁发送ping给其他的集群,ping,频繁的互相之间交换数据,互相进行元数据的更新
- pong: 返回ping和meet,包含自己的状态和其他信息,也可以用于信息广播和更新
- fail: 某个节点判断另一个节点fail之后,就发送fail给其他节点,通知其他节点,指定的节点宕机了
参考:cluster setslot指令
cluster setslot
CLUSTER SETSLOT slot IMPORTING|MIGRATING|STABLE|NODE [node-id]
自3.0.0起可用。
时间复杂度: O(1)
CLUSTER SETSLOT 负责以不同方式更改接收节点中散列槽的状态。它可以取决于所使用的子命令:
1. MIGRATING子命令:设置迁移状态下的散列槽。
2. IMPORTING子命令:在导入状态下设置哈希槽。
3. STABLE 子命令:清除散列槽中的任何导入/迁移状态。
4. NODE 子命令:将哈希槽绑定到不同的节点。
该命令及其子命令集对于启动和结束集群实时重新分片操作很有用,它通过在源节点中设置迁移状态的哈希槽以及在目标节点中导入状态来完成。
下面介绍了每个子命令。最后你会找到如何使用这个命令和其他相关命令执行实时重新分片的描述。
REDIS CLUSTER 搭建,扩容缩容基本原理
## step 3 扩容
## 扩容,给新的一个master 节点分配槽
## 多个来源节点的情况
## 遍历每个节点,计算每个节点应该移出多少个槽 (numslots.to_f/source_tot_slots*s.slots.length)
## numslots.to_f 总共要移动的槽数
## source_tot_slots 所有来源节点的槽数
## s.slots.length 单个来源节点拥有的槽数
## 循环每个slot:
## 接收节点执行 cluster setslot <slot> importing <node_id> (node_id为源节点id)
## 来源节点执行 cluster setslot <slot> migrating <node_id> (node_id为接收节点id)
## 源节点执行 cluster countkeysinslot <slot> 查看要移动的槽 有没有键
## 如果有 则 继续执行 cluster getkeysinslot <slot> <count> 查看slot节点的多少个key值, 如果上一步槽的键数量为空跳过此步骤
## 然后执行 MIGRATE 源节点ip 源节点port "" 0 5000 KEYS key1 key2 key3 把键先迁移,执行这条命令后该键不可读不可写
## 分别在源节点和接收节点执行 cluster setslot <slot> node <node_id>
## 现在看 主要在接收节点上执行 cluster setslot ...node ...命令,只有在接收节点上面执行后会跟新接收节点的epoch 版本号,然后发消息跟其他节点说该slot已经归我管了,如果epoch比接收信息的节点高,接收信息的节点会跟新该slot的状态
## 而源节点上面执行cluster setslot .... node ...命令只移除mirating flag 并不会更新版本号
## 那么问题来了,那importing 和migrating 步骤不就可以省略吗,那源节点也没必要执行cluster setslot ... node ...命令了(其实并不是这样,详见测试)