参考文献

为什么要引入哨兵模式?

  • Redis 的主从复制模式下,一旦主节点由于故障不能提供服务,需要手动将从节点晋升为主节点,同时还要通知客户端更新主节点地址,这种故障处理方式从一定程度上是无法接受的.

  • Redis 2.8 以后提供了 Redis Sentinel 哨兵机制来解决这个问题.

  • Redis Sentinel 是 Redis 高可用的实现方案.Sentinel 是一个管理多个 Redis 实例的工具,它可以实现对 Redis 的监控、通知、自动故障转移.

基本概念

名词 逻辑结构 物理结构
主节点(master) Redis主服务/数据库 一个独立的Redis进程
从节点(slave) Redis从服务/数据库 一个独立的Redis进程
Redis数据节点 主节点和从节点 主节点和从节点的进程
Sentinel节点 监控Redis数据节点 一个独立的Sentinel进程
Sentinel节点集合 若干Sentinel节点的抽象组合 若干Sentinel节点进程
Redis Sentinel Redis高可用实现方案 Sentinel节点集合和Redis数据节点进程
应用方 泛指一个或多个客户端 一个或者多个客户端进程或线程

主从复制的问题

  • Redis主从复制模式可以将主节点的数据改变同步给从节点,这样从节点就有两个作用:
    • 作为主节点的一个备份,一旦主节点出了故障不可达的情况,从节点可以作为后备顶上来,并且保证数据尽量不丢失(主从复制是最终一致性);
    • 从节点可以扩展主节点的读能力,一旦主节点不能支撑住大并发量的读操作,从节点可以在一定程度上帮助主节点分担读压力;
  • 但主从复制也带来了以下问题:
    • 一旦主节点出现故障,需要手动将一个从节点晋升为主节点,同时需要修改应用方的主节点地址,还需要命令其他从节点复制新的主节点,整个过程都需要人工干预;(Redis高可用问题)
    • 主节点的写能力受到单机的限制;(Redis分布式问题)
    • 主节点的存储能力受到单机的限制;(Redis分布式问题)

高可用问题

示例: 一主二从的Redis主从复制模式下出现故障问题后,是如何进行故障转移的.

  • 主节点发送故障后,客户端(Client)连接主节点失败,两个从节点与主节点连接失败造成复制终端;

    img
  • 如果主节点无法正常启动,需要选出一个从节点(slave-1),对其执行slaveof no one命令使其成为新的主节点;

  • 原来的从节点(slave-1)成为新的主节点后,更新应用方的主节点信息,重新启动应用方;

  • 客户端命令另一个从节点(slave-2)去复制新的主节点(new master);

  • 待原来的主节点恢复后,让它去复制新的主节点;

img
  • 上述处理过程存在的问题
    • 判断节点不可达的机制是否健全和标准?
    • 如果有多个从节点,怎么保证只有一个晋升为主节点?
    • 通知客户端新的主节点机制是否足够健壮?
  • Redis Sentinel正是用于解决这些问题

Redis Sentinel的高可用性

当主节点出现故障时,Redis Sentinel能自动完成故障发现和故障转移,并通知应用方,从而实现真正的高可用;

img
  • Redis Sentinel处理故障过程
    • Redis Sentinel是一个分布式架构,其中包含若干个Sentinel节点和Redis数据节点,每个Sentinel节点会对数据节点和其余Sentinel节点进行监控,当他发现节点不可达时,会对节点做下线标识.如果被标识的是主节点,它还会和其他Sentinel节点进行"协商",当大多数Sentinel节点都认为不可达时,它们会选举出一个Sentinel节点来完成自动故障转移工作,同时将这个变化通知给Redis应用方.
    • 整个过程完全是自动的,不需要人工来介入;

Redis Sentinel处理高可用问题

示例: 一主二从的Redis Sentinel模式下出现故障问题后,是如何进行故障转移的

  • 主节点出现故障,此时两个从节点与主节点失去连接,主从复制失败;
  • 每个Sentinel节点通过定期监控发现主节点出现故障;
  • 多个Sentinel节点对主节点的故障达成一致,选举出sentinel-3节点作为领导者负责故障转移;
img
  • Sentinel领导者节点执行故障转移,整个过程和主从复制处理故障一致,只不过是自动完成的;

    img
  • 故障转移后整个Redis Sentinel的拓扑结构图

    img

Redis Sentinel具有的功能

  • 监控: Sentinel节点会定期监测Redis数据节点,其余Sentinel节点是否可达;

  • 通知: Sentinel节点会故障转移的结果通知给应用方;

  • 主节点故障转移: 实现从节点晋升为主节点并维护后续正确的主从关系;

  • 配置提供者: 在Redis Sentinel结构中,客户端在初始化的时候连接的是Sentinel节点集合,从中获取主节点信息;

  • Redis Sentinel包含了若干Sentinel节点,这样做也带来了两个好处:

    • 对于节点的故障判断是由多个Sentinel节点共同完成,这样可以有效地防止误判;
    • Sentinel节点集合是由若干个Sentinel节点组成的,这样即使个别Sentinel节点不可用,整个Sentinel节点集合依然健壮的;
    • 但是Sentinel节点本身就是独立的Redis节点,只不过它们有一些特殊,它们不存储数据,支持部分命令;

Redis Sentinel配置

  • 要保证所有哨兵实例的配置是一致的,尤其是主观下线的判断值down-after-milliseconds

部署三个Sentinel节点,一个主节点,两个从节点组成一个Redis Sentinel

  • 拓扑图

    img
  • 具体物理结构

    角色 IP Port 别名
    master 127.0.0.1 6379 主节点或者6379节点
    slave-1 127.0.0.1 6380 slave-1或者6380节点
    slave-2 127.0.0.1 6381 slave-2或者6381节点
    sentinel-1 127.0.0.1 26379 sentinel-1或者26379节点
    sentinel-2 127.0.0.1 26380 sentinel-2或者26380节点
    sentinel-3 127.0.0.1 26381 sentinel-3或者26381节点
  • 启动主节点

    • 配置

      1
      2
      3
      4
      5
      6
      port 6379
      daemonize yes
      pidfile /var/run/redis_6379.pid
      logfile "/usr/local/redis-6.2.2/logs/6379.log"
      dbfilename dump_6379.rdb
      dir /usr/local/redis-6.2.2/data/
  • 启动两个从节点

    • 配置

      1
      2
      3
      4
      5
      6
      7
      port 6380
      daemonize yes
      pidfile /var/run/redis_6380.pid
      logfile "/usr/local/redis-6.2.2/logs/6380.log"
      dbfilename dump_6380.rdb
      dir /usr/local/redis-6.2.2/data/
      slaveof 127.0.0.1 6379
      1
      2
      3
      4
      5
      6
      7
      port 6381
      daemonize yes
      pidfile /var/run/redis_6381.pid
      logfile "/usr/local/redis-6.2.2/logs/6381.log"
      dbfilename dump_6381.rdb
      dir /usr/local/redis-6.2.2/data/
      slaveof 127.0.0.1 6379
  • 确认主从关系

    • 主节点客户端操作redis-cli -p 6379 -a holelin info replication

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      # Replication
      role:master
      connected_slaves:2
      slave0:ip=127.0.0.1,port=6380,state=online,offset=98,lag=0
      slave1:ip=127.0.0.1,port=6381,state=online,offset=98,lag=1
      master_failover_state:no-failover
      master_replid:72078d9d8d84bc7d89d6f25e7ee1de5366add480
      master_replid2:0000000000000000000000000000000000000000
      master_repl_offset:98
      second_repl_offset:-1
      repl_backlog_active:1
      repl_backlog_size:1048576
      repl_backlog_first_byte_offset:1
      repl_backlog_histlen:98
    • 从节点操作:

      • redis-cli -p 6380 -a holelin info replication

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        # Replication
        role:slave
        master_host:127.0.0.1
        master_port:6379
        master_link_status:up
        master_last_io_seconds_ago:8
        master_sync_in_progress:0
        slave_repl_offset:434
        slave_priority:100
        slave_read_only:1
        replica_announced:1
        connected_slaves:0
        master_failover_state:no-failover
        master_replid:72078d9d8d84bc7d89d6f25e7ee1de5366add480
        master_replid2:0000000000000000000000000000000000000000
        master_repl_offset:434
        second_repl_offset:-1
        repl_backlog_active:1
        repl_backlog_size:1048576
        repl_backlog_first_byte_offset:1
        repl_backlog_histlen:434
      • redis-cli -p 6381 -a holelin info replication

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        # Replication
        role:slave
        master_host:127.0.0.1
        master_port:6379
        master_link_status:up
        master_last_io_seconds_ago:0
        master_sync_in_progress:0
        slave_repl_offset:504
        slave_priority:100
        slave_read_only:1
        replica_announced:1
        connected_slaves:0
        master_failover_state:no-failover
        master_replid:72078d9d8d84bc7d89d6f25e7ee1de5366add480
        master_replid2:0000000000000000000000000000000000000000
        master_repl_offset:504
        second_repl_offset:-1
        repl_backlog_active:1
        repl_backlog_size:1048576
        repl_backlog_first_byte_offset:15
        repl_backlog_histlen:490
  • 部署Sentinel节点

    • 配置

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      port 26379
      daemonize yes
      pidfile /var/run/redis-sentinel-26379.pid
      logfile "/usr/local/redis-6.2.2/logs/sentinel-26379.log"
      dir /usr/local/redis-6.2.2/data/
      # 表示sentinel-1节点需要监控127.0.0.1:6379这个节点 2代表判断主节点失败至少需要2个Sentinel节点同意
      # holelin是主节点的别名
      sentinel monitor holelin 127.0.0.1 6379 2
      # 主节点的认证密码
      sentinel auth-pass holelin holelin
    • 启动Sentinel节点

      • redis-sentinel 配置文件
      • redis-server 配置文件 --sentinel
    • 确认

      1
      2
      3
      4
      5
      6
      7
      8
      [root@holelin redis-6.2.2]# redis-cli -p 26379 info sentinel
      # Sentinel
      sentinel_masters:1
      sentinel_tilt:0
      sentinel_running_scripts:0
      sentinel_scripts_queue_length:0
      sentinel_simulate_failure_flags:0
      master0:name=holelin,status=ok,address=127.0.0.1:6379,slaves=2,sentinels=3

配置优化说明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
port 26379
daemonize yes
pidfile /var/run/redis-sentinel-26379.pid
logfile "/usr/local/redis-6.2.2/logs/sentinel-26379.log"
dir /usr/local/redis-6.2.2/data/
# 表示sentinel-1节点需要监控127.0.0.1:6379这个节点 2代表判断主节点失败至少需要2个Sentinel节点同意
# holelin是主节点的别名
sentinel monitor holelin 127.0.0.1 6379 2
# 主节点的认证密码
sentinel auth-pass holelin holelin
sentinel down-after-milliseconds holelin 30000
sentinel parallel-syncs holelin 1
sentinel fail-over-timeout holelin 180000
# sentinel auth-pass <master-name> <password>
# sentinel notification-script <master-name> <scripte-path>
# sentinel client-reconfig-script <master-name> <script-path>
  • sentinel monitor <master-name> <ip> <port> <quorum>

    • <quorum>代表要判定主节点最终不可达所需要的票数,但实际上Sentinel节点会对所有节点进行监控,但是Sentinel节点的配置中没有看到有关从节点和其余Sentinel节点的配置,那是因为Sentinel节点会从主节点中获取有关从节点以及其余Sentinel节点的相关信息.
    • <quorum>参数用于故障发现和判定,例如将quorum配置为2,则代表至少有2个Sentinel节点认为主节点不可达,那么这个不可达的判定才是客观的;
    • 一般建议设置为Sentinel节点数的一半加1;
    • 同时quorum还与Sentinel节点领导者选举有关,至少有max(quorum,num(sentinels)/2+1)个Sentinel节点参与选举,才能选出领导者Sentinel,从而完成故障转移.
  • 当所有从节点启动后,配置文件中的内容会发生变化,体现在三个方面

    • Sentinel节点自动发现从节点,其余Sentinel节点

    • 去掉默认配置,如parallel-syncs,fail-over-timeout

    • 添加了配置纪元相关参数

      1
      2
      3
      4
      5
      6
      7
      user default on nopass ~* +@all
      sentinel leader-epoch holelin 0
      sentinel known-replica holelin 127.0.0.1 6381
      sentinel known-replica holelin 127.0.0.1 6380
      sentinel known-sentinel holelin 127.0.0.1 26380 8a47d95157673f1a0d4ac67bbc323208daf80d49
      sentinel known-sentinel holelin 127.0.0.1 26381 13c895cd85d5614055f1a633e7939f29ef5f6f11
      sentinel current-epoch 0
  • sentinel down-after-milliseconds <master-name> <times>

    • 每个Sentinel节点都要通过定期发送ping命令来判断Redis数据节点和其他Sentinel节点是否可达,如果超过了down-after-milliseconds配置的时间且没有有效的回复,则判定节点不可达,<times>单位为毫秒,就是超时时间
  • sentinel parallel-syncs <master-name> <nums>

    • 当Sentinel节点集合对主节点故障判定达成一致时,Sentinel领导者节点会做故障转移操作,选出新的主节点,原来的从节点会向新的主节点发起复制操作;parallel-syncs就是用来限制在一次故障转移之后,每次向新的主节点发起复制操作的从节点个数.
  • sentinel fail-over-timeout <master-name> <times>

    • fail-over-timeout: 故障转移超时时间,但实际上它的作用于故障转移的各个阶段
      • a) 选出合适从节点
      • b) 晋升选出的从节点为主节点
      • c) 命令其余从节点复制新的主节点
      • d) 等待原来节点恢复后命令它去复制新的主节点
    • fail-over-timeout的作用具体体现在四个方面
      • 若Redis Sentinel对一个主节点故障转移失败,那么下次再对该主节点做故障转移的起始时间是fail-over-timeout的2倍;
      • 在b)阶段,若Sentinel节点向a)阶段选出来的从节点执行slaveof no one一直失败,当此过程超过fail-over-timeout时,则表示故障转移失败;
      • 在b)阶段如果执行成功,Sentinel节点还会执行info命令来确认a)阶段选出的节点确定晋升为主节点,如果此过程执行时间超过fail-over-timeout时,则表示故障转移失败;
      • 若c)阶段执行时间超过fail-over-timeout(不包含复制时间),则表示故障转移失败.注意即使超过了这个时间,Sentinel节点也会最终配置从节点同步最新的主节点;
  • sentinel auth-pass <master-name> <password>

    • 若Sentinel监控的主节点配置了密码,sentinel auth-pass配置通过添加主节点的密码,防止Sentinel节点对主节点无法监控;
  • sentinel notification-script <master-name> <script-path>

    • sentinel notification-script作用是在故障转移期间,当一些告警级别的Sentinel事件发生(指重要事件,例如-sdown: 客观下线,-odown: 主观下线)时,会触发对应路径的脚本,并想脚本发送响应的事件参数;

      1
      2
      3
      4
      5
      #!/bin/sh
      # 获取所有参数
      msg=$*
      # 报警脚本或接口,将msg作为参数
      exit 0
  • sentinel client-reconfig-script <master-name> <script-path>

    • sentinel client-reconfig-script作用是在故障转移结束后,会触发对应路径的脚本,并向脚本发送故障转移结果的参数.

    • 当故障转移结束,每个Sentinel节点会将故障转移的结果发送的脚本,具体参数如下:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      <master-name> <role> <state> <from-ip> <from-port> <to-ip> <from-port>

      <master-name>: 主节点名
      <role>: Sentinel节点的角色,分别是leader和observer,leader代表当前Sentinel节点是领导者,是它进行的故障转移;observer是其余Sentinel节点
      <state>
      <from-ip>: 原主节点的ip地址
      <from-port>: 原主节点的端口
      <to-ip>: 新主节点的ip地址
      <from-port>: 新主节点的端口
    • 其中sentinel notification-scriptsentinel client-reconfig-script有几点需要注意

      • <script-path>必须有可执行权限

      • <script-path>开头必须包含shell脚本(例如#!/bin/sh),否则事件发生时Redis将无法执行脚本产生如下错误:

        1
        -script-error /usr/local/redis-6.2.2/script notification.sh
      • Redis规定脚本的最大执行时间不能超过60秒,超过后脚本将被杀掉

      • 如果shell脚本以exit 1结束,那么脚本稍后重试执行,如果以exit 2或者更高结束,那么脚本重试.正常返回值是exit 0

调整配置

  • Sentinel节点也支持动态地设置参数,而且和普通的Redis数据节点一样并不是支持所有的参数

    1
    sentinel set <param> <value>
    参数 使用说明
    quorm sentinel set mymaster quorum 2
    down-after-millisenconds sentinel set mymaster down-after-millisenconds 30000
    failover-timeout sentinel set mymaster fail-over-timeout 360000
    parallel-syncs sentinel set mymaster parallel-syncs 2
    notification-script sentinel set mymaster notification-script /opt/xx.sh
    client-recoding-script sentinel set mymaster client-recoding-script /opt/yy.sh
    auth-pass sentinel set mymaster auth-pass masterPassword
    • sentinel set命令只对当前Sentinel节点有效
    • sentinel set 如果执行成功会立即刷新配置文件,这点和Redis普通数据节点设置配置需要执行config rewrite刷新配置文件不同;
    • 建议所有Sentinel节点的配置尽可能一致,这样在故障发现和转移是比较容易达成一致;
    • Sentinel对外不支持config命令

API

  • sentinel masters: 展示所有被监控的主节点状态以及相关的统计信息;

  • sentinel mater <master-name>: 展示指定<master-name>的主节点状态以及相关的统计信息;

  • sentinel slaves <master-name>: 展示指定<maste-name>的从节点状态以及相关统计信息

  • sentinel sentinels <master-name>: 展示指定<master-name>的Sentinel节点集合(不包含当前Sentinel节点)

  • sentinel get-master-addr-by-name <master-name>: 返回指定<master-name>主节点的IP地址和端口

  • sentinel reset <pattern>: 当前Sentinel节点对符合<pattern>(通配符风格)主节点的配置进行重置,包含清除主节点的相关状态(例如故障转移),重新发现从节点和Sentinel节点

  • sentinel failover <master-name>

    • 对指定<master-name>主节点进行强制故障转移(没有和其他Sentinel节点协商),当故障转移完成后,其他Sentinel节点按照故障转移的结果更新自身配置.
  • sentinel chquorum <master-name>

    • 检测当前可达的Sentinel节点总数是否达到<quorum>的个数.例如quorum=3,而当前可达的Sentinel节点的个数为2个,那么将无法记性故障转移,Redis Sentinel的高可用特性也将失去;
  • sentinel flushconfig

    • 将Sentinel节点的配置强制刷到磁盘上,这个命令Sentinel节点自身用得比较多.
  • sentinel remove <master-name>: 取消当前Sentinel节点对于制定<master-name>主节点的监控;

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

  • sentinel set <param> <value>

  • sentinel is-master-down-by-addr <ip> <port> <current_epoch> <runid>

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    ip: 主节点IP
    port: 主节点端口
    current_epoch: 当前配置纪元
    runid: 此参数有两种类型,不同类型决定了此API的作用
    * 当runid等于"*"时,作用是Sentinel节点直接交换主节点下线的判定
    * 当runid等于当前Sentinel节点的runid时,作用是当前Sentinel节点希望目标同意自己成为领导者的请求;

    返回结果包含三个参数:
    * down_state: 目标Sentinel节点对于主节点的下线判断,1是下线,0是在线;
    * leader_runid: 当leader_runid等于"*",代表返回结果是用来做主节点是否不可达;当leader_runid等于具体的runid,代表目标节点同意runid成为领导者
    * leader_epoch: 领导者纪元
  • Sentinel节点之间用来交换对主节点是否下线的判断,根据参数的不同,还可以作为Sentinel领导者选举的通信方式;

Redis Sentinel客户端基本实现原理

  • 实现一个Redis Sentinel客户端的基本步骤

    • 遍历Sentinel节点集合获取一个可用的Sentinel节点,Sentinel节点之间可以共享数据,所有从任意一个Sentinel节点获取主节点的信息都是可以的;

      img

    • 通过sentinel get-master-addr-by-name master-name这个API来获取对应主节点的相关信息;

      img

    • 验证当前获取的"主节点"是真正的主节点,这样做的目的是为了防止故障转移期间主节点的变化;

      img

    • 保持Sentinel节点集合的"联系",时刻获取关于主节点的相关’“信息”

      img

Redis Sentinel的任务

  • 监控
    • 监控是指哨兵进程在运行时,周期性地给所有的主从库发送PING命令,检测它们是否仍然在线运行.如果从库没有在规定时间内响应哨兵的PING命令,哨兵就会把它标记为“下线状态”;同样,如果主库也没有在规定时间内响应哨兵的PING命令,哨兵就会判定主库下线,然后开始自动切换主库的流程.
  • 选主(选择主库)
    • 主库挂了以后,哨兵就需要从很多个从库里,按照一定的规则选择一个从库实例,把它作为新的主库.这一步完成后,现在的集群里就有了新主库.
  • 通知
    • 在执行通知任务时,哨兵会把新主库的连接信息发给其他从库,让它们执行replicaof命令,和新主库建立连接,并进行数据复制.同时,哨兵会把新主库的连接信息通知给客户端,让它们把请求操作发到新主库上.

img

  • 在这三个任务中,通知任务相对来说比较简单,哨兵只需要把新主库信息发给从库和客户端,让它们和新主库建立连接就行,并不涉及决策的逻辑.但是,在监控和选主这两个任务中,哨兵需要做出两个决策:
    • 在监控任务中,哨兵需要判断主库是否处于下线状态;
    • 在选主任务中,哨兵也要决定选择哪个从库实例作为主库.

哨兵模式原理

一套合理的监控机制是Sentinel节点判断节点不可达的重要保证,Redis Sentinel通过三个定时监控任务完成对各个节点发现和监控.

  • 每隔10秒,每个Sentinel节点会向主节点和从节点发送info命令获取最新的拓扑结构;

    • 这个定时任务的作用可以表现在三个方面:
      • 通过向主节点执行info命令,获取从节点的信息,这也是为什么Sentinel节点不需要显示配置监控从节点;
      • 当有新的从节点加入时都可以立刻感知出来;
      • 节点不可达或者故障转移后,可以通过info命令实时更新节点拓扑信息
      • 通过pub/sub机制,哨兵之间可以组成集群,同时,哨兵又通过INFO命令,获得了从库连接信息,也能和从库建立连接,并进行监控了.
    img
  • 每隔2秒,每个Sentinel节点会向Redis master节点的_sentinel_:hello频道发送该Sentinel节点对于主节点的判断以及当前Sentinel节点的信息,同时每个Sentinel也会订阅该频道,来了解其他Sentinel节点以及它们对主节点的判断.

    • 所以这个定时任务完成一下两个工作:

      • 发现新的Sentinel节点:通过订阅主节点的_sentinel_:hello了解其他的Sentinel节点信息,如果是新加入的Sentinel节点,将该Sentinel节点信息保存起来,并与该Sentinel节点创建连接;
      • Sentinel节点之间交换主节点的状态,作为客观下线以及领导者选举的依据;
    • Sentinel节点的publish的消息格式如下:

      1
      <Sentinel节点Ip><Sentinel节点端口><Sentinel节点runId><Sentinel节点配置版本><主节点名字><主节点Ip><主节点端口><主节点配置版本>
    img
  • 每隔1秒,每个Sentinel节点会向主节点,从节点,其余Sentinel节点发送一条ping命令做一次心跳检测,来确认这些节点当前是否可达.

客观下线

  • 当Sentinel主观下线的节点是主节点,该Sentinel节点会通过sentinel is-master-down-by-addr命令向其他Sentinel节点询问对主节点的判断,当超过<quorum>个数,Sentinel节点认为主节点确实有问题,这是该Sentinel节点会做出客观下线的决定
  • 客观下线的含义比较明显,也就是大部分Sentinel节点都对主节点的下线做了同意的判定.

主观下线

  • 三个定时任务,每隔Sentinel节点会每隔1秒对主节点,从节点,其他Sentinel节点发送ping命令做心跳检测,当这些节点ping命令的时间超过down-after-milliseconds没有进行有效回复,Sentinel节点就会对该节点做失败判定,这个行为叫做主观下线;

  • 哨兵进程会使用PING命令检测它自己和主、从库的网络连接情况,用来判断实例的状态

    img
  • 从节点、Sentinel在主观下线后,没有后续的故障转移操作;

    • 如果检测的是从库,那么,哨兵简单地把它标记为“主观下线”就行了,因为从库的下线影响一般不太大,集群的对外服务不会间断.
  • 如果检测的是主库,那么,哨兵还不能简单地把它标记为“主观下线”,开启主从切换.因为很有可能存在这么一个情况:那就是哨兵误判了,其实主库并没有故障.可是,一旦启动了主从切换,后续的选主和通知操作都会带来额外的计算和通信开销.

    • 误判.很简单,就是主库实际并没有下线,但是哨兵误以为它下线了.误判一般会发生在集群网络压力较大、网络拥塞,或者是主库本身压力较大的情况下.

领导者Sentinel节点选举

  • 在投票过程中,任何一个想成为Leader的哨兵,要满足两个条件

    • 第一,拿到半数以上的赞成票;
    • 第二,拿到的票数同时还需要大于等于哨兵配置文件中的quorum值.
    • 以3个哨兵为例,假设此时的quorum设置为2,那么,任何一个想成为Leader的哨兵只要拿到2张赞成票,就可以了.
  • Redis采用Raft算法实现领导者选举,大致思路:

    • 在每个在线的Sentinel节点都有资格成为领导者,当它确认了主节点主观下线的时候,会向其他Sentinel节点发送sentinel is-master-down-by-addr命令要求将自己设置为领导者
    • 收到命令的Sentinel节点,如果没有同意过其他Sentinel节点的sentinel is-master-down-by-addr命令,则会同意当前请求,否则拒绝;
    • 若该Sentinel节点发现自己的票数已经大于等于max(quorum,num(sentinels)/2+1),那么它将成为领导者;
    • 如果此过程没有选举领导者,将进入下一次选举;

故障转移

  • 哨兵选择新主库的过程称为“筛选+打分”.简单来说,我们在多个从库中,先按照一定的筛选条件,把不符合条件的从库去掉.然后,我们再按照一定的规则,给剩下的从库逐个打分,将得分最高的从库选为新主库

    img

    • 设想一下,如果在选主时,一个从库正常运行,我们把它选为新主库开始使用了.可是,很快它的网络出了故障,此时,我们就得重新选主了.这显然不是我们期望的结果.
    • 所以,在选主时,除了要检查从库的当前在线状态,还要判断它之前的网络连接状态.如果从库总是和主库断连,而且断连次数超出了一定的阈值,我们就有理由相信,这个从库的网络状况并不是太好,就可以把这个从库筛掉了.
    • 具体怎么判断呢?你使用配置项down-after-milliseconds * 10.其中,down-after-milliseconds是我们认定主从库断连的最大连接超时时间.如果在down-after-milliseconds毫秒内,主从节点都没有通过网络联系上,我们就可以认为主从节点断连了.如果发生断连的次数超过了10次,就说明这个从库的网络状况不好,不适合作为新主库.
  • 被选举为的Sentinel领导者节点复制故障转移,具体步骤如下:

    • 在从节点列表中选出一个节点作为新的主节点,选择方法如下:

      • 过滤"不健康"(主观下线,断线),5秒内没有回复过Sentinel节点ping响应,与主节点失联超过down-after-miliseconds*10秒的数据节点;
      • 优先级最高的从库得分高: 选择slave-priority(从节点优先级)最高的从节点列表,如果存在则返回,不存在则继续第二轮选举;
      • 和旧主库同步程度最接近的从库得分高:选择复制偏移量最大的从节点(复制的最完整的数据节点),如果存在则返回,不存在则继续第三轮选举
      • ID号小的从库得分高: 每个实例都会有一个ID,这个ID就类似于这里的从库的编号.目前,Redis在选主库时,有一个默认的规定:在优先级和复制进度都相同的情况下,ID号最小的从库得分最高,会被选为新主库.
    • Sentinel领导者节点会让第一步选出的从节点执行slave no one命令让其成为主节点;

    • Sentinel领导者节点会向剩余的从节点发送命令,让它们成为新主节点的从节点,复制规则和parallel-syncs参数有关;

    • Sentinel节点集合会将原来的主节点更新为从节点,并保持对其关注,当其回复后命令它去复制新的主节点;

基于pub/sub机制的客户端事件通知

事件 状态 说明
+reset-master <instance details> 主节点被重置
+slave <instance details> 一个新的从节点被发现并关联
+failover-state-reconf-slaves <instance details> 故障转移进入reconf-slaves状态
从库重新配置事件 +slave-reconf-sent <instance details> 领导者Sentinel节点命令其他从节点复制新的主节点
从库重新配置事件 +slave-reconf-inprog <instance details> 从节点正在重新配置主节点的slave,但同步过程上未完成
从库重新配置事件 +slave-reconf-done <instance details> 其余从节点完成了和新主节点的同步
+sentinel <instance details> 一个新的sentinel节点被发现并关联
主库下线事件 +sdown <instance details> 实例进入"主观下线"状态
主库下线事件 -sdown <instance details> 实例退出"主观下线"状态
主库下线事件 +odown <instance details> 实例进入"客观下线"状态
主库下线事件 -odown <instance details> 实例退出"客观下线"状态
+new-epoch <instance details> 当前纪元被更新
+try-failover <instance details> 故障转移开始
+elected-leader <instance details> 选出了故障转移的Sentinel节点
+failover-state-select-slave <instance details> 故障转移进入select-slave状态(寻找合适的从节点)
no-good-slave <instance details> 没有找到合适的从节点
selected-slave <instance details> 找到了合适的从节点
failover-state-send-slaveof-noone <instance details> 故障转移进入failover-state-send-slaveof-noone(对找到的从节点执行slave no one)
failover-end-for-timeout <instance details> 故障转移由于超时而终止
failover-end <instance details> 故障转移顺利完成
新主库切换 switch-master <master-name><oldip> <oldport> <newip> <newport> 更新主节点信息,这个许多客户端重点关注的
  • <instance details>格式如下

    1
    2
    3
    <instance-type> <name> <ip> <port> @ <master-name> <master-ip> <master-port>
    # 示例
    +slave-reconf-done slave 1273.0.0.1:6381 @ holelin 127.0.0.1 6380

运维实战

节点下线区分

  • 临时下线: 暂时将节点关掉,之后还会重新启动,继续提供服务;
  • 永久下线: 将节点关掉后不再使用,需要做一些清理工作,如删除配置文件,持久化文件,日志文件;

节点下线原因

  • 节点所在的机器出现了不稳定或者即将过保被回收;
  • 节点所在的机器性能比较差或者内存比较小,无法支撑应用方的需求;
  • 节点自身出现不正常情况,需要快速处理;

节点下线

  • 对主节点进行手动下线
    • 比较合理的做法是选出"合适"(例如性能更高的机器)的从节点,使用sentinel failover <master-name> 功能将从节点晋升为主节点,只需要在任意可用的Sentinel节点中执行该命令;
    • Redis Sentinel存在多个从节点时,若想要指定从节点晋升为主节点,可以将其他从节点的slavepriority配置为0,但是需要在failover后,将slavepriority调回原值
  • 对从节点和Sentinel节点进行手动下线
    • 需要确定好是临时下线和永久下线后执行响应操作即可,如果使用读写分离,下线从节点需要保证应用方可以感知从节点的下线变化,从而把读取请求路由到其他节点;
    • 需要注意的是,Sentinel节点依然会这些下线的节点进行定期监控,这是由Redis Sentinel的设计思路所决定的.可能会造成一定的网络资源浪费;

节点上线

  • 添加从节点
    • 原因
      • 使用了读写分离,但先有的从节点无法支撑应用方的流量;
      • 主节点没有可用的从节点,无法支持故障转移;
      • 添加一个性能更好的从节点,利用手动failover替换主节点;
    • 添加方法: 使用slaveof <master-name> <master-port>,节点启动后将被Sentinel节点自动发现;
  • 添加Sentinel节点
    • 原因
      • 当前Sentinel节点数量不够,无法达到Redis Sentinel健壮性要求或者无法达到票数
      • 原Sentinel节点所在机器需要下线
    • 添加方法: 添加Sentinel节点,配置文件添加配置信息sentinel monitor主节点的配置,节点启动后将会被其他的Sentinel节点自动发现;
  • 添加主节点
    • 因为Redis Sentinel中只能有一个主节点,所以不需要添加主节点

Sentinel支持的命令

img

哨兵模式的优缺点

优点

  • 哨兵模式是基于主从模式的,所有主从的优点,哨兵模式都具有.

  • 主从可以自动切换,系统更健壮,可用性更高.

缺点

  • 哨兵依然是主从模式,没法解决写的压力,只能减轻读的压力
  • 存储得不到扩容,存储数据总量是主的数据总量
  • 当主服务器宕机后,从服务器切换成主服务器的这段时间,服务不可用.

面试题

哨兵在操作主从切换的过程中,客户端能否正常地进行请求操作?

  • 如果客户端使用了读写分离,那么读请求可以在从库上正常执行,不会受到影响.但是由于此时主库已经挂了,而且哨兵还没有选出新的主库,所以在这期间写请求会失败,失败持续的时间 = 哨兵切换主从的时间 + 客户端感知到新主库 的时间.
  • 如果不想让业务感知到异常,客户端只能把写失败的请求先缓存起来或写入消息队列中间件中,等哨兵切换完主从后,再把这些写请求发给新的主库,但这种场景只适合对写入请求返回值不敏感的业务,而且还需要业务层做适配,另外主从切换时间过长,也会导致客户端或消息队列中间件缓存写请求过多,切换完成之后重放这些请求的时间变长.
  • 哨兵检测主库多久没有响应就提升从库为新的主库,这个时间是可以配置的(down-after-milliseconds参数).配置的时间越短,哨兵越敏感,哨兵集群认为主库在短时间内连不上就会发起主从切换,这种配置很可能因为网络拥塞但主库正常而发生不必要的切换,当然,当主库真正故障时,因为切换得及时,对业务的影响最小.如果配置的时间比较长,哨兵越保守,这种情况可以减少哨兵误判的概率,但是主库故障发生时,业务写失败的时间也会比较久,缓存写请求数据量越多.

应用程序不感知服务的中断,还需要哨兵和客户端做些什么?

  • 当哨兵完成主从切换后,客户端需要及时感知到主库发生了变更,然后把缓存的写请求写入到新库中,保证后续写请求不会再受到影响,具体做法如下:
    • 哨兵提升一个从库为新主库后,哨兵会把新主库的地址写入自己实例的pubsub(switch-master)中.客户端需要订阅这个pubsub,当这个pubsub有数据时,客户端就能感知到主库发生变更,同时可以拿到最新的主库地址,然后把写请求写到这个新主库即可,这种机制属于哨兵主动通知客户端.
    • 如果客户端因为某些原因错过了哨兵的通知,或者哨兵通知后客户端处理失败了,安全起见,客户端也需要支持主动去获取最新主从的地址进行访问.
    • 所以,客户端需要访问主从库时,不能直接写死主从库的地址了,而是需要从哨兵集群中获取最新的地址(sentinel get-master-addr-by-name命令),这样当实例异常时,哨兵切换后或者客户端断开重连,都可以从哨兵集群中拿到最新的实例地址.
    • 一般Redis的SDK都提供了通过哨兵拿到实例地址,再访问实例的方式,我们直接使用即可,不需要自己实现这些逻辑.当然,对于只有主从实例的情况,客户端需要和哨兵配合使用,而在分片集群模式下,这些逻辑都可以做在proxy层,这样客户端也不需要关心这些逻辑了,Codis就是这么做的.

哨兵集群中有实例挂了,怎么办?会影响主库状态判断和选主吗?

  • "拜占庭将军"问题
  • 存在故障节点时,只要集群中大多数节点状态正常,集群中大多数节点状态正常,集群依旧可以对外提供服务.

哨兵集群多数实例达成共识,判断出主库"客观下线"后,由哪个实例来执行主从切换?

  • 共识算法:指的是集群中多个节点如何就一个问题达成共识.如Paxos,Raft.