Redis Sentinel 是 Redis 的高可用性解决方案:由一个或多个 Sentinel 实例组成的 Sentinel 系统可以监视任意多个主节点,以及这些主节点属下的所有从节点,并在被监视的主节点进入下线状态时,自动将下线主节点属下的某个从节点升级为新的主节点,然后由新的主节点代替已下线的主节点继续处理命令请求。另外,Sentinel 还会继续监视已下线的节点,并在它重新上线时,将它设置为新的主节点的从节点。
启动一个 Sentinel 可以使用如下命令:
redis-sentinel /path/to/your/sentinel.conf
# 或者
redis-server /path/to/your/sentinel.conf --sentinel
这两个命令的效果是完全相同的。我们只需在 sentinel.conf 配置文件中设置主库的 IP 和端口即可:
sentinel monitor <master-name> <ip> <redis-port> <quorum>
哨兵执行流程
其实 Sentinel 本质上只是一个运行在特殊模式下的 Redis 服务器,所以启动 Sentinel 实例就是初始化一个普通的 Redis 服务器。不过,因为 Sentinel 执行的工作和普通 Redis 服务器执行的工作不同,所以 Sentinel 的初始化过程和普通 Redis 服务器的初始化过程并不完全相同。例如,普通服务器在初始化时会通过载入 RDB 文件或者 AOF 文件来还原数据库状态,但因为 Sentinel 并不使用数据库,所以初始化时就不会载入 RDB 或者 AOF 文件。此外,Sentinel 节点还使用单独的命令表,对外提供有限的命令查询。
1. 获取主从节点信息
Sentinel 默认会以每十秒一次的频率向被监视的主服务器发送 INFO 命令,并通过分析 INFO 命令的回复来获取主服务器的当前信息。通过分析主服务器返回的 INFO 命令回复,Sentinel 不仅可以获取主服务器本身的信息,例如 run_id,还能够获取到主服务器属下所有从服务器的信息,例如从服务器的 ip、port。根据这些 IP 地址和端口号,Sentinel 无需用户提供从服务器的地址信息,就可以自动发现从服务器。
就像下图所示,哨兵 2 给主库发送 INFO 命令,主库接受到这个命令后,就会把从库列表返回给哨兵。接着,哨兵就可以根据从库列表中的连接信息,和每个从库建立连接,并在这个连接上持续地对从库进行监控。哨兵 1 和 3 可以通过相同的方法和从库建立连接。
2. 发送接收频道信息
在默认情况下,Sentinel 会以每两秒一次的频率向所有被监视的主服务器和从服务器发送以下格式的命令:
PUBLISH __sentinel__:hello <s_ip>,<s_port>,<s_runid>,<s_epoch>,<m_name>,<m_ip>,<m_port>,<m_epoch>
这条命令向服务器的 sentinel:hello 频道发送了一条信息。其中以 s 开头的参数记录的是 Sentinel 本身的信息,而 m 开头的参数记录的则是主服务器的信息,如果 Sentinel 正在监视的是主服务器,那这些参数记录的就是主服务器的信息;如果 Sentinel 正在监视的是从服务器,则记录的从服务器正在复制的主服务器的信息。
当 Sentinel 与一个主服务器或从服务器建立起订阅连接之后,Sentinel 也会发送如下订阅连接命令:
SUBSCRIBE __sentinel__:hello
因此,对于每个与 Seatinel 连接的服务器,Sentinel 既向服务器的 sentinel:hello 频道发送信息,又通过订阅连接从服务器的 sentinel:hello 频道接收信息。通过这种发布订阅机制,哨兵集群就形成了,它们相互间就可以通过网络连接进行通信,比如说对主库有没有下线这件事儿进行判断和协商。
如下图所示,哨兵 1 把自己的 IP、端口发布到 sentinel:hello 频道上,哨兵 2 和 3 订阅了该频道。那么此时,哨兵 2 和 3 就可以从这个频道直接获取哨兵 1 的 IP 地址和端口号。然后,哨兵 2 和 3 可以和哨兵 1 建立网络连接。通过这个方式,哨兵 2 和 3 也可以建立网络连接,这样就构成了一个哨兵集群。
3. 心跳检测
在默认情况下,Sentinel 会以每秒一次的频率向所有与它创建了命令连接的实例(主服务器、从服务器、其他 Sentinel 节点)发送 PING 命令,并通过实例返回的 PING 命令回复来检测它们是否仍然在线运行。
sentinel.conf 配置文件中的 down-after-milliseconds 选项指定了 Sentinel 判断实例下线所需的时间。如果从库没有在规定时间内响应哨兵的 PING 命令,哨兵就会把它标记为下线状态;同样,如果主库也没有在规定时间内响应哨兵的 PING 命令,哨兵就会判定主库下线,然后开始自动切换主库的流程。
在实际使用时,我们要保证不同的哨兵实例上 down-after-milliseconds 的配置是一样的。如果这个值在不同的哨兵实例上配置不一致,会导致哨兵集群一直没有对有故障的主库形成共识,也就不能及时切换主库,最终的结果就是集群服务不稳定。
4. 主观下线和客观下线
哨兵进程会使用 PING 命令检测它自己和主、从库的网络连接情况,用来判断实例的状态。如果哨兵发现主库或从库对 PING 命令的响应超时了,那么,哨兵就会先把它标记为 主观下线。
如果检测的是从库或哨兵节点,那哨兵简单地把它标记为主观下线就行了,因为从库的下线影响一般不太大,集群的对外服务不会间断。但如果检测的是主库,此时哨兵不能简单地把它标记为主观下线并立即开启主从切换。因为哨兵有些情况下(集群网络压力较大、网络拥塞、主库本身压力较大)会存在误判,在误判的情况下,其实主库是并没有故障的,如果直接启动主从切换,后续的选主和通知操作都会带来额外的计算和通信开销。
正因为这样,我们需要判断是否有误判,以及减少误判。通过引入多个哨兵实例一起来判断,就可以避免单个哨兵因为自身网络状况不好,而误判主库下线的情况了。同时,多个哨兵的网络同时不稳定的概率较小,由它们一起做决策,误判率也能降低。因此,在判断主库是否下线时,只有当多数的哨兵都判断主库已经主观下线了,主库才会被标记为 客观下线。
如下图所示,Redis 主从集群有一个主库、三个从库,还有三个哨兵实例。左图中,哨兵 2 判断主库为“主观下线”,但哨兵 1 和 3 却判定主库是上线状态,此时,主库仍被判断为处于上线状态。右图中,哨兵 1 和 2 都判断主库为“主观下线”,此时,即使哨兵 3 仍然判断主库为上线状态,主库也被标记为“客观下线”了。
简单来说,客观下线的标准就是,当有 N 个哨兵实例时,最好要有 N/2 + 1 个实例判断主库为主观下线,才能最终判定主库为客观下线。这样一来,就可以减少误判的概率,也能避免误判带来的无谓的主从库切换。
5. 选举领头哨兵
借助于多个哨兵实例的共同判断机制,我们可以更准确地判断出主库是否处于下线状态。如果主库的确下线了,哨兵就要开始选主流程了。哨兵集群会通过协商投票选举出一个领头 Sentinel,并由领头 Sentinel 对下线主服务器执行故障转移操作。
任何一个哨兵实例只要自身判断主库主观下线后,就会给其他实例发送 is-master-down-by-addr 命令。然后其他实例会根据自己和主库的连接情况,做出 Y 或 N 的响应,Y 相当于赞成票,N 相当于反对票。
一个哨兵获得了仲裁所需的赞成票数后,就可以标记主库为客观下线。这个所需的赞成票数是通过哨兵配置文件中的 quorum 配置项设定的。例如,现在有 5 个哨兵,quorum 配置的是 3,则一个哨兵需要 3 张赞成票就可以标记主库为客观下线了。这 3 张赞成票包括哨兵自己的一张赞成票和另外两个哨兵的赞成票。
此时,这个哨兵就可以再给其他哨兵发送命令,表明希望由自己来执行主从切换,并让所有其他哨兵进行投票。这个投票过程称为 Leader 选举。在投票过程中,任何一个想成为 Leader 的哨兵,要满足两个条件:
- 第一,拿到半数以上的赞成票;
- 第二,拿到的票数同时还需要大于等于哨兵配置文件中的 quorum 值。
如果在投票过程中未能产生 Leader。哨兵集群会等待一段时间(一般是故障转移超时时间 failover-timeout 的两倍)再重新选举。这是因为哨兵集群能够进行成功投票,很大程度上依赖于选举命令的正常网络传播。如果网络压力较大或有短时堵塞,就可能导致没有一个哨兵能拿到半数以上的赞成票。所以等网络拥塞好转后,再进行投票选举,成功的概率就会增加。
投票示例过程如下图所示:
在 T1 时刻,S1 判断主库客观下线,它想成为 Leader,就先给自己投一张赞成票,然后分别向 S2 和 S3 发送命令表示要成为 Leader。在 T2 时刻,S3 也判断主库为客观下线,它也想成为 Leader,所以也先给自己投一张赞成票,再分别向 S1 和 S2 发送命令,表示要成为 Leader。
在 T3 时刻,S1 收到了 S3 的 Leader 投票请求。因为 S1 已经给自己投了一票 Y,所以它不能再给其他哨兵投赞成票了,所以 S1 回复 N 表示不同意。同时,S2 收到了 T2 时 S3 发送的 Leader 投票请求。因为 S2 之前没有投过票,它会给第一个向它发送投票请求的哨兵回复 Y,给后续再发送投票请求的哨兵回复 N,所以,在 T3 时刻,S2 回复 S3,同意 S3 成为 Leader。
在 T4 时刻,S2 才收到 T1 时 S1 发送的投票命令。此时,S2 只能给 S1 回复 N。发生这种情况,是因为 S3 和 S2 之间的网络传输正常,而 S1 和 S2 之间的网络传输可能正好拥塞了,导致投票请求传输慢了。最后,S3 不仅获得了半数以上的 Leader 赞成票,也达到预设的 quorum 值(quorum 为 2),所以它成为 Leader。
需要注意的是,如果哨兵集群只有 2 个实例,此时,一个哨兵要想成为 Leader,必须获得 2 票,而不是 1 票。所以,如果有个哨兵挂掉了,那此时的集群是无法进行主从库切换的。因此,通常我们至少会配置 3 个哨兵实例。这一点很重要,在实际应用时可不能忽略了。
6. 故障转移
在选举出领头 Sentinel 后,领头 Sentinel 将对已下线的主服务器执行故障转移操作,该操作包含以下三步:
在已下线主服务器属下的所有从服务器里,挑选出一个从服务器,并将其转换为主服务器。
让已下线主服务器属下的所有从服务器改为复制新的主服务器。
将已下线主服务器设置为新的主服务器的从服务器,当这个旧的主服务器重新上线时,它就会成为新的主服务器的从服务器。
在选择新的主节点时,会在已下线主节点属下的所有从节点中,挑选出一个状态良好、数据完整的从节点,然后向这个从节点发送 REPLICAOF no one 命令让其成为主节点。领头 Sentinel 会将已下线主服务器的所有从服务器保存到一个列表里面,然后按照以下规则进行筛选:
删除列表中所有处于下线或者断线状态的从服务器,这可以保证列表中剩余的从服务器都是正常在线的。
期除列表中所有最近五秒内没有回复过领头 Sentinel 的 INFO 命令的从服务器,这可以保证列表中剩余的从服务器都是最近成功进行过通信的。
删除所有与已下线主服务器连接断开超过 down-after-milliseconds * 10 毫秒的从服务器,这可以保证列表中剩余的从服务器都没有过早地与主服务器断开连接,即这些从服务器保存的数据都是比较新的。
之后,领头 Sentinel 将根据从服务器的优先级,对列表中剩余的从服务器进行排序并选出其中优先级最高的从服务器。我们可以分别按照三个规则依次进行三轮打分,这三个规则分别是从库优先级、从库复制进度以及从库 ID 号。只要在某一轮中,有从库得分最高,那么它就是主库了。
6.1 从库优先级
用户可以通过 replica-priority 配置项,给不同的从库设置不同优先级。比如,你有两个从库,它们的内存大小不一样,你可以手动给内存大的实例设置一个高优先级。在选主时,哨兵会给优先级高的从库打高分,如果有一个从库优先级最高,那么它就是新主库了。如果从库的优先级都一样,那么哨兵开始第二轮打分。
6.2 从库复制进度
如果有多个具有相同最高优先级的从服务器,那么领头 Sentinel 将按照从服务器的复制偏移量,对具有相同最高优先级的所有从服务器进行排序,并选出其中偏移量最大的从服务器(复制偏移量最大的从服务器机是保存着最新致据的从服务器)。具体做法是,比较不同从库的 slave_repl_offset 来找出值最大的从库。
6.3 从库 ID 号
最后,如果有多个优先级最高、复制偏移量最大的从服务器,那么领头 Sentinel 将按照运行 ID 对这些从服务器进行排序,并选出其中运行 ID 最小的从服务器。
当选出新的主节点并发送 REPLICAOF no one 命令后,领头 Sentinel 会以每秒 1 次的频率(平时是每 10 秒一次)向被升级的从节点发送 INFO 命令,并观察回复中的角色信息,当被升级节点的 role 变成 master 时,领头 Sentinel 就知道被选中的从节点已经顺利升级成主节点了。
7. 客户端通知
当完成故障转移后,Redis 客户端需要感知到新的主服务器,并把写命令发送到新的主服务器上。因此,每个哨兵实例也提供 PUB/SUB 机制,客户端可以从哨兵订阅消息。哨兵提供的消息订阅频道有很多,不同频道包含了主从库切换过程中的不同关键事件。
知道了这些频道之后,你就可以让客户端从哨兵这里订阅消息了。具体的操作步骤是,客户端读取哨兵的配置文件后,可以获得哨兵的地址和端口,和哨兵建立网络连接。然后,我们可以在客户端执行订阅命令,来获取不同的事件消息。
比如,我们可以执行如下命令来订阅:所有实例进入客观下线状态的事件:
SUBSCRIBE +odown
当然,你也可以执行如下命令,订阅所有的事件:
PSUBSCRIBE *
当哨兵把新主库选择出来后,客户端就会看到下面的 switch-master 事件。这个事件表示主库已经切换了,新主库的 IP 地址和端口信息已经有了。这个时候,客户端就可以用这里面的新主库地址和端口进行通信了。
switch-master <master name> <oldip> <oldport> <newip> <newport>
有了这些事件通知,客户端不仅可以在主从切换后得到新主库的连接信息,还可以监控到主从库切换过程中发生的各个重要事件。这样,客户端就可以知道主从切换进行到哪一步了,有助于了解切换进度。