Redis:分布式 - 哨兵
概念
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
哨兵主要解决的问题。第二个问题是属于存储分布式的问题,留给 Redis
集群去解决,博客集中讨论第一个问题。
哨兵
当主节点出现故障时,Redis Sentinel
能自动完成故障发现和故障转移,并通知应用方,从而实现真正的高可用。
Redis Sentinel
是一个分布式架构,其中包含若干个 Sentinel
节点和 Redis
数据节点,每个Sentinel
节点会对数据节点和其余 Sentinel
节点进行监控,当它发现节点不可达时,会对节点做下线表示。
如果下线的是主节点,它还会和其他的 Sentinel
节点进行协商
,当大多数 Sentinel
节点对主节点不可达这个结论达成共识之后,它们会在内部选举
出一个领导节点来完成自动故障转移的工作,同时将这个变化实时通知给 Redis
应用方。整个过程是完全自动的,不需要人工介入。
整体的架构如图所示:
Redis Sentinel
相比于主从复制模式多了若干Sentinel
节点,用于实现监控数据节点。哨兵节点会定期监控所有节点(包含数据节点和其他哨兵节点)。
此处有多个哨兵节点,是因为如果只使用一个哨兵进行监控的话,如果哨兵本身就崩溃了,那么整个监控服务都崩溃了。另外的,在网络条件较差的情况下,哨兵很可能会误判主节点的存活情况。
针对主节点故障的情况,故障转移流程大致如下:
- 主节点故障,从节点同步连接中断,主从复制停止。
- 哨兵节点通过定期监控发现主节点出现故障。
哨兵节点与其他哨兵节点进行协商,达成多数认同主节点故障的共识。这步主要是防止出故障的不是主节点,而是发现故障的哨兵节点,该情况经常发生于哨兵节点的网络被孤立的场景下。
- 哨兵节点之间使用
Raft
算法选举出一个领导角色,由该节点负责后续的故障转移工作。 - 哨兵领导者开始执行
故障转移
:- 从节点中选择一个作为新主节点,执行
slave no one
- 让其他从节点同步新主节,执行
slaveof
- 通知应用层转移到新主节点
- 从节点中选择一个作为新主节点,执行
Docker 搭建哨兵分布式
为了演示一个完整的Redis
分布式架构,总共要创建三个数据节点,三个哨兵节点。由于大部分人都只有一台主机,所以此时的最佳解决方案是使用docker
,接下来使用docker
在一台主机上模拟一个分布式系统。
- 拉取
redis:5.0.9
版本的镜像:
bash
docker pull redis:5.0.9
当前目录结构如下:
redis-data
用于存放数据节点,redis-sentinel
用于存放哨兵节点。每个目录下都有docker-compose.yml
,用于进行容器编排。
- 编排
redis-data/docker-compose.yml
:
yml
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
该代码完成了三个数据节点的创建,通过命令完成主从关系的配置。执行之前,记得把主机上的redis
停止,把6379
、6380
、6381
三个端口空出来给docker
启动的redis
。
在目录redis-data
中执行:
bash
docker compose up -d
这样就创建好了三个redis
服务端,可以通过redis-cli -p 6379
、redis-cli -p 6380
、redis-cli -p 6381
来验证是否启动成功。
- 编排
redis-sentinel/docker-compose.yml
:
bash
services:
sentinel1:
image: 'redis:5.0.9'
container_name: redis-sentinel-1
restart: always
command: redis-sentinel /root/redis/sentinel/redis-sentinel/sentinel1.conf
volumes:
- ./sentinel1.conf:/root/redis/sentinel/redis-sentinel/sentinel1.conf
ports:
- 26379:26379
sentinel2:
image: 'redis:5.0.9'
container_name: redis-sentinel-2
restart: always
command: redis-sentinel /root/redis/sentinel/redis-sentinel/sentinel2.conf
volumes:
- ./sentinel2.conf:/root/redis/sentinel/redis-sentinel/sentinel2.conf
ports:
- 26380:26379
sentinel3:
image: 'redis:5.0.9'
container_name: redis-sentinel-3
restart: always
command: redis-sentinel /root/redis/sentinel/redis-sentinel/sentinel3.conf
volumes:
- ./sentinel3.conf:/root/redis/sentinel/redis-sentinel/sentinel3.conf
ports:
- 26381:26379
networks:
default:
external:
name: redis-data_default
此处创建了三个redis-sentinel
哨兵,并且规定它们的配置文件分别为./sentinel1.conf
、./sentinel2.conf
、./sentinel3.conf
。
但是docker
存储卷要用绝对路径,所以你要根据自己的主机情况,填入绝对路径。
配置文件内容如下:
bash
bind 0.0.0.0
port 26379
sentinel monitor redis-master redis-master 6379 2
sentinel down-after-milliseconds redis-master 1000
此处要简单解释一下配置文件:
bash
sentinel monitor 主节点名 主节点ip 主节点端⼝ 法定票数
- 主节点名
这个是哨兵内部自己起的名字
- 主节点 ip
部署 redis-master
的设备ip,此处由于是使用 docker
,可以直接写 docker
的容器名,会被自动 DNS
成对应的容器ip主节点端口。
- 法定票数
哨兵需要判定主节点是否挂了,但是有的时候可能因为特殊情况,比如主节点仍然工作正常,但是哨兵节点自己网络出问题了,无法访问到主节点了。此时就可能会使该哨兵节点认为主节点下线,出现误判。使用投票的方式来确定主节点是否真的挂了是更稳妥的做法,需要多个哨兵都认为主节点挂了,票数 >=法定票数 之后,才会真的认为主节点是挂了。
- sentinel down-after-milliseconds
该参数用于设置心跳包的超时时间,主节点和哨兵之间通过心跳包来进行沟通,如果心跳包在指定的时间内还没回来,就视为是节点出现故障。
既然多个配置文件内容相同,为啥要创建多份配置文件?redis-sentinel
在运行中可能会对配置进行重写,修改文件内容,如果用一份文件,就可能出现修改混乱的情况。
最后执行命令,启动容器:
bash
docker compose up -d
启动后,打开刚刚写的配置文件:
bash
bind 0.0.0.0
port 26379
sentinel myid 15d2602413f32eb3ed797d804a728a59d65e43f1
sentinel deny-scripts-reconfig yes
# Generated by CONFIG REWRITE
dir "/data"
sentinel monitor redis-master 172.18.0.2 6379 2
sentinel down-after-milliseconds redis-master 1000
sentinel config-epoch redis-master 0
sentinel leader-epoch redis-master 0
sentinel known-replica redis-master 172.18.0.4 6379
sentinel known-replica redis-master 172.18.0.3 6379
sentinel known-sentinel redis-master 172.18.0.7 26379 475769f5605edb0016ad007ee06351e058589d4a
sentinel known-sentinel redis-master 172.18.0.5 26379 6ddca4a48f3ec926f8d8408794b261803aa6a5ad
sentinel current-epoch 0
可以看到,除了最开始写入的内容,哨兵启动后又增加了很多新内容。
选举
现在通过docker
关掉master
的容器,来模拟主节点崩溃:
bash
docker stop redis-master
此时哨兵节点就已经在后台工作了,查看哨兵的日志:
bash
docker compose logs
输出:
这是第二个哨兵的日志
sdown master
:这代表哨兵节点发现了master
节点掉线,sdown
表示主观认为,也就是说哨兵还不能保证master
一定掉线odown master
:经过与其它哨兵交流,多个哨兵都认为master
节点掉线,odown
表示客观认为,#quorum 3/2
表示法定票数为2
票,目前有三个哨兵都投票认为master
掉线switch-master
:切换主节点,此时已经有新的节点变成主节点了
进入6380
端口的数据节点,输入info replication
:
可以看到role:master
,6380
成为了新的主节点,当然也有可能是6381
。
尝试重启redis-master
:
这个redis-master
重启后,就变成了从节点,不再是主节点了。
流程
看完选举的现象后,接下来讲解一下选举的具体流程。
- 主观下线
sdown
当主节点宕机,此时主节点和哨兵之间的心跳包就没有了响应,站在三个哨兵的角度来看,主节点出现严重故障,因此三个哨兵均会把主节点判定为主观下线
- 客观下线
odown
此时,哨兵均会对主节点故障这件事情进行投票,当故障得票数 >= 法定票数
之后,这意味着主节点故障这个事情被做实了,触发客观下线
- 选取哨兵leader
接下来需要哨兵把剩余的slave
中挑选出一个新的master
,这个工作不需要所有的哨兵都参与,只需要选出个代表 (称为 leader
),由leader
负责进行 slave
升级到 master
的提拔过程,这个选举的过程涉及到 Raft
算法:
- 每个哨兵节点都给其他所有哨兵节点发起一个
拉票请求
" - 收到拉票请求的节点,会回复一个
投票响应
,当哨兵节点收到多个拉票请求
,只对第一个节点投票,后续节点都不投票 - 一轮投票完成之后,发现得票超过半数的节点,自动成为
leader
如果出现平票的情况,就重新再投一次即可。
因此建议哨兵节点设置成奇数,如果是偶数个,则增大了平票的概率,带来不必要的开销。
最终leader
节点负责挑选一个 slave
成为新的 master
当其他的 sentenal
发现新的 master
出现了,就说明选举结束了。
简而言之,Raft
算法的核心就是"先下手为强"谁率先发出了拉票请求,谁就有更大的概率成为 leader
,这里的决定因素成了"网络延时"。
在日志中,vote-for-leader
就是在投票选举leader
哨兵。
leader
哨兵节点选出一个slave
成为新的master
挑选规则:
- 比较优先级,优先级高(数值小的)的优先上位,优先级是配置文件中的配置项中的
slave-priority
或者replica-priority
) - 如果优先级相同,比较
replication
和offset
谁复制的数据多,高的优先上位 - 如果
replication
和offset
也相同,比较run id
,小的优先上位,
当某个数据节点被选为master
后:
leader
哨兵指定该节点执行slave no one
,成为master
leader
哨兵指定剩余节点执行slave of
,成为该节点的从节点
总结一下:
- 主观下线
sdown
:单个哨兵节点认为主节点掉线 - 客观下线
odown
:投票后客观认为主节点掉线 - 选取哨兵
leader
:依据网络情况,选出一个哨兵成为leader
,由leader
完成选举master
- 选举
master
:由leader
依据优先级,数据同步进度,run id
来选出一个节点成为master
- 重构主从关系:由
leader
哨兵,指定数据节点执行指令,重构主从关系
注意事项:
- 哨兵节点不能只有一个,否则哨兵节点挂了也会影响系统可用性
- 哨兵节点最好是奇数个,方便选举
leader
,得票更容易超过半数 - 哨兵节点不负责存储数据,仍然是
redis
主从节点负责存储. - 哨兵+主从复制解决的问题是"提高可用性",不能解决"数据极端情况下写丢失"的问题,哨兵+主从复制不能提高数据的存储容量