Redis 的主从复制模式下,⼀旦主节点由于故障不能提供服务,需要⼈⼯进⾏主从切换,同时⼤量 的客⼾端需要被通知切换到新的主节点上,对于上了⼀定规模的应⽤来说,这种⽅案是⽆法接受的, 于是 Redis 从 2.8 开始提供了 Redis Sentinel(哨兵)加个来解决这个问题。
哨兵机制的介绍
由于对 Redis 的许多概念都有不同的名词解释,所以在介绍 Redis Sentinel 之前,先对⼏个名词 概念进⾏必要的说明,如表所⽰。
|--------------------|--------------------|-------------------------|
| 名词 | 逻辑结构 | 物理结构 |
| 主节点 | Redis 主服务 | ⼀个独⽴的 redis-server 进程 |
| 从节点 | Redis 从服务 | ⼀个独⽴的 redis-server 进程 |
| Redis 数据节点 | 主从节点 | 主节点和从节点的进程 |
| 哨兵节点 | 监控 Redis 数据节点的节点 ⼀ | ⼀个独⽴的 redis-sentinel 进程 |
| 哨兵节点集合 | 若⼲哨兵节点的抽象组合 | 若⼲ redis-sentinel 进程 |
| Redis 哨兵(Sentinel) | Redis 提供的⾼可⽤⽅案 | 哨兵节点集合 和 Redis 主从节点 |
| 应⽤⽅ | 泛指⼀个多多个客⼾端 | ⼀个或多个连接 Redis 的进程 |
手动恢复redis主从复制的流程
哨兵机制,是通过独立的进程来体现的,和之前的redis-server是不同的进程
redis-sentinel不负责存储数据,只是对其他的redis-server进程起到监控的作用
手动恢复redis主从复制的流程
服务器,要求要有比较高的可用性,7*24运行;服务器长期运行,总会有一些"意外",具体啥时候出意外,咱们也不知道,同时也不能全靠人工来盯着服务器运行;我们可以写一个程序,用程序来盯着服务器的运行状态(监控程序,发现服务器的运行出现状态异常了;往往还需要搭配"报警程序")
- 运维⼈员通过监控系统,发现 Redis 主节点故障宕机
- 运维⼈员从所有节点中,选择⼀个(此处选择了 slave 1)执⾏ slaveof no one,使其作为新的主 节点
- 运维⼈员让剩余从节点(此处为 slave 2)执⾏ slaveof {newMasterIp} {newMasterPort} 从新主节 点开始数据同步
- 更新应⽤⽅连接的主节点信息到 {newMasterIp} {newMasterPort}
- 如果原来的主节点恢复,执⾏ slaveof {newMasterIp} {newMasterPort} 让其成为⼀个从节点。 上述过程可以看到基本需要⼈⼯介⼊,⽆法被认为架构是⾼可⽤的。⽽这就是 Redis Sentinel 所要做 的。
如果操作过程出错了咋办,并且恢复的过程也得半个小时以上,这显然并不合适。
自动恢复redis主从复制的流程
监控:这些进程之间,会建立tcp长连接,通过这样的长连接,定期发送心跳包;借助上图的监控机制,就可以及时发现某个主机是否是挂了,如果是从节点挂了,其实没关系;如果是主节点挂了,哨兵就会发挥作用
Redis Sentinel 相⽐于主从复制模式是多了若⼲(建议保持奇数)Sentinel 节点⽤于实现监控数据节 点,哨兵节点会定期监控所有节点(包含数据节点和其他哨兵节点)。针对主节点故障的情况,故障转移流程⼤致如下:
- 主节点故障,从节点同步连接中断,主从复制停⽌
- 哨兵节点通过定期监控发现主节点出现故障。哨兵节点与其他哨兵节点进⾏协商,达成多数认同主 节点故障的共识。这步主要是防⽌该情况:出故障的不是主节点,⽽是发现故障的哨兵节点,该情况经常发⽣于哨兵节点的⽹络被孤⽴的场景下
- 哨兵节点之间使⽤ Raft 算法选举出⼀个领导⻆⾊,由该节点负责后续的故障转移⼯作
- 哨兵领导者开始执⾏故障转移:从节点中选择⼀个作为新主节点;让其他从节点同步新主节点;通知应⽤层(客户端)转移到新主节点。
redis哨兵核心功能:
- 监控
- 自动的故障转移
- 通知
redis哨兵节点,有一个也是可以的,但是:
- 如果哨兵节点只有一个,它自身也是容易出现问题的,万一这个哨兵节点挂了,后续redis节点也挂了,就无法进行自动的恢复过程了
- 出现误判的概率也比较高,网络传输数据是容易出现抖动或者延迟或者丢包的,如果只有一个哨兵节点,出现上述问题之后,影响就比较大
- 基本原则:在分布式系统中,应该避免使用"单点"
- 哨兵节点,最好要搞奇数个,最少也应该是3个(和选举机制有关)
使用docker搭建环境
类似下图的结构,按理说这6个节点要在6个不同的服务器主机上的,此时只有一个云服务器,就在一个云服务器上,来完成这里的环境搭建(在工作中,把上述节点放到一个服务器上,是没有意义的)
由于这些节点有点多,相互之间依赖的端口号/配置文件/数据文件可能会冲突,如果直接部署,就需要小心翼翼的取避开这些冲突-使用docker就可以有效避免上述问题
虚拟机,通过软件,在一个电脑上模拟出另外的一些硬件(构造了另一个虚拟的电脑),虚拟机这样的软件,就可以使用一个电脑,来模拟出多个电脑的情况;但是虚拟机比较吃配置
docker可以认为是一个轻量级"虚拟机",起到了虚拟机这样的隔离环境的效果,但是又没有吃很多的硬件资源,即使是配置比较拉跨的云服务器,也能构造出好几个这样的虚拟环境
- 安装docker和docker-compose
docker中关键概念"容器",每一个容器都可以看做一个轻量级的虚拟机
docker-compose 的安装
bash
# ubuntu
apt install docker-compose
# centos
yum install docker-compose
- 停⽌之前的 redis-server
bash
# 停⽌ redis-server
service redis-server stop
# 停⽌ redis-sentinel 如果已经有的话.
service redis-sentinel stop
- 使⽤ docker 获取 redis 镜像
bash
docker pull redis:5.0.9
#类似于git clone从远程仓库拉取代码
docker中的"镜像"和"容器"类似于"可执行程序"和"进程"的关系;镜像可以自己创建,也可以直接拿别人已经构建好的,docker hub(类似于Github)包含了很多其他大佬构建好的镜像
拉取到的镜像,里面包含一个精简的Linux操作系统,并且上面会安装redis,只要直接基于这个镜像创建一个容器跑起来,此时redis服务器就搭建好了
docker-compose来进行容器编排,此处涉及到多个redis-server也有多个redis哨兵节点,每一个redis-server或者每一个redis哨兵节点都是作为一个单独的容器了(6个容器)
通过一个配置文件,把具体要创建哪些容器,每个容器运行的各种参数,描述清楚,后续通过一个简单的命令,就能够批量的启动/停止这些容器了
使用yml这样的格式来作为配置文件,spring也是使用yml来作为配置文件的
- 创建三个容器,作为redis的数据节点(一主两从)
- 创建三个容器,作为redis的哨兵节点
其实也是可以用一个yml文件,直接启动6个容器;如果把这6个容器同时启动,可能是哨兵先启动完成,数据节点后启动完成,哨兵可能就会先认为是数据节点挂了,虽然对于大局不影响,但是会影响到观察执行日志的过程
bash
version: '3.7'
services:
#名字是自己起的
master:
#基于哪个镜像创建的
image: 'redis:5.0.9'
container_name: redis-master
#是否自动重启
restart: always
command: redis-server --appendonly yes
ports:
#端口映射,也就是隧道
#也就是XShell中的隧道命令
#宿主机的端口:容器内部的端口
- 6379:6379
#名字是自己起的,容器名就相当于域名
slave1:
image: 'redis:5.0.9'
container_name: redis-slave1
restart: always
command: redis-server --appendonly yes --slaveof redis-master 6379
ports:
- 6380:6379
#名字是自己起的
slave2:
image: 'redis:5.0.9'
container_name: redis-slave2
restart: always
command: redis-server --appendonly yes --slaveof redis-master 6379
ports:
- 6381:6379
#这里格式似乎不太正确 请大家自行抄下面的
version: '3.7'
services:
master:
image: 'redis:5.0.9'
container_name: redis-master
restart: always
command: redis-server --appendonly yes
ports:
- 6379:6379
slave1:
image: 'redis:5.0.9'
container_name: redis-slave1
restart: always
command: redis-server --appendonly yes --slaveof redis-master 6379
ports:
- 6380:6379
slave2:
image: 'redis:5.0.9'
container_name: redis-slave2
restart: always
command: redis-server --appendonly yes --slaveof redis-master 6379
ports:
- 6381:6379
- 启动所有容器
bash
docker-compose up -d
#-d表示后台运行
此步骤一定要把redis三个服务器彻底关闭,不然无法运行成功
此处启动的redis就是容器中的redis
- 查看运⾏⽇志
bash
docker-compose logs
redis哨兵节点是单独的redis服务器进程
- 在redis-sentienl中写docker-compose.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:
#映射文件,将配置文件映射到/etc/redis/sentinel.conf
#哨兵节点,会在运行过程中,对配置文件进行自动的修改,因此就不能拿一个配置文件,给三个容器分别映射
- ./sentinel1.conf:/etc/redis/sentinel.conf
ports:
#端口映射:docker核心功能
- 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
- 创建配置⽂件
创建 sentinel1.conf sentinel2.conf sentinel3.conf . 三份⽂件的内容是完全相同 的,都放到 /root/redis-sentinel/ ⽬录中.
bash
bind 0.0.0.0
#允许其他节点来访问
port 26379
#容器内部端口号
sentinel monitor redis-master redis-master 6379 2
#让当前的哨兵节点监控哪个redis服务器,redis-master本来是IP,但是docker会自动进行域名解析
#6379为端口号,2是法定票数(达到法定票数就可以判定redis主节点挂了)
sentinel down-after-milliseconds redis-master 1000
#down-after-milliseconds明确心跳包的超时时间,设定1000ms之内,如果不返回,就认为挂了,此时投票
报错:哨兵节点不认识redis-master,redis-master相当于一个域名,docker会进行域名解析;docker-compose一下启动了N个容器,此时N个容器处于同一个"局域网"中,可以使这N个容器之间可以相互访问,三个redis-server节点是一个局域网,三个哨兵节点是另一个局域网,默认情况下,这俩网络不是互通的
解决方案:使用docker-compose 把此处的两组服务放到同一个局域网中
bash
docker network ls
- 在docker-compose.yml中加入配置局域网
bash
networks:
default:
external:
name: redis-data_default
Question:是否可以把6个容器,都写到同一个yml配置中,一次全都启动,不就保证互通问题了吗?
Answer:如果使用这种方案,由于docker-compose启动容器的顺序不确定,就不能保证redis-server一定是在哨兵之前启动的,分成两组就可以保证上述顺序,观察到的日志是比较可控的
在sentinel1.conf文件中产生了新的内容,这些内容都是哨兵节点启动之后,自动进行修改的
哨兵节点的作用演示
哨兵存在的意义,能够在redis主从结构出现问题的时候(比如主节点挂了),此时哨兵节点就能够自动的帮我们重新选出一个主节点,来代替之前挂了的节点,保证整个redis仍然是可用状态
手动把主节点干掉
当主节点挂了之后,哨兵节点开始工作
- sdown:主观下线,本哨兵节点,认为该节点挂了
- odown:客观下线,好几个哨兵都认为该节点挂了,达成了一致(法定票数)
此时,主节点挂了这个事情就被实锤了
此时就需要哨兵节点选出一个从节点,作为新的主节点,此处就需要提拔出一个新的主节点
即使原主节点启动,也是作为从节点
主从切换的具体流程
哨兵重新选取主节点的流程
- 主观下线:哨兵节点通过心跳包,判定redis服务器是否正常工作,如果心跳包没有如约而至,就说明redis服务器挂了,此时还不能排除网络波动的影响,因此就只能是单方面认为这个redis节点挂了
- 客观下线:多个哨兵都认为主节点挂了(认为挂了的哨兵节点数目达到法定票数),哨兵们就认为这个主节点是客观下线
Question:是否有可能出现非常严重的网络波动,导致所有的哨兵都联系不上redis主节点,误判成挂了呢?
当然是有的,如果出现这个情况,怕是用户的客户端也连不上redis主节点了,此时这个主节点基本也就无法正常工作了
- 要让多个哨兵节点,选出一个leader,由这个leader负责选一个从节点作为新的主节点
日志中可以看到
三个哨兵节点id
- 一号哨兵立即给自己投了一票
- 二号哨兵也立即给了一号哨兵投了一票
- 三号哨兵也立即给了二号哨兵投了一票
bash
redis-sentinel-1 | 1:X 15 Aug 2024 04:10:18.300 # +vote-for-leader 8727c0b46d6aaaee1d5f00ef3ef100e81e73de34 1
redis-sentinel-1 | 1:X 15 Aug 2024 04:10:18.301 # bbd99057b11998a9d52786d2164eaecfaae64441 voted for bbd99057b11998a9d52786d2164eaecfaae64441 1
redis-sentinel-1 | 1:X 15 Aug 2024 04:10:18.308 # 72f040cb2884e70c8a5faa3c7d0baad4da53fce5 voted for 8727c0b46d6aaaee1d5f00ef3ef100e81e73de34 1
每个哨兵手里只有一票,当哨兵1第一个发现客观下线之后,就立即给自己投了一票,并且告诉2,3,我来负责这个事情;2 3反应慢了半拍,才发现是客观下线,一看1乐意负责这个事情,立即投了赞成票(如果总的票数超过哨兵总数的一半,选举完成了->把哨兵个数设置为奇数个节点,就是为了方便投票)
leader选主节点
- 此时leader选举完毕,leader就需要挑选一个从节点,作为新的主节点
- 优先级:每个redis数据节点,都会在配置文件中,有一个优先级的设置,slave-priority,优先级高的从节点就会胜出
- offset:offset最大,就胜出,offset从节点从主节点这边同步数据的进度,数值越大,说明从节点的数据和主节点就越接近
- run id:每个redis节点启动的时候随机生成的一串数字(大小全凭缘分)
把新的主节点指定好了之后,leader就会控制这个节点,执行slaveof no one,成为master,再控制其他节点,执行slaveof,让这些其他节点,以新的master作为主节点了
先选leader,再选主节点
小结
上述过程, 都是 "⽆⼈值守" , Redis ⾃动完成的,这样做就解决了主节点宕机之后需要⼈⼯⼲预的问题, 提⾼了系统的稳定性和可⽤性
⼀些注意事项
- 哨兵节点不能只有⼀个. 否则哨兵节点挂了也会影响系统可⽤性
- 哨兵节点最好是奇数个. ⽅便选举 leader, 得票更容易超过半数
- 哨兵节点不负责存储数据. 仍然是 redis 主从节点负责存储
- 哨兵 + 主从复制解决的问题是 "提⾼可⽤性", 不能解决 "数据极端情况下写丢失" 的问题
- 哨兵 + 主从复制不能提⾼数据的存储容量. 当我们需要存的数据接近或者超过机器的物理内存,这样的结构就难以胜任了.
- 为了能存储更多的数据, 就引⼊了集群.