高可用-Cluster
redis
的哨兵模式基本已经可以实现高可用,读写分离 ,但是在这种模式下每台 redis
服务器都存储相同的数据,很浪费内存,并且总容量有限,所以在 redis3.0
上加入了 cluster
模式,实现的 redis
的分布式存储,也就是说每台 redis
节点上存储不同的内容。
redis-cluster
采用无中心结构,它的特点如下:
多个主节点连成一个集群,每个主节点下可挂载从节点
所有节点彼此互联 (
PING-PONG
机制),内部使用二进制协议(gossip 协议
)优化传输速度和带宽。高可用:主节点故障时,从节点提升为主节点。
主观下线:主节点
PING
另一个主节点时一定时间内未收到PONG
客观下线:半数以上主节点判定某个主节点处于下线状态
从节点升主选举:半数以上主节点投票给某个从节点
客户端与
redis
节点直连,不需要中间代理层。客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可。任一节点都保存了所有节点的信息。
工作方式
在 redis
的每一个节点上,都有这么两个东西,一个是插槽 slot
,它的的取值范围是:0-16383
。还有一个就是 cluster
,可以理解为是一个集群管理的插件
当我们的存取的 key
到达的时候,redis
会根据 crc16
的算法得出一个结果,然后把结果对 16384
取余,这样每个 key
都会对应一个编号在 0-16383
之间的哈希槽,通过这个值,去找到对应的插槽所对应的节点,然后跳转到这个对应的节点上进行存取操作。
为了保证高可用,redis-cluster
集群引入了主从模式,一个主节点对应一或多个从节点。主节点宕机的时候,就会启用从节点。当其它主节点 ping
一个主节点 A
时,如果半数以上的主节点与 A
通信超时,那么认为主节点A
宕机了。如果主节点 A
和它的从节点都宕机了,那么该节点负责的槽就无法再提供服务了,集群可用性无法得到保障。
槽指派
redis
集群通过分片的方式来保存数据库中的键值对:集群的整个数据库被分为 16384
个槽 slot
,数据库中的每个键都属于这 16384
个槽的其中一个,集群中的每个节点可以处理 0 ~ 16384
个槽。
当数据库中的 16384
个槽都有节点在处理时,集群处于上线状态(ok
);相反地,如果数据库中有任何一个槽没有得到处理,那么集群处于下线状态(fail
)。
clusterNode
clusterNode
结构的 slots
属性和 numslot
属性记录了节点负责处理哪些槽:
slots
属性是一个数组类型的 bitmap
,这个数组的长度为 16384/8=2048
,共包含 16384
个二进制位。redis
以 0
为起始索引,16383
为终止索引,对 slots
数组中的 16384
个二进制位进行编号,并根据索引 i
上的二进制位的值来判断节点是否负责处理槽 i
:
如果
slots
数组在索引i
上的二进制位的值为1
,那么表示节点负责处理槽i
。如果
slots
数组在索引i
上的二进制位的值为0
,那么表示节点不负责处理槽i
。
numslots
属性则记录节点负责处理的槽的数量,即 slots
数组中值为 1
的二进制位的数量。
可以通过
cluster nodes
命令查看集群所有节点负责的槽区间
clusterState
一个节点除了会将自己负责处理的槽记录在 clusterNode
结构的 slots
属性和 numslots
属性之外,它还会将自己的 slots
数组通过消息发送给集群中的其他节点,以此来告知其他节点自己目前负责处理哪些槽。
clusterState
结构中的 slots
数组记录了集群中所有 16384
个槽的指派信息:
slots
数组包含 16384
个项,每个数组项都是一个指向 clusterNode
结构的指针:
如果
slots[i]
指针指向NULL
,那么表示槽i
尚未指派给任何节点。如果
slots[i]
指针指向一个clusterNode
结构,那么表示槽i
指派给了clusterNode
结构所代表的节点。clusterNode
,和clusterState
是不同的,前者是01
数组,后者是对象数组。前者用来记录一个节点负责哪些slot
,后者记录整个集群的所有节点。
命令执行流程
如果键所在的槽正好就指派给了当前节点,那么节点直接执行这个命令。
如果键所在的槽并没有指派给当前节点,那么节点会查找该槽所属的节点,向客户端返回一个
MOVED
错误,指引客户端转向至正确的节点,客户端向正确的节点重试。你也可以自己在客户端实现节点缓存,记录
key
和slot
的关系,可以更高效
槽指派
节点使用以下算法来计算给定键 key
属于哪个槽:
重分片
redis
集群可以通过 add-note
命令进行水平扩容。扩容后需要通过 reshard
命令重新分片,将一部分槽改指派给扩容后的新节点。
redis
首先会在源和目标节点设置好中间过渡状态(migrating
状态);然后一次性获取源节点槽位的所有
key
列表 (keysinslot
指令,可以部分获取),再挨个key
进行迁移:源节点对当前的
key
执行dump
指令得到序列化内容;源节点向目标节点发送指令
restore
携带序列化的内容作为参数;目标节点进行反序列化将内容恢复到目标节点的内存中,然后返回源节点
OK
;源节点收到后再把
key
删除;执行期间主线程阻塞,如果
key
的内容过大会导致源节点和目标节点卡顿,影响集群稳定性,所以需要尽可能避免大key
产生。
ASK错误
迁移期间,源节点和目标节点处于 migrating
状态,此时客户端查询源节点的流程如下:
源节点会先在自己的数据库里面查找指定的键,如果找到的话,就直接执行客户端发送的命令。
如果源节点没能在自己的数据库里面找到指定的键,那么这个键可能被迁移到了目标节点,源节点将向客户端返回一个
ASK
错误,指引客户端转向目标节点,并再次发送之前想要执行的命令。
复制与故障转移
复制
redis
集群中的节点分为主节点 master
和从节点 slave
,其中主节点用于处理槽,而从节点则用于复制某个主节点,并在被复制的主节点下线时,代替下线主节点继续处理命令请求。(客户端也可以做读写分离,将读请求打到从节点上)
故障检测
集群中的每个主节点都会定期地向集群中的其他节点发送 PING
消息,以此来检测对方是否在线,如果接收 PING
消息的节点没有在规定的时间内返回 PONG
消息,那么发送 PING
消息的节点就会将接收 PING
消息的节点标记为疑似下线,又称主观下线(probable fail,PFAIL
),并广播给其他节点。
当一个主节点 A
通过消息得知主节点 B
认为主节点 C
进入了主观下线状态时,主节点 A
会在自己的 clusterState.nodes
字典中找到主节点 C
所对应的clusterNode
结构,并将主节点 B
的下线报告(failure report
)添加到 clusterNode
结构的 fail_reports
链表里面。
如果在一个集群里面,半数以上的主节点都将某个主节点 X
报告为主观下线,那么这个主节点 X
将被标记为客观下线(FAIL
),将主节点 X
标记为已下线的节点会向集群广播一条关于主节点 X
的 FAIL
消息,所有收到消息的节点都会立即将主节点 X
标记为客观下线。
故障转移
当一个从节点发现自己正在复制的主节点进入了已下线状态时,从节点将开始对下线主节点进行故障转移,以下是故障转移的执行步骤:
复制下线主节点的所有从节点里面,会有一个从节点被选中。
被选中的从节点会执行
SLAVEOF no one
命令,成为新的主节点。新的主节点会撤销所有对已下线主节点的槽指派,并将这些槽全部指派给自己。
新的主节点向集群广播一条
PONG
消息,这条PONG
消息可以让集群中的其他节点立即知道这个节点已经由从节点变成了主节点,并且这个主节点已经接管了原本由已下线节点负责处理的槽。新的主节点开始接收和自己负责处理的槽有关的命令请求,故障转移完成。
选举新的主节点 Leader Election
redis
的主节点选举采用了 raft
协议类似的方式,基本思路是选举数据最接近原主节点的从节点。以下是集群选举新的主节点的方法:
集群的配置纪元是一个自增计数器,它的初始值为
0
,每次选举+=1
(配置纪元 = 触发选举的轮数)对于每轮选举,集群里每个主节点都有一次投票的机会,而第一个向主节点要求投票的从节点将获得主节点的投票。
当从节点发现自己正在复制的主节点进入客观下线状态时,会发起一轮投票。
从节点拉票步骤:
选举资格检查:检查自己是否已经与之前的
master
节点太久未进行通信。正常来讲,master
会定期向slave
发送ping
消息,如果长时间未通信,那么该slave
节点极有可能在数据上落后master
过多,于是放弃发起选举,否则就进入第二步。随机休眠:每个参与选举的
slave
会进行一段时间的休眠,休眠的时间与自身的数据同步进度有关。数据越新 (repl_offset
更大) ,休眠时间越短,越早醒来,优先级越高,越有可能被选举。发起拉票:从节点会向集群广播一条消息,要求所有收到这条消息、并且具有投票权的主节点向给自己投票。
主节点投票步骤:
投票资格检查:尚未投票的主节点才有资格投票
拉票资格检查:检查拉票方的轮数
term
,比自己的大才行如果主节点尚未投票,那么主节点将向要求投票的从节点返回一条消息,表示这个主节点支持从节点成为新的主节点。
如果一个从节点收集到半数以上主节点
N/2+1
的支持票时,这个从节点就会当选为新的主节点。如果在一轮选举里没有从节点能收集到足够多的支持票,那么集群进入下一轮选举,直到选出新的主节点为止。
注意这一点和
sentinel
是不一样的,sentinel
是先选一个leader sentinel
,再由这个leader
选同步进度最新的作为主。
节点间的内部通信机制
在 redis cluster
架构下,每个 redis
要放开两个端口号,比如一个是 6379
,另外一个就是加 10000
的端口号,比如 16379
。
16379
端口号是用来进行节点间通信的,也就是 cluster bus
的通信,用来进行故障检测、配置更新、故障转移授权。cluster bus
用了另外一种二进制的协议,gossip
协议,用于节点间进行高效的数据交换,占用更少的网络带宽和处理时间。
集群元数据的维护有两种方式:集中式、gossip
协议。
集中式:时效性好;有压力 / 单点问题。
gossip
:元数据的更新比较分散,降低压力;有延时、滞后。
gossip 协议
gossip
协议包含多种消息,包含 ping, pong, meet, fail
等等。
meet
:某个节点发送meet
给新加入的节点,让新节点加入集群中,然后新节点就会开始与其它节点进行通信。redis-trib.rb add-node
命令其实内部就是发送了一个gossip meet
消息给新加入的节点,通知那个节点去加入集群。新节点加入时也有三次握手,分别是:
A: meet -> B、B: pong -> A、A: ping -> B
ping
:每个节点都会频繁给其它节点发送ping
,其中包含自己的状态还有自己维护的集群元数据,互相通过ping
交换元数据。pong
:返回ping
和meet
,包含自己的状态和其它信息,也用于信息广播和更新。fail
:某个节点判断另一个节点fail
之后,就发送fail
给其它节点,通知其它节点说,某个节点宕机了。
ping 消息深入
ping
时要携带一些元数据,如果很频繁,可能会加重网络负担。每个节点每秒会执行
10
次ping
,每次会选择5
个最久没有通信的其它节点。当然如果发现某个节点通信延时达到了cluster_node_timeout / 2
,那么立即发送ping
,避免数据交换延时过长,落后的时间太长了。比如说,两个节点之间都10
分钟没有交换数据了,那么整个集群处于严重的元数据不一致的情况,就会有问题。所以cluster_node_timeout
可以调节,如果调得比较大,那么会降低ping
的频率。每次
ping
,会带上自己节点的信息,还有就是带上1/10
其它节点的信息,发送出去,进行交换。至少包含3
个其它节点的信息,最多包含 总节点数减2
个其它节点的信息。
参考
https://zhuanlan.zhihu.com/p/112651338
Last updated