Redis原理之哨兵
主从库集群模式下,如果从库发生故障,客户端可以继续向主库或其他从库发起请求,进行相关操作。但是如果主库发生故障,就会直接影响从库的同步,和写操作。因此需要能够有一种机制,实现主从库自动切换,就是哨兵机制。
1. 哨兵机制的基本流程
哨兵就是一个运行在特殊模式下的Redis进程,主从库实例运行的同时,它也在运行。哨兵主要负责就是三个任务:监控、选主(选择主库)和通知。
-
监控:是指哨兵进行在运行时,周期性的给所有的主从库发送PING命令,检测它们是否仍然在线运行。如果从库没有在规定时间内响应哨兵的PING命令,哨兵就会把它标记为
下线状态
;同样,如果主库没有响应PING命令,也会被判定主库下线。 -
选主:主库挂了之后,哨兵需要从多个从库中,按照一定的规则选择一个从库实例,执行
slaveof no one
命令,把它作为新的主库。 -
通知:哨兵会把新主库的连接信息发送给其他从库,让它们执行slaveof(或replicaof)命令,和新主库建立连接,并进行数据复制。同时,哨兵会把新主库的连接信息通知给客户端,让它们把请求发送到新主库上。并且会将原来的主库,更新为从库,并保持对其监控;当旧的主库恢复后,哨兵会命令它去复制新的主库。
哨兵机制三项任务与目标 ,如下图所示:
2. 监控
哨兵通过三个定时监控任务,来完成对各个节点的发现和监控。
1)每隔10秒 ,每个哨兵会向主库和从库 发送INFO
命令,获取最新的拓扑结果。哨兵对命令结果进行解析就可以找到相应的从库。这个定时任务作用具体有以下几点:
- 通过向主库执行
info
命令,获取从库的信息,这也是为什么不需要配置从库信息。 - 当有新的从库加入时,可以立刻感知出。
- 节点不可达或故障转移后,可以通过Info命令实时更新节点拓扑信息。
2)每隔2秒 ,每个哨兵节点会向主库、从库和其他哨兵 的__sentinel__:hello
频道上,发送该哨兵节点中主库的信息,以及当前哨兵节点的信息。同时每个哨兵节点也会订阅该频道(订阅每一个主库、从库,通过重写publishCommand命令),来了解其他哨兵节点以及它们对主库的判断。这个定时任务可以完成两个工作:(命令格式:<sentinel_ip>,<sentinel_port>,<sentinel_runid>,<sentinel_epoch>,<master_name>,<master_ip>,<master_port>,<master_epoch>)
- 发现新的哨兵节点,通过订阅主库的
__sentinel__:hello
了解其他哨兵节点的信息。 - 哨兵节点了解其他哨兵对主库的判断。如果出现了主库切换,那么可以从这里得知。
3)每隔1秒 ,每个哨兵节点会向主库、从库和其他哨兵发送一条ping命令,做一次心跳检测,来确认这些节点当前是否可达。
每个哨兵会对主库、从库、其他哨兵创建连接。其中会对主库和从库创建命令连接和消息连接 ,对其他哨兵只会创建命令连接 。命令连接用于发送命令并接收命令回复,消息连接用于订阅__sentinel__:hello
频道。
连接都是长连接。只是哨兵特地说明下,需要和其他哨兵建立连接。至于为什么要建立两条连接,主要做通道隔离,详细可以看《redis设计与实现》,16.1.5中的说明
3. 选主
3.1 主观下线和客观下线
哨兵进程会使用PING命令检测它自己和主、从库的网络情况,用来判断实例的状态。如果发现主、从库没有响应PING命令(超过配置项:down-after-millseconds没有回复,默认值30秒),则哨兵就会先把它标记为主观下线。
如果检测的是从库,那么只要简单标记为主观下线即可,因为从库的下线影响一般不大。但是如果检测的是主库,除了标记为主观下线外,还不能开启主从切换,因为可能存在误判:主库实际并没有下线,但是哨兵误以为它下线了。在集群网络压力较大、网络拥塞、或者主库压力大的时候有可能出现误判。
为了减少误判,通常哨兵会采用多实例组成的集群模式进行部署,也称为哨兵集群。引入多个哨兵实例一起判断,就可以避免单个哨兵因为自身网络状况不好,而误判主库下线的情况。在大多数的哨兵实例,都判断主库已经主观下线了,那么主库就会被标记为客观下线。
判断的原则是:少数服从多数。当有N个哨兵实例时,最好要有个(可以配置)实例判断主库为主观下线。
具体为:当哨兵主观下线的节点是主库时,该哨兵会通过is-master-down-by-addr命令,向其他哨兵询问对主库的状态,当超过(根据配置)个数的哨兵认为主库确实有问题,该哨兵节点会做出客观下线的决定。
例如:默认quorum配置为2,此时有3个哨兵,那么一个哨兵需要2个哨兵认为"主观下线",就可以标记主库为"客观下线"了,这个两个哨兵包括自己,和另外一个哨兵。
3.2 哪个哨兵执行主从切换?
当哨兵发现主库为客观下线时,多个哨兵之间会选举出一个Leader。确定哪个哨兵执行主从切换的过程,和主库"客观下线"的判断过程一样,是一个"投票仲裁"的过程。当发现主库客观下线时,这个哨兵就可以给其他哨兵发送is-master-down-by-addr命令(数据不一样,最后一个runId为哨兵的runId),表明希望由自己来执行主从切换,并让所有其他哨兵进行投票。这个投票过程称为"Leader选举"。因为最终执行主从切换的哨兵称为Leader,投票过程就是确定Leader。
在投票过程,任何一个想成为Leader的哨兵,需要满足两个条件:
-
拿到半数以上的赞成票。(N/2 + 1)
-
拿到的票数同时,还需要大于等于哨兵配置文件中quorum的值。默认quorum为2,3个哨兵实例,那么任何一个想成为Leader的哨兵只要拿到2张赞成票就可以了。
另外,如果在一轮投票中没有产生Leader,哨兵集群会等待一段时间(默认下为10s。min(10s,failover-timeout)),再重新选举,这是因为,哨兵集群能够进行成功投票,很大程度依赖于选举命令能正常网络传播。如果网络压力比较大,或有阻塞,就可能导致没有一个哨兵能拿到半数以上的赞成票。所以等到网络拥塞好转后,在进行投票选举,成功的概率可就会增加。
还有一点需要注意,如果哨兵集群只有2个实例,此时,一个哨兵要想成为Leader,必须获得2票,而不是1票。所以如果有个哨兵挂掉了,此时集群无法进行主从库切换,因此,至少会配置3个哨兵实例。
3.3 如何选新的主库?
可以把哨兵选择新主库的过程,称为:筛选 + 打分。从多个从库中,先按照一定的筛选条件,把不符合条件的从库过滤掉,然后再按照一定的规则,给剩下的从库打分,将得分最高的从库选为新主库。
筛选条件:首先肯定要保证从库仍然在线运行,还要判断它之前的网络连接状态(防止成为新主库后,网络出现故障,又要重新选主)。如果从库和主库断连时间过长,就可以认为这个从库的网络状况不太好,可以过滤掉。
打分:按照三个规则一次进行三轮打分。分别是从库优先级、从库复制进度以及从库ID号。主要在某一轮中,有从库得分最高,那么它就是新主库,选主过程结束。如果没有出现得分最高的从库,那么就继续进行下一轮。
第一轮:优先级最高的从库得分高。
用户可以通过slave-priority配置项,给不同的从库设置不同优先级。在选主时,哨兵会给优先级最高的从库打高分,如果有一个从库优先级最高,那么它就是新从库了。如果从库优先级一样,那么开始第二轮打分。
第二轮:和旧主库同步程度最接近的从库得分高。
如果选择和旧主库同步最接近的那个从库作为主库,那么新主库上就有最新的数据。那么如何判断从库和旧主库的同步进度呢?在主从同步时有提到,主库会用master_repl_offset记录当前最新写操作在repl_backlog_buffer中的位置,而从库会用slave_repl_offset这个值记录当前的复制进度。所以,要找的从库,它的slave_repl_offset需要最接近master_repl_offset(实际代码中是在从库中找slave_repl_offset最大的)
第三轮:ID号小的从库得分高。
每个实例都会有一个ID,这个ID就类似于这里的从库编号。目前Redis在选主库时,有一个默认规定:在优先级和复制进度都相同的情况下,ID号最小的从库得分最高,会被选为新主库。
4. 通知
在配置哨兵信息时,只需要用到下面配置项,设置主库的IP和端口,并没有配置其他哨兵的连接信息。哨兵一般也是集群部署,内部会有机制能够相互发现。
xml
sentinel monitor <master-name> <ip> <redis-port> <quorum>
4.1 基于pub/sub机制的哨兵集群组成
哨兵之间可以相互发现,要归功于Redis提供的pub/sub机制。哨兵只要和主库建立连接,就可以在主库上发布消息。同时也可以在主库上订阅消息,获取其他哨兵发布的连接信息。当多个哨兵实例都在主库上做了发布和订阅操作后,它们之间就能知道彼此的IP地址和端口。
哨兵通过_sentinel_:hello频道,来相互发现和通信。
哨兵向主库发送INFO命令来获取从库的IP地址和端口。
通过pub/sub机制,哨兵之间可以组成集群,同时哨兵又通过INFO命令,获得从库连接信息,也可以和从库建立连接,进行监控。但哨兵不能只和主从库连接,因为主从库切换后,客户端也需要知道新主库的连接信息,才能向新主库发送请求。所以哨兵还需要把新主库的信息告诉客户端。
4.2 基于pub/sub机制的客户端事件通知
从本质上来说,哨兵就是一个运行在特定模式下的Redis实例,只不过它并不服务请求操作,只是完成监控、选主和通知的任务。所以,每个哨兵实例也提供pub/sub机制,客户端可以从哨兵订阅消息。哨兵提供的消息订阅频道有很多,包含了主从库切换过程中的不同关键事件。以下为主要事件:
客户端读取哨兵的配置文件后,可以获得哨兵的地址和端口,和哨兵建立网络连接。然后可以在客户端执行订阅命令,来获取不同的事件消息。有了这些事件,客户端不仅可以知道主从切换后新主库的连接信息,还可以知道主从切换到了哪一步,有助于了解切换进度。
4.3 Jedis SentinelPool库如何实现主从切换
1)先通过哨兵的sentinel get-master-addr-by-name 命令获取主库的地址。
2)为每个哨兵节点开启主库切换的监控线程。内部会订阅sentinel的+switch-master 频道,当出现主从切换的时候,就知道新的主库了。
2)会为每个sentinel节点开启主节点切换的监控线程。内部会订阅sentinel的+switch-master 频道,当出现主从切换的时候,就知道新的Master了。
5. Sentinel配置
5.1 monitor
xml
sentinel monitor <master-name> <ip> <port> <quorum>
哨兵要监控主库,所以配置上必然要知道监控哪个节点。该配置表示要监控一个名字叫,ip地址和端口号为的主库。代表判定主库最终不可达所需要的票数。哨兵能够从主库中,获取从库和其余哨兵节点的信息。
用于故障发现和判定,如将quorum配置为2,说明至少要有2个哨兵节点认为主库不可达,才会判定为客观下线。
同时还与哨兵节点的领导者选举有关,至少要有max(quorum,(num(sentinels)/2 + 1)) 个哨兵节点参与选举,才能选主领导者哨兵。
比如有5个哨兵,quorum为2,哨兵挂了3个。这个时候主库挂掉,虽然可以判定为客观下线,但是哨兵已经没有办法选出一个Leader进行故障转移。
5.2 down-after-milliseconds
xml
sentinel down-after-milliseconds <master-name> <times> 默认30s
每个哨兵节点都要通过定期发送ping命令来判断Redis数据节点与其余哨兵节点是否可达,如果超过down-after-milliseconds配置的时间没有回复,则判断节点不可达。down-after-milliseconds虽然以为参数,但实际上对哨兵节点、主库、从库的失败判定同时有效。
down-after-milliseconds值越大,代表哨兵节点对节点不可达条件判断越宽松,条件宽松会导致应用方需要等待故障转移的时间越长、条件严格虽然可以及时发现并完成故障转移,但也存在一定的误判率。
5.3 failover-timeout
xml
sentinel failover-timeout <master-name> <times> 默认180秒
failover-timeout通常被解释成故障转移超时时间,当实际上它作用故障转移的各个阶段。
a)选出合适从库。
b)晋升选出的从库为主库。
c)命令其余从库复制新的主库。
d)等待原主库恢复后,命令它去复制新的主库。
failover-timeout作用体现以下四方面:
1)如果哨兵对一个主库故障转移失败,那么下次再对该主库做故障转移的起始时间是failover-timeout的2倍。
2)在b)阶段,如果哨兵节点向a)阶段选出的从库执行 slaveof no one
一直失败(如该从库此刻出现故障),当此过程超过failover-timeout时,则故障转移失败。
3)在b)阶段如果执行成功,哨兵节点还会执行info
命令来确认a)阶段选出来的从库确实晋升为主库,如果此过程执行时间超过failover-timeout时,则故障转移失败。
4)如果c)阶段执行时间超过failover-timeout(不包含复制时间),则故障转移失败。注意即使超过这个时间,哨兵节点也会最终配置从库去同步最新的主库。
6. 参考资料
1)《Redis核心技术与实战》------极客时间
2)《Redis开发与运维》
3)《Redis设计与实现》