11、认识redis的sentinel
文章目录
- Sentinel 概述
- Sentinel 核心原理
- sentinelState 结构体
- sentinelRedisInstance结构体
- 连接状态检查
- 发送命令
- PING命令
- INFO 命令
- HELLO 消息
- 监控总结
- 状态检查
- 故障的转移
- failover 状态机
- 转移的简图
- 参考文章
Sentinel 概述
Sentinel 是 Redis 提供的高可用解决方案之一。一个 Sentinel 服务进程其实本身就是 Redis 实例,只不过这个 Redis 服务实例是以 Sentinel 模式运行的,它不对外提供读写键值对的服务,而是监控其他 Redis 服务实例是否运行正常,有点类似现实生活中监工
的感觉。
为了防止 Sentinel 本身出现单点问题,一般会将多个 Sentinel 实例组成一个 Sentinel 集群, 一般推荐 Sentinel 集群的实例个数为奇数。Sentinel 核心功能是监控线上 Redis 实例的状态,当发现某个主库故障的时候,Sentinel 会自动将故障的主库下线,然后从剩余的从库中选出一个合适的从库,提升为新一任主库,继续对外提供服务。
下图展示了一个 Sentinel 集群监控两个线上 Redis 主从集群的架构。
Sentinel 核心原理
介绍完 Sentinel 这个概念以及它的定位之后,我们总结出 Sentinel 的一些核心功能,主要有三点。
- 监控:这是 Sentinel 最基础的功能,Sentinel 会定期检查 Redis 主从库的状态,以及其他的运行情况。
- 自动故障转移:在 Sentinel 检测到主库故障的时候,Sentinel 会触发自动故障转移。Sentinel 会在剩余的在线从库中选举出新一任主库。
- 通知:完成新一任主库的选举之后,Sentinel 会通知其他从库切换成新主库的从库。Sentinel 还可以通过 API 方式给运维人员发送一些集群的状态信息。另一方面,在主库变更的时候,或者客户端第一次访问 Redis 的时候,Sentinel 会将主库的地址下发给客户端。
那 Sentinel 是如何实现上述三个核心功能的呢?首先我们来看 Sentinel 与 Redis 服务实例之间的交互,如下图:
首先来看 Sentinel 集群与主库交互的关键操作。
- Sentinel 集群会定时向所有 Redis 实例发送
PING 命令
,正常在线的 Redis 实例在收到 PING 命令之后,会返回 PONG 响应。这样,Sentinel 就可以检查整个线上 Redis 集群实例的在线情况。 - Sentinel 集群还会向主库定时发送
INFO 命令
,主库收到 INFO 命令之后,会将自身运行情况,以及关联从库的信息以及主从复制信息返回。这样,Sentinel 就可以获取主从复制方面的信息。 - 在 Sentinel 明确了各个 Redis 之间的主从关系之后,会通过
PULISH 命令
向所有 Redis 实例发送自己的信息及主库相关的配置信息,频道名称为__sentinel__:hello
(后面简称 hello 频道)。 - Sentinel 会订阅所有 Redis 实例的 hello 频道,这样,就可以获取正在监控相同 Redis 主从集群的其他 Sentinel 节点的信息,也就感知到了其他 Sentinel 实例的存在。
Sentinel 实例在感知彼此之后,Sentinel 节点之间会进行下面的交互。
- Sentinel 实例会向其他 Sentinel 实例定时发送 PING 命令,检查它们的在线状态。
- 在当前 Sentinel 发现某个 Redis 实例宕机,且持续一段时间(down-after-milliseconds 配置指定)没有响应 PING 命令检测,会认为这个 Redis 实例是“主观下线(SDOWN)”。当发生故障的是主库时,Sentinel 会通过 is-master-down-by-addr 命令向其他 Sentinel 节点查询主库是否在线,如果超过半数的 Sentinel 实例都认为这个主库“主观下线”,那 Sentinel 则会认为主库是“客观下线(ODOWN)”状态。Sentinel 接下来触发新一任主库的选举。
sentinelState 结构体
sentinelState 它是 Sentinel 中最核心的结构体,记录了 Sentinel 实例核心状态信息。
- myid:长度为 40 的字符串,用于唯一标识 Sentinel 实例。我们可以在 sentinel.conf 配置文件中通过
sentinel myid <myid>
配置进行手动指定。如果不手动指定的话,在 Sentinel 启动时会随机生成 myid 标识,随机生成的 myid 将会在 sentinelIsRunning() 中写回到 sentinel.conf 配置文件中。 - current_epoch:纪元是很多分布式协议中都有的一个概念,在分布式系统中,主节点发生故障之后,集群会通过分布式协议重新选出新一任主节点,此时就会将纪元值加一,它用于标识旧时代的结束和新时代的开始,这就防止处于两个时代的节点通信,导致集群内的信息错乱。Sentinel 集群中也有类似的选主操作,current_epoch 字段就是用来记录当前 Sentinel 实例所处的纪元。
- masters:它是一个字典集合,其中记录了当前 Sentinel 集合监控的 Redis 主库集合。其中,Key 是 Redis 实例的名称,Value 是表示 Redis 主库的 sentinelRedisInstance 实例指针,sentinelRedisInstance 结构体在后面详细分析。
- tilt、tilt_start_time、previous_time:tilt 字段标识当前 Sentinel 实例是否开启了TILT 模式。在后面介绍 Sentinel 工作原理的时候,我们会看到 Sentinel 需要系统时间比较准确,但如果系统时间不够准确,或是 Sentinel 被某个耗时操作阻塞,Sentinel 就可能执行一些错误操作,例如,认为某个主库主观下线了。这种情况下,Sentinel 就会进入 TILT 模式,此时的 Sentinel 只会收集集群的信息,不会进行任何其他操作。在 Sentinel 实例的时间恢复正常之后,它就会退出 TILT 模式。tilt_start_time 字段记录了当前 Sentinel 进入 TILT 模式的时间戳。previous_time 字段则记录了上次进行对时的时间戳,如果两次对时间隔超过 2 s,当前 Sentinel 就会进入 TILT 模式。
- announce_ip、announce_port、announce_hostnames、resolve-hostnames:正常情况下,Sentinel 实例是可以通过 inet_ntop() 函数获取自身的 ip 并发送给其他 Redis 实例,但是在某些特殊网络环境中,拿到的 ip 值是不正确的。此时我们就可以在配置文件指定
sentinel announce-ip <announce_ip>
配置项来指定自身的 ip。announce_port、announce_hostnames 两个字段的含义类似。
sentinelRedisInstance结构体
在 sentinelState 结构体中,masters 是比较关键的集合,其中存储的是 sentinelRedisInstance 指针。从名字就可以看出,sentinelRedisInstance 是 Sentinel 用来抽象 Redis 实例的结构体,它可以表示一个 Redis 主库、从库或是 Sentinel 实例。在它抽象不同类型 Redis 实例时,其中用于记录信息的字段也不同。
这里我们将结合下面三条命令介绍 sentinelRedisInstance 实例的构造过程以及字段含义:
sentinel monitor tesMaster 127.0.0.1 6397 2 sentinel known-replica tesMaster 127.0.0.1 6380sentinel known-sentinel tesMaster 127.0.0.1 26379 acechdeaed...
在 sentinel monitor
配置项中指定的是 Sentinel 需要监听的 Redis 主库信息,对应到 sentinelRedisInstance 结构体中的字段分别如下。
- name:主库的名称,也就是上述配置示例中的 testMaster。
- addr:它指向一个 sentinelAddr 实例,sentinelAddr 则包含了主库的 ip 和 port,也就是示例中的 127.0.0.1 这个 ip 和 6379 这个端口值。
- quorum:对应上面配置示例中的 2,它表示至少需要两个 Sentinel 同时认为主库宕机才会允许进行故障转移
- flags :它是一个状态集合,其中每一位都表示一个状态,其中低三位分别对应 SRI_MASTER、SRI_SLAVE、SRI_SENTINEL,三者互斥,分别表示当前 sentinelRedisInstance 代表的是主库、从库、Sentinel 三种类型的 Redis 实例。
- runid:Redis 实例的唯一编号
代表主库的 sentinelRedisInstance 实例创建完成之后,会将 name 与 sentinelRedisInstance 的映射关系写入到前面介绍的 sentinelState.masters 集合中存储。
在上面的示例中, sentinel know-replica
和 sentinel known-sentinel
配置项指定的是需要已知从库的信息以及已知 Sentinel 的信息,其中配置的 ip 和 port 是从库和 Sentinel 的地址,tesMaster 则是从库对应主库的名称。在创建从库和 Sentinel 对应的 sentinelRedisInstance 实例之前,需要确保主库在 sentinelState.masters 集合中已存在。
在从库和 Sentinel 对应的 sentinelRedisInstance 实例创建完成之后,会写入到主库对应sentinelRedisInstance 中的 slaves 和 sentinels 字段中,这两个字段都是 dict 类型,slaves 集合中的 Key 是从库 ip + port 构成的网络地址,sentinels 集合中的 Key 是 sentinel known-sentinel
配置项最后指定的 myid 值,该 myid 值也会记录到 Sentinel 对应 sentinelRedisInstance.runid 字段中,供后续使用。
除了上述主库、从库、Sentinel 的配置解析之外,还会有很多其他以 sentinel 开头的配置,这些配置中都会指定一个主库的名称,相应的这些配置解析之后都会写入到主库 sentinelRedisInstance 的相应字段中,其作用范围也是在对应主库的主从集群中。例如下面这段配置:
#testMaster1的相关配置#主库名称、ip、port以及判定客观下线的Sentinel个数
sentinel monitor testMaster1 127.0.0.1 6397 2 #主库3秒未响应Sentinel的PING请求,则会被判定为主观下线
sentinel down-after-milliseconds testMaster1 30000 #在故障转移时,需要至少启动多少个从库进行同步
sentinel parallel-syncs testMaster1 1#故障转移的超时时间
sentinel failover-timeout testMaster1 180000
sentinel known-replica testMaster1 127.0.0.1 6380
sentinel known-sentinel testMaster1 127.0.0.1 26379 acechdeaed...#testMaster2的相关配置,配置含义同上
sentinel monitor testMaster2 127.0.0.1 7397 4
sentinel down-after-milliseconds testMaster2 30000
sentinel parallel-syncs testMaster2 2
sentinel failover-timeout testMaster2 180000
解析之后的结果如下图所示,表示主库的 sentinelRedisInstance 实例被存放到 sentinelState.masters 集合中,每个主库关联的从库和 Sentinel 对应的 sentinelRedisInstance 实例被存储到主库 sentinelRedisInstance.slaves 和 sentinels 集合中
连接状态检查
检查单个 Redis 实例的状态逻辑流程图如下
如上图所示,核心逻辑分为监控、状态检查、故障转移三部分:
- 在监控部分的逻辑中,Sentinel 会通过发送各种命令来了解 Redis 集群的拓扑以及每个 Redis 实例的状态;
- 状态检查部分是 Sentinel 根据监控部分的结果,判断 Redis 实例是否进入主观下线或是客观下线状态;
- 最后的故障转移部分,是在发现有主库客观下线的时候,自动选出新的主库,继续对外提供服务的过程。
发送命令
Sentinel 与集群中其他 Redis 实例(包括 Sentinel、主库、从库)建立连接之后,会定时发送下面三种命令
- 定时向主库、从库以及 Sentinel 发送 PING 命令。
- 定时向主库和从库发送 INFO 指令。
- 定时向 hello 频道发送 hello 消息。
PING命令
Sentinel 会计算 PING 命令的发送周期,默认是 down_after_period 值,down-after-milliseconds 的含义是认为多长时间没有心跳,就认为这个 Redis 实例客观下线的时间值。我们可以通过下面的配置指定一个主库的 down-after-milliseconds 值。
sentinel down-after-milliseconds mymaster 30000
INFO 命令
默认情况下,Sentinel 会 10 秒
发送一次 INFO 命令,但是有两个例外情况。
- 一个是对端 Redis 实例是从库,且它的主库处于 ODOWN 状态(客观下线)时,需要将 INFO 命令的发送频率提高到 1 秒一次,来及时获取主库的状态。
- 另一个是对端 Redis 实例是从库,且它与主库的主从复制连接断开时,需要将 INFO 命令的发送频率提高到 1 秒一次,来获取准确的重连时间
解析INFO 的返回值
-
获取Redis 实例的 runId,并更新到相应 sentinelRedisInstance 的 runId 字段中
-
如果对端是主库,则解析 INFO 响应中前缀为 “slave” 的行,得到该主库下的所有从库的 ip 和 port。然后,根据 ip 和 port 从其sentinelRedisInstance 的 slaves 字典中查询对应 Slave,如果查找失败,则创建对应的 sentinelRedisInstance 实例并记录到 slaves 字典中
-
如果对端是从库,会解析 master_link_down_since_seconds 这一行,得到主从复制连接断开的时长,并记录到 master_link_down_time 字段中
-
根据 INFO 响应中包含 role:master 还是 role:slave 决定该 Redis 实例的角色是否发生变化,比如这个 Redis 实例从从库提升为了主库;再比如这个 Redis 实例本身是主库,由于各种原因,切换成从库了。如果角色信息发生变化,会更新到其sentinelRedisInstance 的 role_reported 字段中。如果这个 Redis 实例本身是从库或者切换之后成为从库的话,需要解析并记录 master_host、master_port、master_link_status 等信息,这些信息是该从库对应主库的一些信息,同时更新 slave_conf_change_time 字段,记录最后更新其主库信息的时间,还会解析 slave_priority、slave_repl_offset、replica_announced 得到从库的优先级、Replication Offset 等主从复制的状态并记录到相应字段中。
-
最后更新该 Redis 实例相关的 info_refresh 字段,记录最后一次 INFO 响应的时间戳
-
处理完 INFO 命令返回值之后, 尤其是 Redis 实例角色的变更,做一些额外的处理操作,这些操作与故障转移相关
HELLO 消息
最后,我们来看 hello 消息的发送,Sentinel 默认情况下会每 2 秒向其监控的主库、从库发送 hello 消息。hello 消息的格式如下图所示,分为 8 个部分,每一部分之间通过逗号分隔
该 hello 消息是 Sentinel 发送 PUBLISH 命令到 __sentinel__:hello
频道中的。
解析HELLO 的返回值
- 更新 pc_last_activity 时间戳,记录最后一次收到 hello 消息的时间戳,在后面判断对端 Redis 实例是否下线时会用到该时间戳。
- 当前 Sentinel 可以明确其他 Sentinel 与各个主库之间的对应关系,从而更新自身维护的 Sentinel 与主库的网络拓扑。
- 同步自身 epoch 与各个主库的 epoch。
监控总结
下面我们就通过一个相对完整的示例梳理一下该过程的内容。
首先我们假设现在有三个主从集群,如下图所示:
sentinel monitor Master1 127.0.0.1 6379 2sentinel monitor Master2 127.0.0.1 7379 2sentinel monitor Master3 127.0.0.1 8379 2
在这三个 Sentinel1 启动过程中,会解析各自的配置文件,创建各个主库对应的 sentinelRedisInstance 实例,同时建立相应的连接。下图展示了 Sentinel1 启动完成的状态,其中 s1-M1 就是 Sentinel1 为主库1 创建的 sentinelRedisInstance 实例,其他同理:
Sentinel1 与主库1 建立的连接是两条异步连接,在建连完成之后,会立刻分别发送 PING 命令和 SUBSCRIBE 命令,同时,分别注册 PING 响应的回调函数以及订阅 hello 频道的回调函数。
接下来,Sentinel1 会周期性地向主库1 发送 INFO 和 PING 命令。在 INFO 命令的返回值中,“slave” 开头的行里面记录了主库下面挂的从库信息,在上述示例中,主库1 的 INFO 返回值中会有 slave0、slave1 两行数据,分别记录了从库1 和从库2 两个从库的地址,Sentinel 会为这两个从库创建对应的 sentinelRedisInstance 实例,并在下一个周期中创建与从库1 、从库2 的网络连接,如下图所示:
在后续定时任务中,Sentinel1 会周期性地发送 PING 命令、INFO 命令、Hello 消息来监控主库1 以及从库1 、 从库2 构成的这个主从复制集群。同理,Sentinel 1 也会为主库2(从库3、从库4)、主库3(从库5、从库6)这 6 个 Redis 实例创建对应的 sentinelRedisInstance 实例,维护它们之间的主从关系,并建立相应的连接进行监控,这里就不再重复了。
Sentinel1 与主库1 、从库1 、从库2 之间还会订阅 hello 频道。当 Sentinel2 与主库1 (从库1 、从库2)建立完连接之后,会周期性地向这三个 Redis 实例的 hello 频道 PUBLISH 一条 hello 消息。此时,订阅了主库1 hello 频道的 Sentinel1 ,就能感知到 Sentinel2;同样的,Sentinel2 也订阅了主库1 的 hello 频道,也能收到 Sentinel1 发来的 hello 消息,感知到其存在。
在 Sentinel1 收到 Sentinel2 通过主库1 中 hello 频道发来的 hello 消息之后,会为 Sentinel2 创建一个 sentinelRedisInstance 实例,并记录到 s1-M1->sentinels 字典中,如下图所示,Sentinel1 只会与 Sentinel2 创建一个连接,用来发送 PING、INFO 以及 PUBLISH 命令。
Sentinel1 除了通过主库1 的 hello 频道感知到 Sentinel2 之外,还会通过主库2、主库3 感知到 Sentinel2,此时 Sentinel1 会在 s1-M1、s1-M2、s1-M3 的 sentinels 字典中,为 Sentinel2 创建三个不同的 sentinelRedisInstance 实例。
状态检查
哨兵如何判断主库已经下线了呢?
首先要理解两个概念:主观下线和客观下线
- 主观下线:任何一个哨兵都是可以监控探测,并作出Redis节点下线的判断;
- 客观下线:有哨兵集群共同决定Redis节点是否下线
当某个哨兵(如下图中的哨兵2)判断主库“主观下线”后,就会给其他哨兵发送 is-master-down-by-addr
命令。接着,其他哨兵会根据自己和主库的连接情况,做出 Y 或 N 的响应,Y 相当于赞成票,N 相当于反对票。
如果赞成票数(这里是2)是大于等于哨兵配置文件中的 quorum
配置项(比如这里如果是quorum=2), 则可以判定主库客观下线了。
哨兵集群的选举
判断完主库下线后,由哪个哨兵节点来执行主从切换呢?这里就需要哨兵集群的选举机制了。
- 为什么必然会出现选举/共识机制?
为了避免哨兵的单点情况发生,所以需要一个哨兵的分布式集群。作为分布式集群,必然涉及共识问题(即选举问题);同时故障的转移和通知都只需要一个主的哨兵节点就可以了。
- 哨兵的选举机制是什么样的?
哨兵的选举机制其实是一个Raft选举算法: 选举的票数大于等于num(sentinels)/2+1时,将成为领导者,如果没有超过,继续选举
任何一个想成为 Leader 的哨兵,要满足两个条件:
- 第一,拿到半数以上的赞成票
- 第二,拿到的票数同时还需要大于等于哨兵配置文件中的 quorum 值
以 3 个哨兵为例,假设此时的 quorum 设置为 2,那么,任何一个想成为 Leader 的哨兵只要拿到 2 张赞成票,就可以了。
故障的转移
Sentinel 感知到 Master 节点发生客观下线之后, Sentinel 会选择一个 Slave 节点提升为新 Master 节点,并对外继续提供服务,之后,其他 Slave 节点会与新提升的 Master 节点进行主从复制,这个过程就是我们所说的 “故障转移(failover)” 。
Redis 之所以引入 Sentinel,其最主要的目的就是实现“自动故障转移”,也就是说,无需人为干预,Sentinel 自动完成整个“故障转移(failover)”的流程,这就可以减少运维成本,提升了整个 Redis 服务的可用性。
failover 状态机
在开始介绍 failover 操作之前,我们先来关注一下 Master 节点对应的 sentinelRedisInstance.failover_state 字段
,它是用来控制 failover 执行流程的核心状态字段,整个 Sentinel 的 failover 操作是以状态机的方式进行驱动的。
也就是说,failover_state 这个状态字段控制着整个 failover 的进程,只有 failover_state 的状态值不断向前推进,failover 操作才能继续向下执行。
failover_state 字段的状态机如下图所示:
为了书写简单,后续 failover_state 状态的枚举值都省略“SENTINEL_FAILOVER_STATE_”前缀。下面是 failover_state 在不同状态值所表示的含义
-
NONE 状态:Master 节点一切正常,没有 failover 发生。
-
WAIT_START 状态:Master 节点客观下线,且符合 failover 的各项条件,等待其 Leader Sentinel 进行 failover。
这一步需要确认自身是否为Leader Sentine, 这里会迭代 master->sentinels 字典(也就是监听宕机 Master 节点的 Sentinel 对应的 sentinelRedisInstance 实例),统计所有 Sentinel 节点的投票结果(即统计其 leader、leader_epoch 字段值)。如果当前 Sentinel 得到了半数以上的投票,则当前 Sentinel 节点为 Leader 节点
-
SELECT_SLAVE 状态:Leader Sentinel 会选择一个 Slave 晋级为 Master。选择Slave的关键如下
- 过滤掉不健康的(下线或断线),没有回复过哨兵ping响应的从节点
- 选择
salve-priority
从节点优先级最高(redis.conf)的 - 选择复制偏移量最大,只复制最完整的从节点
- 将得到的Slave节点数组, 然后根据Slave 的优先级**、Replication Offset 以及 runId 对 Slave 节点进行排序, 取第一个作为master节点, 这里称之为
Promoted Slave 节点
-
SEND_SLAVEOF_NOONE 状态:Leader Sentinel 会向选出的 Slave 发送
SLAVEOF NO ONE
命令,使其变更为 Master。 -
WAIT_PROMOTION 状态:等待 Slave 晋升为 Master。
-
RECONF_SLAVES 状态:向其他 Slave 发送
SLAVEOF
命令,使它们成为新一任 Master 的 Slave。 -
UPDATE_CONFIG 状态:Sentinel 开始监听新的 Master。
如果发生了 Failover,Redis Client 是如何知道新一任 Master 主库的地址呢?
一种方式是使用 VIP + client_reconfig_script 脚本的方式,这样客户端看到的始终是 VIP,Sentinel 在 failover 完成时漂移 VIP 到新一任 Master,客户端重新建连即可。这种方式会引入 VIP 这一额外组件,还要额外编写 client_reconfig_script 脚本,增加了运维成本。
另一种方式是将查找新一任 Master 的能力封装到 Redis 客户端,在 Redis 官网中提供了一个 Sentinel Client 的实现指南
转移的简图
假设根据我们一开始的图:(我们假设:判断主库客观下线了,同时选出sentinel 3
是哨兵leader)
故障转移流程如下:
-
将slave-1脱离原从节点(PS: 5.0 中应该是
replicaof no one
),升级主节点, -
将从节点slave-2指向新的主节点
-
通知客户端主节点已更换
-
将原主节点(oldMaster)变成从节点,指向新的主节点
转移之后
个人公众号: 行云代码
参考文章
https://www.pdai.tech/md/db/nosql-redis/db-redis-x-sentinel.html
https://juejin.cn/book/7144917657089736743/section/7147530603431198754
https://juejin.cn/book/7144917657089736743/section/7147530515090767907
https://juejin.cn/book/7144917657089736743/section/7147530551501520911