redis-集群理论篇
前言
哨兵解决和主从不能自动故障恢复的问题,但是同时也存在难以扩容以及单机存储、读写能力受限的问题,并且集群之前都是一台redis
都是全量的数据,这样所有的redis
都冗余一份,就会大大消耗内存空间。
redis
集群是一个由多个主从节点群组成的分布式服务器群,它具有复制、高可用和分片特性。不需要**sentinel
哨兵也能完成节点移除和故障转移的功能。需要将每个节点设置成集群模式,这种集群模式没有中心节点,可水平扩展,据官方文档称可以线性扩展到上万个节点(官方推荐不超过1000个节点)。redis集群的性能和高可用性**均优于之前版本的哨兵模式。集群模式实现了Redis
数据的分布式存储,**实现数据的分片**
,**每个redis节点存储不同的内容**
,并且解决了在线的节点收缩和扩容问题。
本文对集群的节点,槽(slot),重新分片,转向,故障转移做介绍。
节点
节点的扩缩机制
redis
集群有节点组成,刚开始每个节点都是独立的,要组建一个可用的集群,需要把各个独立的节点连接起来。
加入节点:
-
CLUSTER MEET [ip1 port1]
-
redis-cli --cluster add-node [新节点ip:port] [旧主节点ip:port] --cluster-slave --cluster-master-id [id]
- 加入一个节点
- –cluster-slave,新节点挂在旧节点下的一个从节点(不加这个,加入一个主节点)
- cluster-master-id ,arg设置旧节点的id(不加这个,加入一个主节点)
-
移除节点
- cluster forget
- redis-cli del-node [ip:port] [node-id]
-
重新设置主节点,只能针对slave节点操作
- cluster replicate <master_node_id>
ps:指令实践参考:redis-基于docker搭建redis集群。
节点通信
节点之间实现了将数据进行分片存储,那么节点之间又是怎么通信的呢?集群要作为一个整体工作,离不开节点之间的通信。在哨兵系统中,节点分为数据节点和哨兵节点:前者存储数据,后者实现额外的控制功能。在集群中,没有数据节点与非数据节点之分:所有的节点都存储数据,也都参与集群状态的维护。
集群中的每个节点,都提供了两个 TCP 端口:
- 普通端口:即我们在前面指定的端口。普通端口主要用于为客户端提供服务;但在节点间数据迁移时也会使用。
- 集群端口:端口号是
普通端口+10000
,如 7000 节点的集群端口为 17000。集群端口只用于节点之间的通信
,如搭建集群、增减节点、故障转移等操作时节点间的通信;不要使用客户端连接集群接口。为了保证集群可以正常工作,在配置防火墙时,要同时开启普通端口和集群端口。
维护集群的元数据(集群节点信息,主从角色,节点数量,各节点共享的数据等)有两种方式:**集中 式和gossip**
。redis cluster
节点间采取gossip
协议进行通信
集中式:
优点在于元数据的更新和读取,时效性非常好,一旦元数据出现变更立即就会更新到集中式的存储中,其他节点读取的时候立即就可以立即感知到;不足在于所有的元数据的更新压力全部集中在一个地方,可能导致元数据的存储压力。 如:zookeeper集中式存储元数据。
gossip:在节点数量有限的网络中,每个节点都“随机”的与部分节点通信,经过一番杂乱无章的通信,每个节点的状态很快会达到一致。优点在于元数据的更新比较分散,不是集中在一个地方,更新请求会陆陆续续打到所有节点上去更新,有一定的延时,降低了压力;缺点在于元数据同步较慢。
节点间发送的消息主要分为 5 种:
- MEET 消息:某个节点发送
meet
给新加入的节点,让新节点加入集群中,然后新节点就会开始与其他节点进行通信; - PING 消息:每个节点都会频繁给其他节点发送
ping
,其中包含自己的状态还有自己维护的集群元数据,互相通过ping
交换元数据(类似自己感知到的集群节点增加和移除,hash slot信息等); - PONG 消息:对
ping
和meet
消息的返回,包含自己的状态和其他信息,也可以用于信息广播和更新; - FAIL 消息 :某个节点判断另一个节点
fail
之后,就发送fail
给其他节点,通知其他节点,指定的节点宕机了; - PUBLISH 消息:节点收到
PUBLISH
命令后,会先执行该命令,然后向集群广播这一消息,接收节点也会执行该PUBLISH
命令。
所以加入一个新节点时:
老成员
收到Meet
消息后,在没有故障的情况下会回复PONG
消息,表示欢迎新结点的加入,除了第一次发送Meet
消息后,之后都会发送定期PING
消息,实现节点之间的通信。
槽分配
数据分片
Redis 集群使用数据分片而非一致性哈希来实现: 一个Redis
集群包含 16384
个哈希槽(hash slot)
, 数据库中的每个键都属于这 16384 个哈希槽的其中一个, 集群使用公式 CRC16(key) % 16384
来计算键 key
属于哪个槽, 其中 CRC16(key) 语句用于计算键 key 的 CRC16 校验和 。
通过cluster nodes
指令查看每个节点的槽信息。
Redis Cluster
的哈希槽算法,CRC16(caicai)%16384 = 3967
这个key
就被分配到了节点6371
上 。如下图:
节点-槽-数据的关系
之所以进行分槽存储,是将一整堆的数据进行分片,防止单台的redis数据量过大,影响性能的问题。
注意:
- 加入一个节点,刚开始不会有任何的槽位,需要进行分配。
- 移除节点 A, 那么集群只需要将节点 中的所有哈希槽移动到其他节点 , 然后再移除空白的节点 就可以了。 应先下线从节点再下线主节点,因为若主节点先下线,从节点会被指向其他主节点,造成不必要的全量复制。
槽相关指令
- cluster addslots [slot …] :将一个或多个槽指派给当前节点。
- cluster delslots [slot …] :移除一个或多个槽对当前节点的指派。
- cluster flushslots :让当前节点变成一个没有指派任何槽的节点。
- cluster setslot node <node_id> :将槽 slot 指派给 node_id 指定的节点,如果槽已经指派给
另一个节点,那么先让另一个节点删除该槽,然后再进行指派。 - cluster setslot migrating <node_id> :将本节点的槽 slot 迁移到 node_id 指定的节点中。
- cluster setslot importing <node_id> :从 node_id 指定的节点中导入槽 slot 到本节点。
- 从源节点处获取属于槽slot的键值对。redis-trip向源节点发送命令
- cluster getkeysinslot //count为最多获取count个键值对
- cluster setslot stable :取消对槽 slot 的导入( import)或者迁移( migrate)。
重定义跳转
当客户端向一个错误的节点发出了指令,该节点会发现指令的key
所在的槽位并不归自己管理,这时它会向客户端发送一个特殊的跳转指令携带目标操作的节点地址,告诉客户端去连这个节点去获取数据。客户端收到指令后除了跳转到正确的节点上去操作,还会同步更新纠正本地的槽位映射表缓存,后续所有key
将使用新的槽位映射表。
重新分片
将任意数量已经指派给某个节点的槽,改为指派给另一个节点,槽对应的所有键值对都需要迁移。
特点:
- 1、重新分片是可以在线进行的,集群无需下线
- 2、 重新分片过程中,源节点和目标节点都可以继续处理命令请求
操作流程:重新分片操作由Redis
的集群管理软件 redis-trib
负责执行的 。
- 1、通知目标节点准备导入属于槽
slot
的键值对。redis-trip向目标节点发送命令(导入:import)
会修改目标节点的clusterState.importing_slots_from
数组。
cluster setslot importing <source_id> //slot为槽的编号,source_id为源节点id
- 2、通知源节点准备迁移属于槽slot的键值对。
redis-trip
向源节点发送命令(迁移:migrate)
会修改源节点的clusterState.migrating_slots_to
数组;
cluster setslot migrating <target_id> //target_id为目标节点id
- 3、从源节点处获取属于槽
slot
的键值对。redis-trip
向源节点发送命令;
cluster getkeysinslot //count为最多获取count个键值对
- 4、将获得的键值对从源节点迁移到目标节点。对于获得的每一个键值对,
redis-trip
都向源节点发送一个migrate
命令;
migrate <target_id> <target_port> <key_name> 0 //目标节点id、端口、键名、超时时间设置
-
5、重复
3、4
两步操作,直到属于槽slot
的所有键值对都成功从源节点迁移至目标节点; -
6、全部迁移成功后,将槽
slot
指派给目标节点。redis-trip
向集群中任意一个节点发送命令,将slot
指派给目标节点。
cluster setslot NODE <target_id> //正式指派槽slot给界目标id的节点
- 然后这个信息会通过消息发送到整个集群,最终所有节点都会知道这个消息,指派成功后,通过消息发送,通知整个集群中的节点,然后节点们根据消息,对节点内的clusterState结构和clusterNode结构进行更新。
命令执行流程(是否处理该槽,是否存在键,是否正在迁移,是否是ASK命令转向过来的等知识点)
总结:先通知目标节点准备导入键值对,再通知源节点准备迁移键值对,然后开始键值对的迁移,键值对迁移成功后通过命令将槽指派给目标节点。键值对的迁移分为:从源节点获取键值对,使用migrate
命令将键值对从源节点迁移到目标节点,重复操作,直到键值对迁移完毕。
**需注意的是:如果经判断发现槽中没有保存键值对,则两步准备后直接将槽指派给目标节点即可。**
ASK错误
在进行重新切片的时候,源节点向目标节点迁移一个槽的过程中,可能出现这样的情况,属于被迁移的槽一部分键保存在源节点,另一部分在目标节点。
- 客户端向源节点发送指令;
- 源节点会先从自己的 数据库里面找,找到执行相应的命令;
- 没有找到,那么这个键可能已被迁移到了目标节点上,源节点向客户端返回一个ASK错误,引导客户端去正确的节点执行指令。
ASK和MOVED区别
ASK 错误说明数据正在迁移,不知道何时迁移完成,因此重定向是临时的,SMART 客户端不会刷新 slots 缓存;MOVED 错误重定向则是(相对)永久的,SMART 客户端会刷新 slots 缓存。
故障转移
主节点负责处理槽,从节点用于复制某个主节点;当主节点下线时,在其所有从节点中选取一个用作主节点(类似备份?)(由Sentinel系统监控)
设置从节点
cluster replicate <node_id> //接收命令的节点成为从节点,node_id为其主节点的id
设置从节点后,从节点对应的clusterNode
的slaveof
指针、flags
属性做出相应修改;主节点的slaves
指针数组、numsslaves
属性也更新,集群中的节点之间通过互相发送消息的方式来交换集群中各个节点的状态信息。
故障检测
通过定时任务发送 PING
消息检测其他节点状态;节点下线分为主观下线和客观下线;客观下线后选取从节点进行故障转移。与哨兵一样,集群只实现了主节点的故障转移;从节点故障时只会被下线,不会进行故障转移。
故障转移
- 从复制该主节点的所有从节点里选出一个从节点;
- 该从节点执行
Slaveof no one
命令,成为新主节点; - 新主节点撤销所有对已下线主节点的槽指派,将这些槽指派给自己;
- 新主节点向集群广播一条
PONG
消息,通知其他节点自己已成为新节点, - 新主节点开始接受和处理相关命令请求。
节点数量:在故障转移阶段,需要由主节点投票选出哪个从节点成为新的主节点;从节点选举胜出需要的票数为 N/2+1
;其中 N
为主节点数量(包括故障主节点),但故障主节点实际上不能投票。因此为了能够在故障发生时顺利选出从节点,集群中至少需要 3
个主节点(且部署在不同的物理机上)。
故障转移时间:从主节点故障发生到完成转移,所需要的时间主要消耗在主观下线识别、主观下线传播、选举延迟等几个环节。
如何选取新主节点
选举产生:基于Raft算法。(Sentinel系统的领头Sentinel选举也采用这种方式)当slave
发现自己的master
变为FAIL
状态时,便尝试进行Failover
,以期成为新的master
。由于挂掉的master
可能会有多个slave
,从而存在多个slave
竞争成为master
节点的过程, 其过程如下:
- 1.
slave
发现自己的master
变为FAIL
; - 2.将自己记录的集群
currentEpoch
加1
,并广播FAILOVER_AUTH_REQUEST
信息; - 3.其他节点收到该信息,只有
master
响应,判断请求者的合法性,并发送FAILOVER_AUTH_ACK
,对每一个epoch
只发送一次ack
; - 4.尝试
failover
的slave
收集master
返回的FAILOVER_AUTH_ACK
; - 5.
slave
收到超过半数master
的ack
后变成新Master
(这里解释了集群为什么至少需要三个主节点,如果只有两个,当其中一个挂了,只剩一个主节点是不能选举成功的); - 6.
slave
广播Pong
消息通知其他集群节点;
从节点并不是在主节点一进入 FAIL
状态就马上尝试发起选举,而是有一定延迟,一定的延迟确保我们等待FAIL
状态在集群中传播,slave
如果立即尝试选举,其它masters
或许尚未意识到FAIL
状态,可能会拒绝投票;
集群脑裂数据丢失问题
redis
集群没有过半机制会有脑裂问题,网络分区导致脑裂后多个主节点
对外提供写服务,一旦网络分区恢复,会将其中一个主节点
变为从节点
,这时会有大量数据丢失。
规避方法可以在redis
配置里加上参数(这种方法不可能百分百避免数据丢失)
min‐replicas‐to‐write 1 //写数据成功最少同步的slave数量,这个数量可以模仿大于半数机制配置,比如
集群总共三个节点可以配置1,加上leader就是2,超过了半数
注意:这个配置在一定程度上会影响集群的可用性,比如slave要是少于1个,这个集群就算leader正常也不能提供服务了,需要具体场景权衡选择。
Redis集群对批量操作命令的支持
对于类似mset,mget
这样的多个key
的原生批量操作命令,redis
集群只支持所有key
落在同一slot
的情况,如果有多个key
一定要用mset
命令在redis
集群上操作,则可以在key
的前面加上{XX}
,这样参数数据分片hash
计算的只会是大括号里的值,这样能确保不同的key
能落到同一slot
里去,示例如下:
mset {user1}:1:name caicai{user1}:1:age 18
这条命令在集群下执行,redis
只会用大括号里的 user1
做hash slot
计算,所以算出来的slot
值肯定相同,最后都能落在同一slot
。
集群的限制及应对方法
key
批量操作受限:例如mget、mset
操作,只有当操作的 key 都位于一个槽时,才能进行keys/flushall
等操作:keys/flushall
等操作可以在任一节点执行,但是结果只针对当前节点。- ·事务/Lua 脚本·:集群支持事务及
Lua
脚本,但前提条件是所涉及的key 必须在同一个·节点
。 - 数据库:单机
Redis
节点可以支持16
个数据库,集群模式下只支持一个,即db0
。 - 复制结构:只支持一层复制结构,不支持嵌套。
参数优化
集群是否完整才能对外提供服务
- 当
redis.conf
的配置cluster-require-full-coverage
为no
时,表示当负责一个插槽的主库下线且没有相应的从
库进行故障恢复时,集群仍然可用,如果为yes
则集群不可用。
网络抖动
clusternodetimeout
,表示当某个节点持续 timeout
的时间失联时,才可以认定该节点出现故障,需要进行主从切换。如果没有这个选项,网络抖动会导致主从频繁切换 (数据的重新复制)。