redis系列--深入哨兵集群

时间:2021-12-04 18:53:21

一、前言

  在之前的系列文章中介绍了redis的入门、持久化以及复制功能,如果不了解请移步至redis系列进行阅读,当然我也是抱着学习的知识分享,如果有什么问题欢迎指正,也欢迎大家转载。而本次将介绍哨兵集群相关知识,包括哨兵集群部署、哨兵原理、相关配置、故障转移等内容,正因为redis有了哨兵机制,而在很多企业(包括笔者自身的公司)采用的是哨兵模式下的redis主从。

二、哨兵(Sentinel)简介

   哨兵(后文统称sentinel)是官方推荐的的高可用(HA)解决方案。在之前的文章中介绍过redis的主从高可用解决方案,这种方案的缺点在于当master故障时候,需要手动进行故障恢复,而sentinel是一个独立运行的进程,它能监控一个或多个主从集群,并能在master故障时候自动进行故障转移,更为理想的是sentinel本身是一个分布式系统,其分布式设计思想有点类似于zookeeper,当某个时候Master故障后,sentinel集群采用Raft算法来选取Leader,故障转移由Leader完成。而对于客户端来说,操作redis的主节点,我们只需要询问sentinel,sentinel返回当前可用的master,这样一来客户端不需要关注的切换而引发的客户端配置变更。一个典型的sentinel架构如下图:

redis系列--深入哨兵集群

sentinel的主要功能:

  • 监控(Monitoring): sentinel 会不断地检查Master和Slave是否运作正常。
  • 通知(Notification):当被监控的某个Redis实例出现问题时, 哨兵(sentinel) 可以通过 API 向管理员或者其他应用程序发送通知。
  • 自动故障迁移(Automatic failover):当一个Master不能正常工作时,sentinel)会开始一次自动故障迁移操作,它会将失效Master的其中一个Slave升级为新的Master, 并让失效Master的其他Slave改为复制新的Master; 当客户端试图连接失效的Master时,集群也会向客户端返回新Master的地址,使得集群可以使用Master代替失效Master。
  • 配置中心(Configuration provider):如果故障转移发生了,sentinel会返回新的master地址。

三、Sentinel集群部署

环境规划

  本次部署过程中将分别部署三个哨兵节点来监控一主二从的redis集群,主从的搭建过程可以参考笔者博文《redis系列--主从复制以及redis复制演进》,以下是环境规则:

  • 哨兵节点:10.1.210.32:26379、10.1.210.33:26379、10.1.210.34:26379
  • redis实例:10.1.210.69:6379(主)、10.1.210.69:6380(从)、10.1.210.69:6381(从)

安装配置

  sentinel安装与redis安装过程一致,请参考redis系列文章,而在源码中,redis提供了参考配置示例sentinel.conf(可以使用命令grep -E -v ^# sentinel.conf 查看),如下:

port 26379

dir /tmp

sentinel monitor mymaster 127.0.0.1 6379 2

sentinel down-after-milliseconds mymaster 30000

sentinel parallel-syncs mymaster 1

sentinel failover-timeout mymaster 180000

配置说明:

sentinel monitor mymaster 127.0.0.1 6379 2

这行配置代表sentinel监控的master名字叫做mymaster(可以自己取),地址是127.0.0.1,端口是6379。最后一个2代表当sentinel集群中有2个sentinel认为master故障时候才判定master真正不可用。官方把该参数称为quorum,在后续选举领头哨兵时候会用到,在下文将进行介绍。

sentinel down-after-milliseconds mymaster 30000

sentinel会向master发送心跳PING来确认master是否存活,如果master在“一定时间范围”内不回应PONG 或者是回复了一个错误消息,那么这个sentinel会主观地(单方面地)认为这个master已经不可用了(subjectively down, 也简称为SDOWN)。而这个down-after-milliseconds就是用来指定这个“一定时间范围”的,单位是毫秒,在这里表示30秒时间内master不回应PONG则主观不可用。

sentinel parallel-syncs mymaster 1

该配置表明在发生failover主备切换时候,最多允许多少个slave同时同步新的master。这个数字越小,完成failover所需的时间就越长,但是如果这个数字越大,就意味着越多的slave因为replication而不可用。可以通过将这个值设为 1 来保证每次只有一个slave处于不能处理命令请求的状态。

sentinel failover-timeout mymaster 180000

failover-time超时时间,当failover开始后,在此时间内仍然没有触发任何failover操作,当前sentinel将会认为此次failover失败,单位毫秒。

不难发现关于sentinel的配置都是固定格式如下:

sentinel <option_name> <master_name> <option_value>

运行Sentinel

  启动sentinel的方式有两种,两种方式都必须指定配置文件:

#第一种(推荐)
redis-sentinel /path/to/sentinel.conf #第二种
redis-server /path/to/sentinel.conf --sentinel

以下是笔者三个节点的配置文件,并同时拷贝到三个节点进行启动:

bind 10.1.210.32 #IP地址

port 26379   #端口

dir /opt/db/redis  # 数据存储目录

daemonize yes  #后台运行

logfile /opt/db/redis/sentinel.log  #日志

sentinel monitor mymaster 10.1.210.69 6379 2

sentinel down-after-milliseconds mymaster 30000

sentinel parallel-syncs mymaster 1

sentinel failover-timeout mymaster 180000

10.1.210.32

bind 10.1.210.33 #IP地址

port 26379   #端口

dir /opt/db/redis  # 数据存储目录

daemonize yes  #后台运行

logfile /opt/db/redis/sentinel.log  #日志

sentinel monitor mymaster 10.1.210.69 6379 2

sentinel down-after-milliseconds mymaster 30000

sentinel parallel-syncs mymaster 1

sentinel failover-timeout mymaster 180000

10.1.210.33

bind 10.1.210.34 #IP地址

port 26379   #端口

dir /opt/db/redis  # 数据存储目录

daemonize yes  #后台运行

logfile /opt/db/redis/sentinel.log  #日志

sentinel monitor mymaster 10.1.210.69 6379 2

sentinel down-after-milliseconds mymaster 30000

sentinel parallel-syncs mymaster 1

sentinel failover-timeout mymaster 180000

10.1.210.34

通过redis-sentinel /opt/db/redis/sentinel.conf启动每个sentinel,以下是启动日志(可以发现sentinel自动通过master发现slave和其他sentinel):

redis系列--深入哨兵集群

此时,一个sentinel集群就搭建完成。

sentinel相关命令

  和redis一样,sentinel可以通过客户端使用命令操作,例如查看master状态SENTINEL masters,示例:

redis系列--深入哨兵集群

以下是所有命令以及解释:

SENTINEL masters  #列出所有被监视的master,以及当前master状态
SENTINEL master <master name> #列出指定的master
SENTINEL slaves <master name> #列出给定master的所有slave以及slave状态
SENTINEL sentinels <master name> #列出监控指定的master的所有sentinel
SENTINEL get-master-addr-by-name <master name> #返回给定master名字的服务器的IP地址和端口号
SENTINEL reset <pattern> #重置所有匹配pattern表达式的master状态
SENTINEL failover <master name> #当msater失效时, 在不询问其他 Sentinel 意见的情况下, 强制开始一次自动故障迁移,但是它会给其他sentinel发送一个最新的配置,其他sentinel会根据这个配置进行更新
SENTINEL ckquorum <master name> #检查当前sentinel的配置能否达到故障切换master所需的数量,此命令可用于检测sentinel部署是否正常,正常返回ok
SENTINEL flushconfig #强制sentinel将运行时配置写入磁盘,包括当前sentinel状态

四、Sentinel原理

SDOWN和ODOWN

  在介绍sentinel原理之前,需要了解的两个概念SDOWN和ODOWN:

  • SDOWN:全拼Subjectively Down,称为主观下线,指的是单个sentinel对redis实例作出的下线状态判断。
  • ODOWN:全拼Objectively Down,称为客户端下线,指多个 Sentinel 实例在对同一个redis做出 SDOWN 判断,并且通过 SENTINEL is-master-down-by-addr 命令互相交流之后,得出的redis实例下线判断。(一个 Sentinel 可以通过向另一个 Sentinel 发送 SENTINEL is-master-down-by-addr 命令来询问对方是否认为给定的redis实例已下线。)

从sentinel的角度来看,如果发送了PING心跳后,在一定时间内没有收到合法的回复,就达到了SDOWN的条件。这个时间在配置中通过master-down-after-milliseconds参数配置。

当sentinel发送PING后,以下回复之一都被认为是合法的:

PING replied with +PONG.
PING replied with -LOADING error.
PING replied with -MASTERDOWN error.

其它任何回复(或者根本没有回复)都是不合法的。

从SDOWN切换到ODOWN不需要任何一致性算法,只需要一个gossip协议:如果一个sentinel收到了足够多的sentinel发来消息告诉它某个master已经down掉了,SDOWN状态就会变成ODOWN状态。如果之后master可用了,这个状态就会相应地被清理掉。

真正进行failover需要一个授权的过程,这个授权的过程即是leader选取过程,但是所有的failover都开始于一个ODOWN状态。ODOWN状态只适用于master,对于不是master的redis节点sentinel之间不需要任何协商,slaves和sentinel不会有ODOWN状态。

实现原理  

  一个sentinel启动时会读取配置文件,并通过sentinel monitor <master-name> <ip> <port> <quorum>配置寻找要监控的主数据库,这个配置在之前已经进行详细说明,其中master-name是由一个大小写字母、数字、和“._-”组成的数据库主库名字,为了考虑到主库的IP地址和端口可能在故障切换后发生变化,所以还需要ip和port来标示这个主库。一个哨兵可监控多个主从系统从而形成网状结构,正如在前面简介的图示一样。

  sentinel启动后,会与监控的数据库建立两条连接,如下图(主库10.1.210.69:6379与10.1.210.32的sentinel节点两条链接):

redis系列--深入哨兵集群

这两个连接与普通客户端一样,其中一条连接用来订阅master的__sentinel__:hello频道用于获取其他监控该数据库的sentinel节点信息,另外一条用于哨兵定期向主数据库发送INFO等命令获取主库本身信息,原因在于当客户端进入订阅模式以后只能接受消息,不能发送命令,所以还需要建立一条连接。

  与监控的主库建立连接完成后,sentinel定时执行以下操作:

  1. 每10s会向主数据库和从数据库发送INFO命令;
  2. 每1s向master、slave以及其他哨兵节点发送PING命令;
  3. 每2s向master和slave的__sentiel__:hello频道发送自己的信息来宣布自己的存在,同时该过程也是实现哨兵之间自动发现的基础;

这三个操作贯穿了哨兵整个生命周期,非常重要,也是其原理的核心,所以以下将详细介绍该操作过程。

  首先,sentinel启动后,向主库发送INFO命令使得sentinel可以获取当前主库的相关信息(包括运行的ID,复制信息、以及属于该主库的从库节点信息),这也是为什么在配置监控时候只需要配置监控的主库信息sentinel就自动找到其对应的从库,进而实现从库的监控。而后和每个从库同样建立两个连接,这两个连接和上文介绍的与主库的连个连接完全一致,在此之后,哨兵会每10s定时向已知所有主从发送INFO命令获取信息更新并进行相应操作,比如对新增的从库建立连接并加入监控队列、又或者是主库信息发生变化(由failover引起的)进行信息更新等。

  接下来哨兵向master和slave的__sentinel__:hello频道发送信息与同样监控该redis示例的其他哨兵分享自己的信息。发送的消息内容为:

<哨兵地址> ,<哨兵端口>,<哨兵运行的ID>,<哨兵配置的版本>,<主库名称>,<主库地址>,<主库端口>,<主库配置版本>,该消息包含了哨兵基本信息以及监控的主库信息,当其他sentinel收到消息后会判断发消息的哨兵是不是新的哨兵,如果是则将其加入已发现的哨兵列表,并创建一个到其的连接(与数据库不同)哨兵与哨兵之间只会创建一条连接用于发送PING命令,同时sentinel会判断主数据库的配置版本,如果该版本比记录数据库版本高,则更新主数据库的数据,其作用在后续介绍。

  实现了自动发现从数据库和其他sentinel节点后,sentinel后续要做的任务是定时监控这些已经发现的主从节点和sentinel节点是否在线。这种监控实现方式是在通过一定时间间隔发送PING命令实现,时间间隔配置通过down-after-milliseconds指定,当超过down-after-milliseconds配置的时间后,如果被PING的数据库或者sentinel未回复,则哨兵认为其主观下线(主观下线在上面已经介绍了),如果该节点是主库sentinel会进一步进行判断是否需要对其进行故障恢复(failover):sentinel会发送SENTINEL is-master-down-by-addr命令询问其他sentinel节点是否也认为该主库主观下线,如果达到指定数量(在示例配置中也进行了说明,示例配置的是2)时,哨兵会认为其客观下线,并选取领头的哨兵(leader)进行故障恢复,选举过程后续介绍。

  选举完零头哨兵后,领头哨兵会开始对主数据库进行故障恢复,这一过程称为failover,在选取新的master时候,sentinel会考虑以下情况:

  • 跟master断开连接的时长
  • slave的优先级 (由slave-priority配置指定)
  • 复制偏移量offset
  • 实例运行的id(run id)

具体的选取顺序如下:

如果一个slave跟master断开连接已经超过了down-after-milliseconds的10倍,外加master宕机的时长,那么slave就被认为不适合选举为master,计算公式如下:

(down-after-milliseconds * 10) + milliseconds_since_master_is_in_SDOWN_state

接下来会对slave进行排序

  1. 按照slave优先级进行排序,slave-priority越低,优先级就越高;
  2. 如果slave priority相同,那么比较复制偏移量,offset越靠后(越大)则表明和旧的主库数据同步越接近,优先级就越高 ;
  3. 如果上面两个条件都相同,那么选择一个run id最小的从库;

选出从库后,零头哨兵将向从数据库发送SLAVEOF NO ONE命令升级其为新的主库,然后在向其他从库发送SLAVEOF命令将从的主库升级到最新的主库,最后更新内部记录将已经停止的主库更新为新的主库的从库,使得当该故障的主库再次恢复时候自动以从库角色继续提供服务,从启动到故障恢复完成这一些列过程即是哨兵的工作的完整流程也是其原理所在。

领头哨兵选举

  在原理中提及到了,当sentinel发现主库客观下线时候会进行领头哨兵选举进行故障恢复,其选举算法采用Raft算法,这也为什么说其设计思想类似与zookpeer,选举过程大体如下:

  1. 发现主库客观下线的哨兵节点(这里称为A)向每个哨兵节点发送命令要求对方选举自己为领头哨兵(leader);
  2. 如果目标哨兵没有选举过其他人,则同意将A选举为领头哨兵;
  3. 如果A发现有超过半数且超过quorum参数值的哨兵节点同意选自己成为领头哨兵,则A哨兵成功选举为领头哨兵。
  4. 当有多个哨兵节点同时参与领头哨兵选举时,出现没有任何节点当选可能,此时每个参选节点等待一个随机时间进行下一轮选举,直到选出领头哨兵。

配置版本号作用

  同样,在原理介绍时候提及到了master的配置版本号,当一个sentinel被授权后,它将会获得宕掉的master的一份最新配置版本号,当failover执行结束以后,这个版本号将会被用于最新的配置。因为大多数sentinel都已经知道该版本号已经被要执行failover的sentinel拿走了,所以其他的sentinel都不能再去使用这个版本号。这意味着,每次failover都会附带有一个独一无二的版本号。我们将会看到这样做的重要性。

而且,sentinel集群都遵守一个规则:如果sentinel A推荐sentinel B去执行failover,A会等待一段时间后,自行再次去对同一个master执行failover,这个等待的时间是通过failover-timeout配置项去配置的。从这个规则可以看出,sentinel集群中的sentinel不会再同一时刻并发去failover同一个master,第一个进行failover的sentinel如果失败了,另外一个将会在一定时间内进行重新进行failover,以此类推。

sentinel保证了活跃性:如果大多数sentinel能够互相通信,最终将会有一个被授权去进行failover.
sentinel也保证了安全性:每个试图去failover同一个master的sentinel都会得到一个独一无二的版本号。

五、Sentinel状态持久化

  snetinel的状态会被持久化地写入sentinel的配置文件中。每次当收到一个新的配置时,或者新创建一个配置时,配置会被持久化到硬盘中,并带上配置的版本戳。这意味着,可以安全的停止和重启sentinel进程。下面是被重写的配置文件截图:

redis系列--深入哨兵集群

七、配置传播

  一旦一个sentinel成功地对一个master进行了failover,它将会把关于master的最新配置通过广播形式通知其它sentinel,其它的sentinel则更新对应master的配置,一个faiover要想被成功实行,sentinel必须能够向选为master的slave发送SLAVE OF NO ONE命令,然后能够通过INFO命令看到新master的配置信息。

  当将一个slave选举为master并发送SLAVE OF NO ONE`后,即使其它的slave还没针对新master重新配置自己,failover也被认为是成功了的,然后所有sentinels将会发布新的配置信息。

新配在集群中相互传播的方式,就是为什么我们需要当一个sentinel进行failover时必须被授权一个版本号的原因。

每个sentinel使用发布/订阅的方式持续地传播master的配置版本信息,配置传播的发布/订阅管道是:__sentinel__:hello,我们可以通过订阅其频道查看频道中的消息,如下:

redis系列--深入哨兵集群

  因为每一个配置都有一个版本号,所以以版本号最大的那个为标准。例如:假设有一个名为mymaster的地址为10.1.210.69:6379。一开始,集群中所有的sentinel都知道这个地址,于是为mymaster的配置打上版本号1。一段时候后mymaster死了,有一个sentinel被授权用版本号2对其进行failover。如果failover成功了,假设地址改为了10.1.210.69:6380,此时配置的版本号为2,进行failover的sentinel会将新配置广播给其他的sentinel,由于其他sentinel维护的版本号为1,发现新配置的版本号为2时,版本号变大了,说明配置更新了,于是就会采用最新的版本号为2的配置。

八、结束语

  从redis的入门再到哨兵模式,再到平时使用其API进行相关操作,通过一段时间的研究对redis也算有了一定层次的认识,所以把这些过程都记录下来,分享给其他人,希望有更多的人不仅知道如何使用,更能明白其中的原理,在出问题时候能即使的定位问题。当然可能在文章中可能存在不正确的地方也欢迎大家指正,毕竟没有源码级别的理解。最后可能需要研究的部分就是redis的集群,后续在研究完之后写文章介绍,这也算对redis有一个比较全面的认识。