🐼主从复制的问题
我们说redis既要保证效率,也要保证高可用性。单单使用主从复制做到了吗?从节点做到了,任意一个从节点挂了,其他的从节点仍然可以读,但是如果主节点挂了呢?那么这个redis集群就只能读了,再好长一段时间都不能写!直到redis主节点启动起来。并没有保证redis主节点的高可用性
如果主节点发生故障时,进行主备切换的过程是复杂的,需要完全的人⼯参与,导致故障恢复时间无法保障。
这个过程大概分为如下几步:
- 运维⼈员通过监控系统,发现 Redis 主节点故障宕机
- 运维⼈员从所有节点中,选择⼀个(此处选择了 slave 1)执行slaveof no one,使其作为新的主节点。
- 运维⼈员让剩余从节点(此处为 slave 2)执⾏ slaveof {newMasterIp} {newMasterPort} 从新主节点开始数据同步
- 更新应⽤方连接的主节点信息到 {newMasterIp} {newMasterPort}。
- 如果原来的主节点 恢复,执行slaveof {newMasterIp} {newMasterPort} 让其成为⼀个从节点。
- 上述过程可以看到基本需要⼈⼯介⼊,⽆法被认为架构是⾼可用的。而这就是 Redis Sentinel 所要做的。
Redis 哨兵主要解决的问题就是redis的高可用 问题!
🐼什么是哨兵
首先什么是哨兵?就是一个进程!哨兵自动恢复主节点故障
哨兵的作用:当主节点出现故障时,Redis Sentinel 能自动完成故障发现和故障转移,并通知应用方,从而实现真正的高可⽤。
哨兵的结构:Redis Sentinel 是⼀个分布式架构,其中包含若干个 Sentinel 节点和 Redis 数据节点,每个 Sentinel 节点会对数据节点和其余 Sentinel 节点进⾏监控,当它发现节点不可达时,会对节点做下线表示。如果下线的是主节点,它还会和其他的 Sentinel 节点进行**"协商"** ,当大多数 Sentinel 节点对主节点不可达这个结论达成共识之后,它们会在内部 "选举" 出⼀个领导节点来完成自动故障转移的⼯作,同时将这个变化实时通知给 Redis 应用方。整个过程是完全自动的,不需要⼈⼯介⼊。整体的架构如图所示。

✅sentinel节点如何进行监控的呢?sentinel会和数据节点以及其他哨兵节点之间建立TCP长连接,以来定期发送tcp的心跳包。借助上面的监控机制,主节点就知道哪个节点挂了,哪个节点没有挂。
Redis Sentinel 相比于主从复制模式是多了若干个哨兵节点(建议保持奇数 )
如果从节点挂了,那么没有关系。
针对主节点故障的情况,故障转移流程大致如下:
如果主节点挂了,就是上面所说的协商和推举,这步主要是防止该情况:出故障的不是主节点,而是发现故障的哨兵节点,比如网络问题卡了,导致一个哨兵"误判",该情况经常发⽣于哨兵节点的⽹络被孤立的场景下。
然后在当前节点中,选出一个leader,即从众多从节点中选中一个节点作为主节点。让其他从节点同步新主节点;通知应用层(客户端)转移到新主节点。
✅为什么哨兵也要多个呢?避免单点,实现高可用,是reids的核心任务!
🐼实操部署哨兵节点
我们以上面数据节点集合为例,搞三个redis节点,一个作为主节点,2个从节点。另外搞三个哨兵节点。
按理来说,这6个哨兵节点是在不同的主机间的,他们之间是通过网络或者局域网通信的。但是我们这里由于资源限制,都搞在一个主机了(不过,这是毫无意义的,在实际开发中)。
这里会有一个问题,如果这6个节点在同一台主机上部署,可能会有端口号,配置文件,数据文件的冲突,而如果在6台主机上部署,不用考虑这么多乱七八糟的事情,那我们这么干不纯属是给自已找事嘛!
有没有一种方法,模拟出6台主机的效果?搞6个虚拟机?显然不太现实!毕竟很吃配置。
而docker虚拟化容器技术就能很好的解决上述的问题。他可以以很轻量化的配置就完成虚拟化效果。
这里就不得不谈一下docker中最重要的两个概念,镜像和容器。
镜像和容器就**类似于"可执行程序"和"进程"**之间的关系。我们可以根据一个"可执行程序"(镜像)启动很多个"进程"(容器)。
而镜像网上已经有很多现成的大佬们已经写好的了,包括redis镜像,我们拉取下来直接就能用
ok,介绍完以上。下面是安装redis镜像具体的操作了:
cpp
准备⼯作
1) 安装 docker 和 docker-compose (可以网上查一下部署教程)
2) 停⽌之前的 redis-server ,一定要停止,防止对现在的redis服务器产生干扰,比如占用了端口号
3) 使⽤ docker 获取 redis 镜像 docker pull redis:5.0.9
拉取到的镜像,里面有一个轻量化的Linux系统并且有redis,只要基于这个镜像创建一个redis容器,就算好了一个reids服务器
关于docker命令的使用敲一下docker
基于docker来搭建redis哨兵环境
我们使用docker-compose来统一容器编排,通过一个**.yml配置文件**,来配置批量化的容器,和容器的各种参数,后面我们仅需要一个命令,就能批量化的启动和停止这些容器。
什么是.yml文件,类似于json格式的,便于网络发送的,但相比于json,对格式要求更严格,可读性更好
下面,我们来启动3个redis服务器容器和三个redis哨兵容器。
.yml的编排这里我们就把redis一个主节点和2个从节点统一放到数据节点的.yml文件中了。
这里我们就把三个哨兵节点统一放到哨兵节点的.yml文件中了。
总共两个.yml文件。如图:

其实一个.yml文件完全可以启动6个容器,但是谁先起来,不好说,会影响看日志。
注意,.yml文件必须以docker-compose命名!
先看数据节点的.yml文件
bash
version: '3.7'
services:
# Redis Master (主节点)
master:
image: 'redis:5.0.9'
container_name: redis-master
restart: always
# 启动命令:开启 AOF 持久化
command: redis-server --appendonly yes
ports:
- "6379:6379"
# Redis Slave 1 (从节点 1)
slave1:
image: 'redis:5.0.9'
container_name: redis-slave1
restart: always
# 启动命令:作为 master 的从节点
command: redis-server --appendonly yes --slaveof redis-master 6379
ports:
- "6380:6379"
# 依赖于 master 服务,确保 master 先启动
depends_on:
- master
# Redis Slave 2 (从节点 2)
slave2:
image: 'redis:5.0.9'
container_name: redis-slave2
restart: always
# 启动命令:作为 master 的从节点
command: redis-server --appendonly yes --slaveof redis-master 6379
ports:
- "6381:6379"
# 依赖于 master 服务
depends_on:
- master
这个文件内容很容易理解,也不需要我们记住。
唯一需要注意的两点:
1.端口号,为什么有两个?其中,第一个是**外面的的端口号,不能重复,里面的是容器内部的端口号,可以重复。**这样我们可以通过容器外的端口号访问到容器内的端口号。这有点向反向代理的感觉了,有点像内网穿透ftp的哪个例子了哈哈。(容器外的端口号像外网的ip,不能重复,容器内的像内网的ip,可以重复)
2.我们这里没有显示写redis-master的ip,这里的映射非常像NAT,容器名类似于域名一样,docker会自动解析,我们无需考虑了。
下面我们一次性批量化的启动所有redis数据节点容器:
cpp
docker-compose up -d

如果启动后发现前面的配置有误, 需要重新操作, 使用docker-compose down 即可停止并删除
刚才创建好的容器.
查看运行⽇志
bash
docker-compose logs
我们可以使用redis-cli连接不同的redis服务器了。
下面我们来配置哨兵节点,由于哨兵节点,会在运行过程中,对配置文件进行自动的修改,因此,就不能用一个配置文件,做三个哨兵的映射,我们需要创建三个哨兵的配置文件。
这三个哨兵的配置文件内容可以是一样的,内容为:
bash
bind 0.0.0.0
port 26379
sentinel monitor redis-master 172.18.0.2 6379 2
sentinel down-after-milliseconds redis-master 1000
具体含义如下:

目录结构如图:

再看哨兵节点的yml文件:
bash
version: '3.7'
services:
sentinel1:
image: 'redis:5.0.9'
container_name: redis-sentinel-1
restart: always
command: redis-sentinel /etc/redis/sentinel.conf
volumes:
- ./sentinel1.conf:/etc/redis/sentinel.conf
ports:
- "26379:26379"
sentinel2:
image: 'redis:5.0.9'
container_name: redis-sentinel-2
restart: always
command: redis-sentinel /etc/redis/sentinel.conf
volumes:
- ./sentinel2.conf:/etc/redis/sentinel.conf
ports:
- "26380:26379"
sentinel3:
image: 'redis:5.0.9'
container_name: redis-sentinel-3
restart: always
command: redis-sentinel /etc/redis/sentinel.conf
volumes:
- ./sentinel3.conf:/etc/redis/sentinel.conf
ports:
- "26381:26379"
和数据节点的yml文件类似。
在哨兵的目录下加载哨兵的配置文件,启动三个哨兵节点:
cpp
docker-compose up -d
发现启动失败,原因是哨兵节点,不认识redis-master,这是为啥?其实我们使用docker-compose启动的一批容器,其中redis服务器数据节点的容器属于一个局域网,而三个哨兵节点,属于另一个局域网,默认情况下,两个局域网是不通的!
解决方案,可以使用docker-compose把两个局域网放到同一个局域网中。
我们先使用docker network ls列出当前docker中的局域网,
其中第一个是redis服务器容器创建的局域网,第二个是三个哨兵节点创建的局域网。现在我们只需要让哨兵节点加入redis服务器的局域网即可,而不是创建新的局域网。
在哨兵节点的.yml配置文件中加入以下语句:
cpp
networks:
default:
external:
name: redis-data_default
再次启动,
可以看到, 哨兵节点已经通过主节点, 认识到了对应的从节点.

并且哨兵节点的配置文件自动发生了修改了。对⽐这三份⽂件, 可以看到配置内容是存在差异的.
最后注意,你要启动谁的.yml文件使用docker-compose,就去谁的目录下。此时docker-compose这个命令就执行的哪个配置文件。跟使用makefile得在Makefile的目录下同理。
🐼哨兵节点存在的意义
下面我们来看看哨兵节点存在的意义,到底一个主节点挂了,哨兵节点能不能自动 的给我们选举出一个主节点,来代替之前的主节点 ,保证redis仍然是高可用状态
手动把 redis-master 干掉。
docker stop redis-master

此时我们进入哨兵的配置目录,使用docker-compost logs来观察哨兵的日志:
这里以三号哨兵节点为例:

我们可以看到其中有两个字段,sdown和odown。
主观下线 (Subjectively Down, SDown): 哨兵感知到主节点没心跳了. 判定为主观下线.
客观下线 (Objectively Down, ODown): 多个哨兵达成⼀致意见, 才能认为 master 确实下线了
可是,有没有自动的选举出一个主节点代替挂了的主节点呢?
我们分别使用redis-cli连接剩余两个redis服务器,并使用info replication来查看节点信息。

并且6381可以完成写的任务。
如果我们把原本的 redis-master 启动起来,发现它会自动变成从节点了。

所以,依赖于哨兵,只要我们把配置文件配置好了,尽管其中一个主节点挂了,还可以用从节点代替主节点,而原本挂的主节点变为从节点,和新的主节点进行数据同步,实现高可用性,这就是哨兵的意义。
🐼哨兵重新选举主节点的流程
下面我们结合日志来说一下哨兵重新选举主节点的流程。
1.主观下线
当 redis-master 宕机, 此时 redis-master 和三个哨兵之间的⼼跳包就没有了.
此时, 站在三个哨兵的⻆度来看, redis-master 出现严重故障. 因此三个哨兵均会把 redis-master 判定为主观下线 (SDown)

2.客观下线
此时, 哨兵 sentenal1, sentenal2, sentenal3 均会对主节点故障这件事情进⾏投票. 当故障得票数 >= 配置的法定票数之后,就是我们在sentinel.conf中配置的。
此时意味着 redis-master 故障这个事情被做实了. 此时触发客观下线 (ODown)
3) 选举出哨兵的 leader
接下来需要哨兵把剩余的 slave 中挑选出⼀个新的 master. 这个⼯作不需要所有的哨兵都参与. 只需要选出个代表 (称为 leader), 由 leader 负责进⾏ slave 升级到 master 的提拔过程 .所以要先从哨兵选leader,由leader再决定!
这个选举的过程涉及到 Raft 算法。Raft 算法的核⼼就是 "先下⼿为强". 谁率先发出了拉票请求, 谁就有更大的概率成为 leader.这⾥的决定因素成了 "网络延时". 网络延时本⾝就带有⼀定随机性.具体选出的哪个节点是 leader, 这个不重要, 重要的是能选出⼀个节点即可.
leader 节点负责挑选⼀个 slave 成为新的 master. 当其他的 sentenal 发现新的 master 出现了, 就
说明选举结束了.

如果总的票数超过哨兵总数的一半,就算结束了。所以为啥哨兵的个数就是奇数,目的就是为了选举方便投票选leader。上面的投票过程,就是看谁的网络号,手速快。
4)leader 挑选出合适的 slave 成为新的 master
选举的规则如下:
⽐较优先级. 优先级高(数值小的)的上位. 优先级是配置⽂件中的配置项( slave-priority 或者replica-priority ).
-
比较 replication offset 谁复制的数据多, 高的上位.
-
比较 run id , 谁的 id 小, 谁上位.
当某个 slave 节点被指定为 master 之后,
-
leader 指定该节点执⾏ slave no one , 成为 master
-
leader 指定剩余的 slave 节点, 都依附于这个新 master
总结,上述过程, 都是 "⽆⼈值守" , Redis ⾃动完成的. 这样做就解决了主节点宕机之后需要⼈⼯⼲预的问题, 提⾼了系统的稳定性和可⽤性.
⼀些注意事项:
• 哨兵节点不能只有⼀个. 否则哨兵节点挂了也会影响系统可用性.
• 哨兵节点最好是奇数个. 方便选举 leader, 得票更容易超过半数.
• 哨兵节点不负责存储数据. 仍然是 redis 主从节点负责存储.
•选举的时候,不是直接选出主节点,而是先从哨兵节点中选出leader,再由leader来完成后续主节点的指定
• 哨兵 + 主从复制解决的问题是 "提⾼可⽤性", 不能解决 "数据极端情况下写丢失" 的问题.
• 哨兵 + 主从复制不能提⾼数据的存储容量. 当我们需要存的数据接近或者超过机器的物理内存, 这样的结构就难以胜任了.
为了能存储更多的数据, 就引⼊了集群.