Redis哨兵(Sentinel)和集群(Cluster)正是为了解决主从复制架构下高可用性和大规模数据存储这两大核心痛点而设计的
1.主从遗留问题
Redis的主从复制模式可以将主节点的数据同步到从节点,这样从节点可以起到两个作用:
- 作为主节点的一个备份,一旦主节点出了故障不可达的情况,从节点可以作为后备顶上去,并且保证数据不丢失
- 从节点可以分担主节点上的读压力,让主节点只承担写请求处理,将所有的读请求负载均衡到各个从节点上
但是主从复制遗留有以下问题:
- 主节点发生故障时,进行主从切换的过程是复杂的,需要完全的人工参与,导致故障恢复时间无法保证
- 主节点可以将读压力分散出去,但写压力/存储压力是无法被分担的,还是收到单机的限制
其中第一个问题是高可用问题,即Redis哨兵 主要解决的问题。第二个问题属于存储分布式的问题,是Redis集群解决的
2. 哨兵(Sentinel)
2.1 基本概念

Redis Sentinel 是Redis的高可用实现方案,在实际的生产环境,对提高整个系统的高可用是非常有帮助的。
人工恢复主节点故障
Redis主从复制模式下,一旦主节点由于故障不能提供服务,需要人工进行主从切换,同时大量的客户端需要被通知切换到新的主节点上,对于有一定规模的应用来说,这种方案是无法接受的
Redis 主从复制模式下,主节点故障后需要进行的人工工作作是比较繁琐的

1)通过监控系统发现Redis主节点故障宕机
2)选择一个从节点作为新主节点
3)让剩余从节点从新主节点开始数据同步
4)更新应用方连接的主节点信息
5)如果原来的主节点恢复,使其称为一个从节点
上述过程需要人工介入,无法被认为架构是高可用的。
哨兵自动回复主节点故障
当主节点出现故障,Redis Sentinel 能自动完成故障发现和故障转移,并通知应用方,从而实现真正的高可用,Redis Sentinel 是一个分布式架构,其中包含若干个(建议奇数)Sentinel 节点和 Redis 数据节点
整个过程是完全自动的,不需要人工介入

哨兵节点会定期监控所有节点(包含数据节点和其他哨兵节点)
1)主节点故障,从节点同步连接中断,主从复制停止
2)哨兵节点通过定期监控发现主节点出现故障,哨兵节点与其他哨兵节点进行协商,达成多数认同主节点故障的共识。这步主要是防止该情况:出故障的不是主节点,而是发现故障的哨兵节点,该情况进程发生于哨兵节点的网络被孤立的场景下
3)哨兵节点选举出一个领导角色,由该节点负责后续的故障转移工作
4)哨兵领导者开始执行故障转移:从节点选择一个作为新节点,让其他从节点同步新节点,通知应用层转移到新节点

Redis Sentinel 具有以下功能:
- 监控 :Sentinel 节点会近期检测Redis数据节点、其余哨兵是否可达
- 故障转移:实现从节点晋升为主节点并维护后续正确的主从关系
- 通知:Sentinel 节点会将故障转移的结果通知给应用方
2.2 安装部署(基于docker)
准备工作
1)安装 docker 和 docker-compose
2)停止之前的Redis服务器

3)使用 docker 获取Redis镜像

编排redis主从节点
1)编写docker-compose。yml


2)启动所有容器
docker-compose up -d

3)查看日志
docker compose logs
4)验证
主节点

从节点

1)编写docker-compose。yml


2)创建配置文件



3)启动
docker compose up -d

4)查看日志
docker compose logs
5)观察redis-sentinel 配置 rewrite
再次打开哨兵的配置文件,发现文件内容已经自动修改了

2.3 重新选举
手动干掉 redis-master
docker stop redis-master

观察日志

**主观下线:**哨兵感知到主节点没心跳了,判定为主观下线
**客观下线:**多个哨兵达成统一意见,才能认为master确实下线了
接下来,哨兵选举出leader

由哨兵领导挑选出一个新的master

此时,对于Redis来说仍然是可以正常使用的
redis-master 重启之后,观察日志可看到其变为了从节点
docker start redis-master


结论:
- Redis主节点如果宕机,哨兵会把其中一个从节点提拔为主节点
- 当之前的Redis主节点重启后,这个主节点被加入到哨兵的监控中,但是只会被作为从接节点使用
2.4 选举原理
1)主观下线
当 redis-master 宕机,此时 redis-master和单个哨兵之间的心跳包就没有了
此时,站在三个哨兵的角度来看,redis-master出现了严重故障,因此三个哨兵均会把 redis-master判定为主观下线
2)客观下线
此时,哨兵均会对主节点故障这件事进行投票,当故障票数 >=配置的法定票数之后
此时意味着 redis-master 故障这个事情被做实了,此时触发客户端下线

3)选举出哨兵的leader
接下来需要哨兵在剩余的从节点中选出一个新的主节点,这个工作不需要所有哨兵都参与,只需要选出一个代表
Raft算法:

简而言之:Raft算法的核心是"先下手为强",谁率先发出了拉票请求,谁就有更大的概率称为leader(这里的决定因素成了"网络延时",网络延时本身就带有一定的随机性)
4)leader 挑选出合适的 slave 称为新的 master
- 比较优先级:优先级高的先上位,优先级是配置文件中的配置向
- 比较 replication offsets :比较谁复制的数据多
- 比较 run id:id小的上位
2.5 小结
上述过程,都是"无人值守",Redis自动完成,这样就解决了主节点宕机之后需要人工干预的问题,提高了系统的稳定性
注意事项:
- 哨兵节点不能只有一个,否则哨兵节点挂了也会影响系统的可用性
- 哨兵节点最好时奇数个,方便选举leader,得票更容易超过半数
- 哨兵节点不负责存储数据,仍然是redis主从节点负责存储
- 哨兵+主从复制解决的问题是"提高可用性",不能解决"数据极端情况下丢失"的问题
- 哨兵+主从复制不能提高数据的存储容量,当需要存的数据接近或者超过机器的物理内存时要引入集群
3. 集群(Cluster)
3.1 基本概念
上述的哨兵模式提高了系统的可用性,但是真正用来存储数据的还是master和slave节点,所有的数据都需要存储在单个master/slave节点上
Redis集群引入多组 master/slave,每一组 master/slave 存储数据全集的一部分,从而构成一个更大的整体,称为Redis集群(Cluster)

每个红框部分都可以称为是一个分片(Sharding)
如果全量数据进一步增加,值要再增加更多的分片即可
3.2 数据分片算法
Redis cluster 的核心思路是多组机器来存储数据的每一个部分,那么接下来的核心问题就是,给定一个数据(一个具体的key),那么这个数据应存储在哪个分片上,读取的时候又应该去哪个分片读取
围绕这个问题,业界有三种比较主流的实现方式
3.2.1 哈希求余
设有N个分片,使用 0\~N-1 这样的序号进行编号
针对某个给定的key,先计算hash值,再把得到的结果%N,得到的结果即为分片编号

后续如果要获取某个key的记录,也是针对key计算hash值(如md5),再对N求余,就可以找到对应的分片编号
md5 本身就是一个计算hash值的算法,针对一个字符串,将里面的内容进行一系列的数学变换,最后变为整数
MD5 是一个非常广泛使用的hash算法
特点:
- 计算的结果是定长的
- 计算结果是分散的(连个源字符串,哪怕大部分是相同的,只有一小部分是不同的,算出来的值也会差别很大)
- 计算结果是不可逆的
优点:简单高效,数据分配均匀
缺点:一旦需要进行扩容,N改变了,原有的映射规则被破坏了,就需要让节点之间的数据相互传输,重新排列,以满足新的映射规则,此时需要搬运的数据量是比较多的,开销较大
3.2.2 一致性哈希算法
为了降低上述的搬运开销,能够更高效扩容,业界提出了"一致性哈希"
key映射到分片序号的过程不再是简单的求余,而是改为以下过程:
1)把0~2^32-1 这个数据空间映射到一个圆环上,数据按照顺时针方向增长

2)假设当前有三个分片,找到key映射到哪个文职,顺时针往下找,找到的第一个分片就是ikey锁从属的分片

如果要扩容一个分片,原有分片在环上的位置不变,只要在环上安排一个新的分片位置即可

优点:大大降低了扩容时数据搬运的规模,提高了扩容操作的效率
缺点:数据分配不均匀(有多有少,数据倾斜)
3.2.3 哈希槽分区算法(Redis采用)
为了解决以上问题(搬运成本高和数据分配不均匀),Redis cluster 引入了哈希槽(hash slots)算法
hash_slot = crc16(key) % 16384
其中crc16 也是一种哈希算法
相当于把整个哈希值映射到 16384 个槽位上 0\~16383
然后再把这些槽位比较均匀的分配给每个分片,每个分片都需要记录自己持有哪些分片

这里的分片规则是很灵活的,每个分片特有的槽位不一定连续,每个分片的主节点都使用位图来表示自己持有哪些槽位,对于16384个槽位来说,需要2048个字节(2kb)大小的空间
如果需要进行扩容,可以针对原有的槽位进行重新分配

在实际使用Redis集群分片的时候,不需要手动指定指定哪些槽位分配给某些分配,只需要告诉某个分片应持有多少个槽位即可,Redis会自动完成后续的槽位分配,以及对应key的搬运工作
此时有两个问题:
问题一:Redis集群是最多有16384个分片吗?
并非如此,如果一个分片只有一个槽位,这对于这个集群的数据均匀是难以保证的
实际上Redis的作者建议集群分片数不应该超过1000
问题二:为什么是16384个槽位
- 节点之间通过心跳包通信,心跳包包含了该节点持有哪些slots,这个是使用位图这样的数据结构表示的,需要位图大小是2kb,如果给定的slots数多了,此时就需要消耗更多的空间,对于内存来说不算什么,但在频繁的网络心跳包中,是一个不小的开销
- 另一方面,Redis集群一般不超过1000个分片,所以16k对于最大1000个分片来说是足够用的,同时也会使对应的槽位配置位图体积不至于很大
3.3 集群搭建(基于docker)
创建目录和配置
创建 redis-cluster目录

generate.sh内容(脚本)


执行:
bash generate.sh





编写 docker-compose.yml
先创建networks,并分配网段为 127.30.0.0/24
配置每个节点

......
启动容器
docker compose up -d

构建集群
redis-cli --cluster create 172.30.0.101:6379 172.30.0.102:6379 172.30.0.103:6379 172.30.0.104:6379 172.30.0.105:6379 172.30.0.106:6379 172.30.0.107:6379 172.30.0.108:6379 172.30.0.109:6379 --cluster-replicas 2


使用集群来存储数据
之前学过的命令大部分都是使用的,但是操作多个key的操作,由于key分散在不同的分片上,就可能出现问题

key通过hash计算之后。属于103的分片
在启动 redis-cli的时候加上 -c选项,此时客户端发现key的操作不再当前分片上会自动重定向到对应的分片主机上

3.4 主节点宕机
演示效果
手动停止 redis1

连接redis2 查看结果

redis3 晋升为主节点
重新启动,再次查看

重新启动后reids1 仍然是从节点
处理流程
1)故障判定
集群中的所有节点都会周期性的使用心跳包进行通信
- 节点A给节点B发送ping包,B就会给A返回一个pong包,ping包和pong包除了 message type属性除外,其他部分都是一样的,这里包含了集群的配置信息(该节点的id、该节点从属于哪个分片、是主节点还是从节点,从属于谁、持有哪些slots的位图......)
- 每个节点每秒钟,都会给随机的一些节点发送ping包,而不是全发一遍这样的设定是为了避免在节点很多的时候,心跳包过多
- 当节点A给节点B发送ping包,B不能如期回应的时候,此时A就会尝试重置和B的tcp连接,看能否连接成功。如果仍然连接失败,A就会把B设为PFALT状态(相当于主观下线)
- A判定B为PFALT之后,会通过Redis内置的Gossip协议,和其他节点进行沟通,向其他节点确认B的状态(每个节点都会维护一个自己的"下线列表",由于视角不同,每个节点的下线列表也不一定相同)
- 此时A发现其他很多的节点,也认为B为PFALT,并且数目超过总集群个数的一半,那么A就会把B标记为FALT,并且把这个消息同步给其他节点
至此,B就彻底被判定为故障节点了

2)故障迁移
如果B为从节点就不需要进行故障迁移
如果B为主节点,那么就由B的从节点触发故障迁移
所谓故障迁移就是指把从节点提拔为主键带你,继续给整个Redis集群提供支持
具体流程:
- 从节点判定自己是否具有参选资格.如果从节点和主节点已经太久没通信(此时认为从节点的数据和主节点差异太大了),时间超过阈值,就失去竞选资格.
- 具有资格的节点,比如C和D,就会先休眠一定时间.休眠时间=500ms基础时间+0,500ms随机
时间+排名*1000ms.offset的值越大,则排名越靠前(越小). - 比如C的休眠时间到了,C就会给其他所有集群中的节点,进行拉票操作.但是只有主节点才有投票资格.
- 主节点就会把自己的票投给C(每个主节点只有1票).当C收到的票数超过主节点数目的一半,C就会晋升成主节点.(C自己负责执行slaveof no one,并且让D执行slaveof C).
- 同时,C还会把自己成为主节点的消息,同步给其他集群的节点,大家也都会更新自己保存的集群结构信息.
上述选举的过程,称为Raft算法,是在一种分布式系统中广泛使用的算法,在随机休眠时间的加持下,基本上谁先唤醒,谁就能竞选成功
3.5 集群扩容
扩容时在一个开发中经常遇见的场景
随着业务的发展,现有集群很可能无法容纳日益增长的数据,此时给集群中加入更多的新机器,就可以使存储的空间更大
第一步:把新新节点加入到集群
redis-cli --cluster add-node 172.30.0.110:6379 172.30.0.101:6379


第二步:从新分配 slots
redis-cli --cluster reshard 172.30.0.101:6379



确定之后,会初步打印出搬运⽅案,让⽤⼾确认. 之后就会进⾏集群的key搬运⼯作.这个过程涉及到数据搬运.可能需要消耗⼀定的时间.

第三步:给新的主节点添加从节点
redis-cli --cluster add-node 172.30.0.111:6379 172.30.0.101:6379 --cluster slave --cluster-master-id 172.30.1.110 节点的 nodeId


3.6 集群缩容
扩容时比较常见的,但是缩容并不常见
第一步:删除从节点
redis-cli --cluster del-node 集群中任⼀节点 ip:port 要删除的从机节点 nodeId
redis-cli --cluster del-node 172.30.0.101:6379 03f4a97806a0d3de2299cc16e6a3559f0c832bc1

第二步:从新分配slots
redis-cli --cluster reshard 172.30.0.101:6379

先输入接收的节点,后输入要删除的主节点

第三步:删除主节点
redis-cli --cluster del-node 集群中任⼀节点 ip:port 要删除的从机节点 nodeId
