什么是 Sentinel

Sentinel(哨兵) 只是 Redis 的一种运行模式,不提供读写服务,默认运行在 26379 端口上,依赖于 Redis 工作。Redis Sentinel 的稳定版本是在 Redis 2.8 之后发布的。

Redis 在 Sentinel 这种特殊的运行模式下,使用专⻔的命令表,也就是说普通模式运行下的 Redis 命令将无法使用。

通过下面的命令就可以让 Redis 以 Sentinel 的方式运行:

redis-sentinel /path/to/sentinel.conf
或者
redis-server /path/to/sentinel.conf --sentinel

Redis 源码中的 sentinel.conf 是用来配置 Sentinel 的,一个常⻅的最小配置如下所示:

// 指定要监视的 master
// 127.0.0.1 6379 为 master 地址
// 2 表示当有 2 个 sentinel 认为 master 失效时,master 才算真正失效
sentinel monitor mymaster 127.0.0.1 6379 2
// master 节点宕机多⻓时间才会被 sentinel 认为是失效
sentinel down-after-milliseconds mymaster 60000
entinel failover-timeout mymaster 180000
sentinel parallel-syncs mymaster 1

sentinel monitor resque 192.168.1.3 6380 4
sentinel down-after-milliseconds resque 10000
sentinel failover-timeout resque 180000
// 在发生主备切换时最多可以有 5 个 slave 同时对新的 master 进行同步
sentinel parallel-syncs resque 5

Redis Sentinel 实现 Redis 集群高可用,只是在主从复制实现集群的基础下,多了一个 Sentinel ⻆色来帮助我们监控 Redis 节点的运行状态并自动实现故障转移。

Sentinel 作用

监控、选主、通知,

根据 Redis Sentinel 官方文档的介绍,sentinel 节点主要可以提供 4 个功能:

  • 监控:监控所有 redis 节点(包括 sentinel 节点自身)的状态是否正常。指哨兵运行时,定时给所有库发送 ping。从库未响应,则标记下线状态。主库未响应,则标记下线状态,同时自动切换主库。
  • 选主: 即选择一个从库作为主库。通知,即将新主库信息发送给其他从库,让其执行 replicaof,与新主库建立连接,同时数据复制;同时通知客户端发送到主库上。
  • 通知:通知 slave 新的 master 连接信息,让它们执行 replicaof 成为新的 master 的 slave。
  • 配置提供 :客户端连接 sentinel 请求 master 的地址,如果发生故障转移, sentinel 会通知新的 master 链接信息给客户端。

Redis Sentinel 本身设计的就是一个分布式系统,建议多个 sentinel 节点协作运行。这样做的好处是:

  • 多个 sentinel 节点通过投票的方式来确定 sentinel 节点是否真的不可用,避免误判
  • Sentinel 自身就是高可用。

如果想要实现高可用,建议将哨兵 Sentinel 配置成单数且大于等于 3 台。

Sentinel 如何检测节点下线

相关的问题:

  • 主观下线与客观下线的区别?
  • Sentinel 是如何实现故障转移的?
  • 为什么建议部署多个 sentinel 节点(哨兵集群)?

Redis Sentinel 中有两个下线(Down)的概念:

  • 主观下线(SDOWN) :sentinel 节点认为某个 Redis 节点已经下线了(主观下线),但还不是很确定,需要其他 sentinel 节点的投票。
  • 客观下线(ODOWN) :法定数量(通常为过半)的 sentinel 节点认定某个 Redis 节点已经下线(客观下线),那它就算是真的下线了。

也就是说,主观下线当前的 sentinel 自己认为节点宕机,客观下线是 sentinel 整体达成一致认为节点宕机。

每个 sentinel 节点以每秒钟一次的频率向整个集群中的 master、slave 以及其他 sentinel 节点发送一个 PING 命令。

如果对应的节点超过规定的时间(down-after-millisenconds)没有进行有效回复的话,就会被其认定为是主观下线(SDOWN) 。注意!这里的有效回复不一定是 PONG,可以是-LOADING 或者 -MASTERDOWN。

如果被认定为主观下线的是 slave 的话, sentinel 不会做什么事情,因为 slave 下线对 Redis 集群的影响不大,Redis 集群对外正常提供服务。但如果是 master 被认定为主观下线就不一样了,sentinel 整体还要对其进行进一步核实,确保 master 是真的下线了。

所有 sentinel 节点要以每秒一次的频率确认 master 的确下线了,当法定数量 (通常为过半)的 sentinel 节点认定 master 已经下线, master 才被判定为客观下线(ODOWN) 。这样做的目的是为了防止误判,毕竟故障转移的开销还是比较大的,这也是为什么 Redis 官方推荐部署多个 sentinel 节点(哨兵集群)。

随后, sentinel 中会有一个 Leader 的⻆色来负责故障转移,也就是自动地从 slave 中选出一个新的 master 并执行完相关的一些工作(比如通知 slave 新的 master 连接信息,让它们执行 replicaof 成为新的 master 的 slave)。

如果没有足够数量的 sentinel 节点认定 master 已经下线的话,当 master 能对 sentinel 的 PING 命令进行有效回复之后,master 也就不再被认定为主观下线,回归正常。

Sentinel 如何选主

先按照一定筛选条件,再按照一定规则对剩余从库进行打分。

筛选条件:处于在线状态(相较下线),且主从断连不超过十次(网络)。down-after-milliseconds 配置来认定主从库断连的最大超时时间。

ƒ打分规则:从库优先级(slave-priority 配置)、从库复制进度(backlog slave_repl_offset)、从库 ID。按顺序打分,当有一轮有从库分最高,那么直接认定为新主库。其中,从库复制进度是查看从库 offset 和主库 offset 接近情况。

Sentinel 如何选 Leader

选择 Leader 的作用 —— 切换 Redis 主库。

在投票过程中,任何一个想成为 Leader 的哨兵,要满足两个条件:第一,拿到半数以上的赞成票;第二,拿到的票数同时还需要大于等于哨兵配置文件中的 quorum 值。以 3 个哨兵为例,假设此时的 quorum 设置为 2,那么,任何一个想成为 Leader 的哨兵只要拿到 2 张赞成票,就可以了。

分布式共识:Raft

哨兵互相发现的原理

在配置哨兵的信息时,我们只需要用到下面的这个配置项,设置主库的 IP 和端口,并没有配置其他哨兵的连接信息。

sentinel monitor <master-name> <ip> <redis-port> <quorum> 

这些哨兵实例既然都不知道彼此的地址,又是怎么组成集群的呢?要弄明白这个问题,我们就需要学习一下哨兵集群的组成和运行机制了。

哨兵实例之间可以相互发现,要归功于 Redis 提供的 pub/sub 机制,也就是发布 / 订阅机制。

哨兵只要和主库建立起了连接,就可以在主库上发布消息了,比如说发布它自己的连接信息(IP 和端口)。同时,它也可以从主库上订阅消息,获得其他哨兵发布的连接信息。当多个哨兵实例都在主库上做了发布和订阅操作后,它们之间就能知道彼此的 IP 地址和端口。

除了哨兵实例,我们自己编写的应用程序也可以通过 Redis 进行消息的发布和订阅。所以,为了区分不同应用的消息,Redis 会以频道的形式,对这些消息进行分门别类的管理。所谓的频道,实际上就是消息的类别。当消息类别相同时,它们就属于同一个频道。反之,就属于不同的频道。只有订阅了同一个频道的应用,才能通过发布的消息进行信息交换。

在主从集群中,主库上有一个名为“__sentinel__:hello”的频道,不同哨兵就是通过它来相互发现,实现互相通信的。

我来举个例子,具体说明一下。在下图中,哨兵 1 把自己的 IP(172.16.19.3)和端口(26579)发布到“__sentinel__:hello”频道上,哨兵 2 和 3 订阅了该频道。那么此时,哨兵 2 和 3 就可以从这个频道直接获取哨兵 1 的 IP 地址和端口号。

然后,哨兵 2、3 可以和哨兵 1 建立网络连接。通过这个方式,哨兵 2 和 3 也可以建立网络连接,这样一来,哨兵集群就形成了。它们相互间可以通过网络连接进行通信,比如说对主库有没有下线这件事儿进行判断和协商。

400

哨兵除了彼此之间建立起连接形成集群外,还需要和从库建立连接。这是因为,在哨兵的监控任务中,它需要对主从库都进行心跳判断,而且在主从库切换完成后,它还需要通知从库,让它们和新主库进行同步。

那么,哨兵是如何知道从库的 IP 地址和端口的呢?

这是由哨兵向主库发送 INFO 命令来完成的。就像下图所示,哨兵 2 给主库发送 INFO 命令,主库接受到这个命令后,就会把从库列表返回给哨兵。接着,哨兵就可以根据从库列表中的连接信息,和每个从库建立连接,并在这个连接上持续地对从库进行监控。哨兵 1 和 3 可以通过相同的方法和从库建立连接。

400

你看,通过 pub/sub 机制,哨兵之间可以组成集群,同时,哨兵又通过 INFO 命令,获得了从库连接信息,也能和从库建立连接,并进行监控了。

但是,哨兵不能只和主、从库连接。因为,主从库切换后,客户端也需要知道新主库的连接信息,才能向新主库发送请求操作。所以,哨兵还需要完成把新主库的信息告诉客户端这个任务。

而且,在实际使用哨兵时,我们有时会遇到这样的问题:如何在客户端通过监控了解哨兵进行主从切换的过程呢?比如说,主从切换进行到哪一步了?这其实就是要求,客户端能够获取到哨兵集群在监控、选主、切换这个过程中发生的各种事件。

客户端和哨兵信息同步的方式

依赖 pub/sub 机制,来帮助我们完成哨兵和客户端间的信息同步。

从本质上说,哨兵就是一个运行在特定模式下的 Redis 实例,只不过它并不服务请求操作,只是完成监控、选主和通知的任务。所以,每个哨兵实例也提供 pub/sub 机制,客户端可以从哨兵订阅消息。哨兵提供的消息订阅频道有很多,不同频道包含了主从库切换过程中的不同关键事件。

频道有这么多,一下子全部学习容易丢失重点。为了减轻你的学习压力,我把重要的频道汇总在了一起,涉及几个关键事件,包括主库下线判断、新主库选定、从库重新配置。

400

知道了这些频道之后,你就可以让客户端从哨兵这里订阅消息了。具体的操作步骤是,客户端读取哨兵的配置文件后,可以获得哨兵的地址和端口,和哨兵建立网络连接。然后,我们可以在客户端执行订阅命令,来获取不同的事件消息。

举个例子,你可以执行如下命令,来订阅“所有实例进入客观下线状态的事件”:

SUBSCRIBE +odown

当然,你也可以执行如下命令,订阅所有的事件:

PSUBSCRIBE  *

当哨兵把新主库选择出来后,客户端就会看到下面的 switch-master 事件。这个事件表示主库已经切换了,新主库的 IP 地址和端口信息已经有了。这个时候,客户端就可以用这里面的新主库地址和端口进行通信了。

switch-master <master name> <oldip> <oldport> <newip> <newport>

有了这些事件通知,客户端不仅可以在主从切换后得到新主库的连接信息,还可以监控到主从库切换过程中发生的各个重要事件。这样,客户端就可以知道主从切换进行到哪一步了,有助于了解切换进度。

好了,有了 pub/sub 机制,哨兵和哨兵之间、哨兵和从库之间、哨兵和客户端之间就都能建立起连接了,再加上我们上节课介绍主库下线判断和选主依据,哨兵集群的监控、选主和通知三个任务就基本可以正常工作了。不过,我们还需要考虑一个问题:主库故障以后,哨兵集群有多个实例,那怎么确定由哪个哨兵来进行实际的主从切换呢?

Sentinel 防止脑裂吗

如果 M1 和 R2、R3 之间的网络被隔离,也就是发生了脑裂, M1 和 R2、R3 隔离在了两个不同的网络分区中。这意味着,R2 或者 R3 其中一个会被选为 master,这里假设为 R2。

但是!这样会出现问题了!!

如果客户端 C1 是和 M1 在一个网络分区的话,从网络被隔离到网络分区恢复这段时间,C1 写入 M1 的数据都会丢失,并且,C1 读取的可能也是过时的数据。这是因为当网络分区恢复之后,M1 将会成为 slave 节点。

想要解决这个问题的话也不难,对 Redis 主从复制进行配置即可。

下面对这两个配置进行解释:

  • min-replicas-to-write 1:用于配置写 master 至少写入的 slave 数量,设置为 0 表示关闭该功能。3 个节点的情况下,可以配置为 1 ,表示 master 必须写入至少 1 个 slave ,否则就停止接受新的写入命令请求。
  • min-replicas-max-lag 10 :用于配置 master 多⻓时间(秒)无法得到从节点的响应,就认为这个节点失联。我们这里配置的是 10 秒,也就是说 master 10 秒都得不到一个从节点的响应,就会认为这个从节点失联,停止接受新的写入命令请求。

不过,这样配置会降低 Redis 服务的整体可用性,如果 2 个 slave 都挂掉, master 将会停止接受新的写入命令请求。

主库挂了,客户端能正常请求?应用程序无感知需要怎么做?

读操作正常,写操作失败。

  • 一方面,不感知的情况下,客户端缓存应用的写操作。由于 Redis 应用场景一般不会有同步写,写请求求通常不会在应用程序的关键路径上,所以,客户端缓存写请求后,给应用程序返回一个确认就行。
  • 另一方面,主从切换完成后,客户端要能和新主库重新建立连接,哨兵需要提供订阅频道,让客户端能够订阅到新主库的信息。同时,客户端也需要能主动和哨兵通信,询问新主库的信息。

问题 1:在主从切换过程中,客户端能否正常地进行请求操作呢?

主从集群一般是采用读写分离模式,当主库故障后,客户端仍然可以把读请求发送给从库,让从库服务。但是,对于写请求操作,客户端就无法执行了。

问题 2:如果想要应用程序不感知服务的中断,还需要哨兵或客户端再做些什么吗?

一方面,客户端需要能缓存应用发送的写请求。只要不是同步写操作(Redis 应用场景一般也没有同步写),写请求通常不会在应用程序的关键路径上,所以,客户端缓存写请求后,给应用程序返回一个确认就行。

另一方面,主从切换完成后,客户端要能和新主库重新建立连接,哨兵需要提供订阅频道,让客户端能够订阅到新主库的信息。同时,客户端也需要能主动和哨兵通信,询问新主库的信息。