Redis:分布式 - 哨兵

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 的主从复制模式可以将主节点的数据改变同步给从节点,这样从节点就可以起到两个作用:

  1. 作为主节点的一个备份,一旦主节点出了故障不可达的情况,从节点可以作为后备,并且保证数据尽量不丢失(主从复制表现为最终一致性)。
  2. 从节点可以分担主节点上的读压力,让主节点只承担写请求的处理,将所有的读请求负载均衡到各个从节点上。

但是主从复制模式并不是万能的,它同样遗留下以下几个问题:

  1. 主节点发生故障时,进行主备切换的过程是复杂的,需要完全的人工参与,导致故障恢复时间无法保障。
  2. 主节点可以将读压力分散出去,但写压力/存储压力是无法被分担的,还是受到单机的限制。

其中第一个问题是高可用问题,即 Redis 哨兵主要解决的问题。第二个问题是属于存储分布式的问题,留给 Redis 集群去解决,博客集中讨论第一个问题。


哨兵

当主节点出现故障时,Redis Sentinel能自动完成故障发现和故障转移,并通知应用方,从而实现真正的高可用。

Redis Sentinel是一个分布式架构,其中包含若干个 Sentinel 节点和 Redis 数据节点,每个Sentinel节点会对数据节点和其余 Sentinel节点进行监控,当它发现节点不可达时,会对节点做下线表示。

如果下线的是主节点,它还会和其他的 Sentinel 节点进行协商,当大多数 Sentinel 节点对主节点不可达这个结论达成共识之后,它们会在内部选举出一个领导节点来完成自动故障转移的工作,同时将这个变化实时通知给 Redis 应用方。整个过程是完全自动的,不需要人工介入。

整体的架构如图所示:

Redis Sentinel相比于主从复制模式多了若干Sentinel节点,用于实现监控数据节点。哨兵节点会定期监控所有节点(包含数据节点和其他哨兵节点)。

此处有多个哨兵节点,是因为如果只使用一个哨兵进行监控的话,如果哨兵本身就崩溃了,那么整个监控服务都崩溃了。另外的,在网络条件较差的情况下,哨兵很可能会误判主节点的存活情况。

针对主节点故障的情况,故障转移流程大致如下:

  1. 主节点故障,从节点同步连接中断,主从复制停止。
  2. 哨兵节点通过定期监控发现主节点出现故障。

哨兵节点与其他哨兵节点进行协商,达成多数认同主节点故障的共识。这步主要是防止出故障的不是主节点,而是发现故障的哨兵节点,该情况经常发生于哨兵节点的网络被孤立的场景下。

  1. 哨兵节点之间使用 Raft 算法选举出一个领导角色,由该节点负责后续的故障转移工作。
  2. 哨兵领导者开始执行故障转移
    • 从节点中选择一个作为新主节点,执行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停止,把637963806381三个端口空出来给docker启动的redis

在目录redis-data中执行:

bash 复制代码
docker compose up -d

这样就创建好了三个redis服务端,可以通过redis-cli -p 6379redis-cli -p 6380redis-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:master6380成为了新的主节点,当然也有可能是6381

尝试重启redis-master

这个redis-master重启后,就变成了从节点,不再是主节点了。


流程

看完选举的现象后,接下来讲解一下选举的具体流程。

  • 主观下线 sdown

当主节点宕机,此时主节点和哨兵之间的心跳包就没有了响应,站在三个哨兵的角度来看,主节点出现严重故障,因此三个哨兵均会把主节点判定为主观下线

  • 客观下线 odown

此时,哨兵均会对主节点故障这件事情进行投票,当故障得票数 >= 法定票数之后,这意味着主节点故障这个事情被做实了,触发客观下线

  • 选取哨兵leader

接下来需要哨兵把剩余的slave 中挑选出一个新的master ,这个工作不需要所有的哨兵都参与,只需要选出个代表 (称为 leader),由leader负责进行 slave 升级到 master 的提拔过程,这个选举的过程涉及到 Raft 算法:

  1. 每个哨兵节点都给其他所有哨兵节点发起一个拉票请求"
  2. 收到拉票请求的节点,会回复一个投票响应,当哨兵节点收到多个拉票请求,只对第一个节点投票,后续节点都不投票
  3. 一轮投票完成之后,发现得票超过半数的节点,自动成为 leader 如果出现平票的情况,就重新再投一次即可。

因此建议哨兵节点设置成奇数,如果是偶数个,则增大了平票的概率,带来不必要的开销。

最终leader 节点负责挑选一个 slave 成为新的 master 当其他的 sentenal发现新的 master 出现了,就说明选举结束了。

简而言之,Raft 算法的核心就是"先下手为强"谁率先发出了拉票请求,谁就有更大的概率成为 leader,这里的决定因素成了"网络延时"。

在日志中,vote-for-leader就是在投票选举leader哨兵。

  • leader哨兵节点选出一个 slave 成为新的 master

挑选规则:

  1. 比较优先级,优先级高(数值小的)的优先上位,优先级是配置文件中的配置项中的 slave-priority 或者replica-priority)
  2. 如果优先级相同,比较 replicationoffset 谁复制的数据多,高的优先上位
  3. 如果replicationoffset也相同,比较 run id ,小的优先上位,

当某个数据节点被选为master后:

  1. leader哨兵指定该节点执行slave no one,成为master
  2. leader哨兵指定剩余节点执行slave of,成为该节点的从节点

总结一下:

  • 主观下线 sdown:单个哨兵节点认为主节点掉线
  • 客观下线 odown:投票后客观认为主节点掉线
  • 选取哨兵leader:依据网络情况,选出一个哨兵成为leader,由leader完成选举master
  • 选举master:由leader依据优先级,数据同步进度,run id来选出一个节点成为master
  • 重构主从关系:由leader哨兵,指定数据节点执行指令,重构主从关系

注意事项:

  • 哨兵节点不能只有一个,否则哨兵节点挂了也会影响系统可用性
  • 哨兵节点最好是奇数个,方便选举 leader,得票更容易超过半数
  • 哨兵节点不负责存储数据,仍然是 redis 主从节点负责存储.
  • 哨兵+主从复制解决的问题是"提高可用性",不能解决"数据极端情况下写丢失"的问题,哨兵+主从复制不能提高数据的存储容量

相关推荐
技术路上的苦行僧19 分钟前
分布式专题(10)之ShardingSphere分库分表实战指南
分布式·shardingsphere·分库分表
小蜗牛慢慢爬行28 分钟前
如何在 Spring Boot 微服务中设置和管理多个数据库
java·数据库·spring boot·后端·微服务·架构·hibernate
hanbarger31 分钟前
nosql,Redis,minio,elasticsearch
数据库·redis·nosql
微服务 spring cloud1 小时前
配置PostgreSQL用于集成测试的步骤
数据库·postgresql·集成测试
先睡1 小时前
MySQL的架构设计和设计模式
数据库·mysql·设计模式
弗罗里达老大爷1 小时前
Redis
数据库·redis·缓存
GitCode官方1 小时前
GitCode 光引计划投稿 | GoIoT:开源分布式物联网开发平台
分布式·开源·gitcode
仰望大佬0072 小时前
Avalonia实例实战五:Carousel自动轮播图
数据库·microsoft·c#
学不透java不改名2 小时前
sqlalchemy连接dm8 get_columns BIGINT VARCHAR字段不显示
数据库