
深入Redis系列(九)Redis高可用之哨兵Sentinel大揭秘

在上一节内容中我们知道Redis主从复制是Redis实现高可用的基础。
所谓高可用说简单点就是当单个Redis节点故障时依旧可以保障整体服务可用,实现方式是通过主从复制创建多个包含相同数据的Redis节点—— [
然而当主节点不可用时,从节点无法执行写命令导致整个Redis服务不可写,由此Redis引入了哨兵模式,将剩余从节点中的一个从节点晋升为主节点,从而维持Redis的读写服务正常。
下面我们正式开始介绍Redis哨兵机制。
一、什么是Redis哨兵,它解决了什么问题,什么场景下会使用到哨兵?
什么是Redis哨兵
Redis哨兵其实就是一个运行在特殊模式下的Redis进程,主从库实例运行的同时,它也在运行。
为什么Redis需要使用哨兵
Redis使用哨兵(Sentinel)主要是为了实现高可用性。在分布式系统中,任何一个组件的故障都可能导致服务的中断,Redis也不例外。虽然Redis支持主从复制来增强数据的冗余性和读性能,但主库故障时的自动故障转移却需要哨兵机制来实现。

哨兵解决了什么问题
1. 自动故障转移:当主库出现故障时,哨兵能够自动检测并触发故障转移过程,选出一个新的主库,保证Redis服务正常。
2. 监控和通知:哨兵不仅监控主库的运行状态,还监控从库以及哨兵集群本身的状态,一旦检测到问题,能够向管理员发送通知。
什么场景下会使用到哨兵
哨兵机制通常在以下场景中被广泛使用。
-
需要高可用性的Redis服务:对于那些不能容忍Redis服务中断的应用,比如实时分析系统、缓存服务、消息队列后端存储等,哨兵机制能够提供主从库的自动切换,保证服务的连续性。
-
多实例部署:在Redis集群中,通常会有多个主从库实例来分散负载和增强数据可靠性。哨兵机制能够有效地管理和监控这些实例,确保它们能够正常工作。
-
云环境或动态基础设施:在云环境或动态变化的基础设施中,云节点可能会因为硬件故障、网络问题或维护活动而频繁重启导致Redis节点暂时不可用。如果刚好重启的是Redis主节点,哨兵机制就能够重新选举主节点确保Redis服务的稳定。
具体场景示例
假设有一个电商平台,它使用Redis作为缓存服务来存储商品的热门信息(如浏览量、评论数等)。为了保证高可用性,Redis被配置为“一主四从”的架构,并使用三个哨兵实例来监控这个主从集群。
一天晚上,由于主库所在的服务器硬件故障,主库突然宕机。在没有哨兵机制的情况下,电商平台可能会因为无法访问Redis缓存服务而遭受严重的性能下降甚至服务中断。
然而,由于配置了哨兵机制,哨兵实例很快检测到了主库的故障,并根据配置的故障转移策略选出了一个从库作为新的主库。同时,哨兵还更新了其他从库的复制目标,使它们开始复制新的主库。这个过程对用户来说是透明的,电商平台继续正常运行,用户可以继续浏览和购买商品。
这个场景展示了哨兵机制在Redis高可用性方面的重要作用。通过自动故障转移和监控通知等功能,哨兵机制能够有效地减少因Redis服务中断而对业务造成的影响。
二、哨兵机制的基本流程是怎么样的?
哨兵机制的基本流程有3个:监控、选主 和 通知;

1. 监控
哨兵进程会周期性地给所有的主从库发送PING命令,检测它们是否仍然在线运行。
如果从库没有在规定时间内响应哨兵的PING命令,哨兵就会把它标记为“下线状态”。
如果主库也没有在规定时间内响应哨兵的PING命令,哨兵就会判定主库下线,然后开始自动切换主库的流程。
在判定主库下线时,涉及到主观下线和客观下线的概念。
主观下线:哨兵发现主库或从库对PING命令的响应超时了,就会先把它标记为“主观下线”。
客观下线:在判断主库是否下线时,不能由一个哨兵说了算,具体来说,当N个哨兵实例中有N/2 +
1个实例判断主库为“主观下线”时,主库才会被标记为“客观下线”,触发主从切换流程。
之所以区分主观下线和客观下线是因为可能存在下线误判,而哨兵的误判一般会发生在集群网络压力较大、网络拥塞,或者是主库本身压力较大的情况下。
2. 选主
所谓选主是指当主节点宕机时,在众多从节点中选择一个当主节点的过程,它分为两部分内容:筛选 + 打分。
筛选:哨兵会按照从库的在 线状态和网络状态
,筛选过滤掉一部分不符合要求的从库,被筛掉的这部分从库无法晋升为主库。
在线状态是指哨兵要确保所选的从库仍然在线运行,这是从库现状良好的基本表现,不在线的从库无法晋升为主库。
网络状态是指哨兵判断从库之前的网络连接状态,如果从库总是和主库断连,且断连次数超出了一定的阈值,则可能表明其网络状况不佳,会被筛掉,无法晋升为主库。
更具体的来说,Redis配置中的 down-after-milliseconds 是我们认定主从库断连的最大连接超时时间。如果在 down-after-
milliseconds 毫秒内,主从节点都没有通过网络联系上,我们就可以认为主从节点断连了。如果发生断连的次数超过了 10
次,就说明这个从库的网络状况不好,不适合作为新主库。
打分: 经过筛选之后,哨兵依次按照优先级、复制进度、ID号大小再对剩余的 从库进行打分,只要有得分最高的从库出现,就把它选为新主库。
打分的具体流程如下。
a. 判断从库的优先级
用户可以通过slave-priority配置项,给不同的从库设置不同优先级。哨兵会给优先级高的从库打高分,如果有一个从库优先级最高,则会被选为新主库。
比如,你有两个从库,它们的内存大小不一样,你可以手动给内存大的实例设置一个高优先级。
如果从库的优先级都一样,那么哨兵会根据从库的复制进度开始第二轮打分。
b. 判断从库的复制进度
这点的判断依据是与旧主库同步程度最接近的从库得分高。这保证了新选出的主库能够拥有最完整的数据。
具体来说, 就是寻找到一个从库它的slave_repl_offset 与主库的 master_repl_offset最接近。如果在所有从库中,有从库的
slave_repl_offset 最接近 master_repl_offset,那么它的得分就最高,可以作为新主库。如下图所示,从库2应该晋升为新主库。

如果所有从库的复制进度都相同,则根据从库的ID号进行第三轮打分。
c.判断从库的ID号
在优先级和复制进度都相同的情况下,ID号最小的从库得分最高,会被选为新主库。这是Redis在选主库时的默认规定。
3. 通知
当选出主库之后,哨兵会执行最后一个任务:通知。
在执行通知任务时,哨兵会把新主库的连接信息发给其他从库,让它们执行 replicaof 命令,和新主库建立连接,并进行数据复制。
同时,哨兵会把新主库的连接信息通知给客户端,让它们把请求操作发到新主库上。
_ 一个重要的思考题 _
在主从切换过程中,客户端能否正常地进行请求操作呢?如果想要应用程序不感知服务的中断,还需要哨兵或需要客户端再做些什么吗?
先不着急看答案,自己思考一下。
下面公布答案:实际上这个问题的答案比较开放,每个人的解决方案都不同,但言之有理即可。
如果客户端使用了读写分离,那么读请求可以在从库上正常执行,不会受到影响。但是由于此时主库已经挂了,而且哨兵还没有选出新的主库,所以在这期间写请求会失败,失败持续的时间
= 哨兵切换主从的时间 + 客户端感知到新主库 的时间。
如果不想让业务感知到异常,客户端只能把写失败的请求先缓存起来或写入消息队列中间件中,等哨兵切换完主从后,再把这些写请求发给新的主库。但这种场景只适合对写入请求返回值不敏感的业务,而且还需要业务层做适配,另外主从切换时间过长,也会导致客户端或消息队列中间件缓存写请求过多,切换完成之后重放这些请求的时间变长。
哨兵检测主库多久没有响应就提升从库为新的主库,这个时间是可以配置的(down-after-
milliseconds参数)。配置的时间越短,哨兵越敏感,哨兵集群认为主库在短时间内连不上就会发起主从切换,这种配置很可能因为网络拥塞但主库正常而发生不必要的切换。当然,当主库真正故障时,因为切换得及时,对业务的影响最小。如果配置的时间比较长,哨兵越保守,这种情况可以减少哨兵误判的概率,但是主库故障发生时,业务写失败的时间也会比较久,缓存写请求数据量越多。
应用程序不感知服务的中断,还需要哨兵和客户端做些什么?
当哨兵完成主从切换后,客户端需要及时感知到主库发生了变更,然后把缓存的写请求写入到新库中,保证后续写请求不会再受到影响,具体做法如下:
哨兵提升一个从库为新主库后,哨兵会把新主库的地址写入自己实例的发布订阅通道pubsub中。客户端需要订阅这个pubsub。这个pubsub有数据时,客户端就能感知到主库发生变更,同时可以拿到最新的主库地址,然后把写请求写到这个新主库即可,这种机制属于哨兵主动通知客户端。
如果客户端因为某些原因错过了哨兵的通知,或者哨兵通知后客户端处理失败了,安全起见,客户端也需要支持主动去获取最新主从的地址进行访问。
所以,客户端需要访问主从库时,不能直接写死主从库的地址了,而是需要从哨兵集群中获取最新的主库地址(通过sentinel get-master-addr-
by-name命令),这样当实例异常时,哨兵切换后或者客户端断开重连,都可以从哨兵集群中拿到最新的实例地址。
一般Redis的SDK都提供了通过哨兵拿到实例地址,再访问实例的方式,我们直接使用即可,不需要自己实现这些逻辑。
三、如果哨兵挂掉了,主从库还如何切换——哨兵集群
为了实现高可用,单点故障问题是必须解决的一个问题,因此Redis的哨兵也不会是一个单节点哨兵,而是部署多个哨兵实例组成哨兵集群共同参与主从库的切换。
实际上,我们在部署哨兵的时候会执行一条这样的命令,假设我们需要部署一个拥有5个哨兵节点的哨兵集群,就需要在5个节点的服务或服务器上执行以下命令。
sentinel monitor <master-name> <ip> <redis-port> <quorum>
这个命令各个参数的含义如下:
这段配置的作用是告诉哨兵实例要监控的主库是哪个,以及如何连接到这个主库。
接下来我们需要考虑哨兵集群的几个关键问题。
1.哨兵集群的哨兵之间如何相互发现和连接;
2.哨兵集群如何发现他们所需要监控的从节点;
3.主从切换之后,客户端如何知道新主库的IP和地址;
3.主从切换时,由哨兵集群中的哪一个哨兵执行主从切换行为;
哨兵集群的哨兵之间如何相互发现和连接?
答:通过发布订阅功能。在Redis主节点上,存在一个特定的频道“sentinel:hello”,哨兵节点通过与Redis主节点建立连接,并在主节点的这个频道上发布自己的连接信息(如IP地址和端口号)。同时,哨兵节点也可以从这个频道订阅消息,以获取其他哨兵发布的连接信息。
当多个哨兵节点都在主节点上进行了发布和订阅操作后,它们之间就能知道彼此的IP地址和端口,从而实现相互发现。

上面这张图描述了哨兵2和哨兵3与哨兵1建立连接的过程。
哨兵2和哨兵3之间建立连接的话,还需要哨兵2或哨兵3向主库的 sentinel:hello频道发送自己的IP和端口才行。
哨兵除了彼此之间建立起连接形成集群外,还需要和从库建立连接。这是因为,在哨兵的监控任务中,它需要对主从库都进行心跳判断,而且在主从库切换完成后,它还需要通知从库,让它们和新主库进行同步。
于是这就引出了下一个问题。
哨兵集群如何发现他们所需要监控的从节点?
答: 哨兵与主库建立了连接 之后 ,哨兵会向主库发送INFO命令。
主库接收到INFO命令后,会将从库列表返回给哨兵。该从库列表包含了从库的连接信息。哨兵根据从库列表中的连接信息,与每个从库建立连接,并在这些连接上持续地对从库进行监控。
如下图所示:

通过 pub/sub 机制,哨兵之间可以组成集群,同时,哨兵又通过 INFO 命令,获得了从库连接信息,也能和从库建立连接,并进行监控了。
不过哨兵不能只和主、从库连接,还需要和客户端建立联系。因为主从库切换后,客户端也需要知道新主库的连接信息,才能向新主库发送请求操作。所以,哨兵还需要完成把新主库的信息告诉客户端这个任务。
于是这就引出了下一个问题。
主从切换之后,客户端如何知道新主库的IP和地址?
答:还是可以依赖发布订阅频道,客户端可以订阅哨兵集群的+switch-
master频道,当发生主从切换的时候,哨兵会将新主库的IP和端口通过这个频道发送给客户端。
除了这个频道之外,哨兵节点还创建了很多其他频道供客户端了解主从节点的实时状态,下面是一些重要的频道。

如果需要让客户端监听所有频道,可以在客户端执行
PSUBSCRIBE *
通过发布订阅机制,哨兵和哨兵之间、哨兵和从库之间、哨兵和客户端之间就都能建立起连接了,再加上主库下线判断和选主依据,哨兵集群的监控、选主和通知三个任务就基本可以正常工作了。
不过,我们还需要思考一个问题。主库故障以后,哨兵集群有多个实例,怎么确定由哪个哨兵来进行主从切换?
主从切换时,由哨兵集群中的哪一个哨兵执行主从切换行为?
答:哨兵集群会先选举出一个领头哨兵,再由这个领头哨兵执行主从切换。
当一个主服务器被判断为客观下线时,监视主服务器的各个哨兵会进行协商,选举出一个领头 哨兵 ,并由领头 哨兵 对下线主服务器执行故障转移操作。
以下是Redis选举领头哨兵的规则和方法。
所有在线的哨兵都有被选为领头哨兵的资格,换句话说,监视同一个主服务器的多个在线哨兵中的任意一个都有可能成为领头哨兵。
每个发现主服务器进入客观下线的哨兵都会要求其他哨兵将自己设置为局部领头 哨兵。
当一个哨兵(源哨兵)向另一个哨兵(目标哨兵)发送哨兵 is-master-down-by-
addr命令,这表示源哨兵要求目标哨兵将前者设置为后者的局部领头哨兵。
哨兵设置局部领头哨兵的规则是先到先得:最先向目标哨兵发送设置要求的源哨兵将成为目标哨兵的局部领头哨兵,而之后其他源哨兵发出的选举请求将被目标哨兵拒绝。
目标哨兵在接收到哨兵is-master-down-by-
addr命令之后,将向源哨兵返回一条命令回复,表明这个源哨兵将认你为领头哨兵,或者表明我这个源哨兵已经认其他哨兵为领头哨兵了。这就类似于选举投票,源哨兵的回复相当于是在向目标哨兵投出了自己的一票。
当然,目标节点在发送选举请求之前会先给自己投一票。
如果有某个哨兵被半数以上的哨兵设置成了局部领头哨兵,那么这个哨兵成为领头哨兵。
举个例子,在一个由10个哨兵组成的哨兵系统里面,只要有大于等于10/2+1=6个哨兵将某个哨兵设置为局部领头哨兵,那么被设置的那个哨兵就会成为领头哨兵。
哨兵成为领头哨兵所需的票数可以由
如果在给定时限内,没有一个哨兵被选举为领头哨兵,那么各个哨兵将在一段时间之后再次进行选举,直到选出领头哨兵为止。
需要注意的是如果哨兵集群只有 2 个实例,此时,一个哨兵要想成为 Leader,必须获得 2 票,而不是 1
票。所以,如果有个哨兵挂掉了,此时的集群是无法进行主从库切换的。因此通常我们至少会配置 3 个哨兵实例。这一点很重要,在实际应用时可不能忽略。
最后需要提醒的是,哨兵集群的实例数量不能配置太多。
哨兵在判定“主观下线”和选举“哨兵领导者”时,都需要和其他节点进行通信,交换信息,哨兵实例越多,通信的次数也就越多,而且部署多个哨兵时,会分布在不同机器上,节点越多带来的机器故障风险也会越大。这些问题都会影响到哨兵的通信和选举,出问题时也就意味着选举时间会变长,切换主从的时间变久。
在实际应用中,部署三个哨兵实例是一个常见的选择,因为这样可以满足基本的故障检测和自动切换需求,同时保持系统的简洁性。
如果希望进一步提升判断准确率,可以适当增加哨兵实例的数量,例如使用五个哨兵实例。但需要注意的是,过多的哨兵实例可能会增加系统的复杂性和通信开销。
至此,Redis的高可用实现就介绍完毕啦,下一节我们聊聊Redis的分布式切片集群。
如果本文对大家有帮助,麻烦大家动动小手点个免费的“赞”或“在看”,大家的鼓励就是阿沛持续更新的动力~

往期精彩回顾
深入Redis系列(五)Redis事件机制详解 IO多路复用、文件事件、时间事件、reactor模式
深入Redis系列(四)Redis Stream轻量级消息队列详解
深入Redis系列(三)redis持久化之RDB快照持久化和AOF日志持久化
