数据库
首页 > 数据库> > Redis设计与实现2.2:Sentinel

Redis设计与实现2.2:Sentinel

作者:互联网

Sentinel哨兵

这是《Redis设计与实现》系列的文章,系列导航:Redis设计与实现笔记

哨兵:监视、通知、自动故障恢复

启动与初始化

Sentinel 的本质只是一个运行在特殊模式下的 Redis 服务器,所以启动 Sentinel 的步骤如下:

  1. 初始化一个普通的 Redis 服务器,不过也有一些不同:

    image_lymtics

  2. 将一部分 Redis 服务器使用的代码替换成 Sentinel 专用代码

    举两个例子:

    1. 服务器端口由 redis.h/REDIS_SERVERPORT 修改为 sentinel.c/REDIS_SENTINELPORT

    2. 服务器的命令表替换为 sentinel.c/sentinelcmds

      // 服务器在 sentinel 模式下可执行的命令
      struct redisCommand sentinelcmds[] = {
          {"ping",pingCommand,1,"",0,NULL,0,0,0,0,0},
          {"sentinel",sentinelCommand,-2,"",0,NULL,0,0,0,0,0},
          {"subscribe",subscribeCommand,-2,"",0,NULL,0,0,0,0,0},
          {"unsubscribe",unsubscribeCommand,-1,"",0,NULL,0,0,0,0,0},
          {"psubscribe",psubscribeCommand,-2,"",0,NULL,0,0,0,0,0},
          {"punsubscribe",punsubscribeCommand,-1,"",0,NULL,0,0,0,0,0},
          {"publish",sentinelPublishCommand,3,"",0,NULL,0,0,0,0,0},
          {"info",sentinelInfoCommand,-1,"",0,NULL,0,0,0,0,0},
          {"shutdown",shutdownCommand,-1,"",0,NULL,0,0,0,0,0}
      };
      
  3. 初始化 Sentinel 状态

    可以看一下这个状态的定义:

    /* Main state. */
    /* Sentinel 的状态结构 */
    struct sentinelState {
    
    // 当前纪元
    uint64_t current_epoch;     /* Current epoch. */
    
    // 保存了所有被这个 sentinel 监视的主服务器
    // 字典的键是主服务器的名字
    // 字典的值则是一个指向 sentinelRedisInstance 结构的指针
    dict *masters; 
    
    // 是否进入了 TILT 模式?
    int tilt;           /* Are we in TILT mode? */
    
    // 目前正在执行的脚本的数量
    int running_scripts;    /* Number of scripts in execution right now. */
    
    // 进入 TILT 模式的时间
    mstime_t tilt_start_time;   /* When TITL started. */
    
    // 最后一次执行时间处理器的时间
    mstime_t previous_time;     /* Last time we ran the time handler. */
    
    // 一个 FIFO 队列,包含了所有需要执行的用户脚本
    list *scripts_queue;    /* Queue of user scripts to execute. */
    
    } sentinel;
    
  4. 初始化 Sentinel 状态的 masters 属性

    dict *masters; 是一个字典结构,键是被监视主服务器的名称,值是主服务器对应的 sentinel.c/sentinelReidsInstance 结构

    这个初始化是根据被载入的 Sentinel 配置文件来进行的

  5. 创建网络连接

    Sentinel 将成为主服务器的客户端,它可以向主服务器发送命令,并从命令回复中获取相关的信息。会创建两个连向主服务器的异步网络连接:

    • 一个是命令连接,专门用于向主服务器发送命令,并接收命令回复
    • 一个是订阅连接,专用用于订阅主服务器的 __sentinel__:hello 频道(订阅的好处是可以防止消息丢失)

与服务器进行通信

获取主服务器信息

image_lymtics

如上图所示:

获取从服务器信息

当 Sentinel 发现主服务器有新的从节点时,会创建到从节点的命令连接和订阅链接:

image_lymtics

同样的,以10秒一次的频率发送 INFO 命令并获取返回信息:

image_lymtics

并更新自己保存的信息。

发送频道信息

默认情况下,Sentinel会以每两秒一次的频率,通过命令向所有被监视的主服务器和从服务器发送命令:

PUBLISH __sentinel__:hello "xxx"

这条命令向服务器的 __sentinel__ 频道发送了一条消息,在上面我用"xxx"表示出来了,其具体组成有:

image_lymtics

即两部分:

接收频道消息

前面提到了,Sentinel 会向服务器的频道发送信息:

PUBLISH __sentinel__:hello "xxx"

另一方面,Sentinel 还会订阅所有被监视服务器的频道:

SUBSCRIBE __sentinel__:hello

对于监视同一个服务器的多个 Sentinel 来说,这些消息会被用于更新其他 Sentinel 对发送信息的 Sentinel 的认知,也会被用于更新其他 Sentinel 对被监视服务器的认知。

sequenceDiagram participant s1 as Sentinel participant f as 服务器的hello频道 participant s2 as Sentinel s1 ->> f: "messageA" f -->> s2: "messageA" note over s2: 更新相关数据 f -->> s1: "messageA" note over s1: 是我自己发的啊,那没事了 s2 ->> f: "messageB" f -->> s1: "messageB" note over s1: 更新相关数据 f -->> s2: "messageB" note over s2: 是我自己发的啊,那没事了

而更新的具体数据是:sentinelState 结构体的 dict *masters; 变量(上文提到过)指向的 sentinelRedisInstancesentinels 字典变量(这个变量保存了所有监视这个服务器的 Sentinel)

image_lymtics

而具体的更新流程是:

flowchart LR A[/获取一条信息/] --> B{{是我发的吗}} --Y--> C[/那没事了/] B --N--> 提取数据 --> D{{是否之前见过这个Sentinel}} --Y--> E[/更新/] D --N--> F[/添加/]

这样做的一个好处是,可以自动发现其他 Sentinel,并形成相互连接的网络,而无需手动配置。

Sentinel 之间只会创建命令链接,而不会创建订阅链接。

因为之所以和服务器需要创建订阅链接就是用来发现未知的新的 Sentinel 的。

服务器意外状态

检测主观下线状态

Sentinel 会以每秒一次的频率向所有与他建立了命令简介的实例(包括主、从、Sentinel服务器)发送 PING 命令,并通过返回信息判断实例的状态。

sequenceDiagram participant Sl as Slaver participant M as Master participant Se as Sentinel note over Se: 以本实例的视角来看 participant Se2 as Sentinel loop Every Second Se ->>+ M: PING Se ->>+ Sl: PING Se ->>+ Se2: PING M ->>- Se: REPLY Sl ->>- Se: REPLY Se2 ->>- Se: REPLY end

实例对 PING 的回复有两种:

如果一个实例在 down-after-milliseconds 配置的时间内没有返回有效回复,就会被标记为主观下线状态

检测客观下线状态

Sentinel 也要问问别的监控目标的 Sentinel 的意见,才好决定是否是真的下线了。

sequenceDiagram participant s1 as sentinel participant s2 as sentinel participant s3 as sentinel note over s2: 我先发现的 s2 ->>+ s1: is-master-down-by-addr s2 ->>+ s3: is-master-down-by-addr s1 ->> s1: 解析、检查 s3 ->> s3: 解析、检查 s1 ->>- s2: multi bulk s3 ->>- s2: multi bulk s2 ->> s2: 汇总结果 note over s2: 认为主节点客观下线 note over s2: 我们来进行选举吧!

is-master-down-by-addr 有几个参数,包含了:

multi bulk 是 Sentinel 的返回值(为什么叫这个名字?文档是这么叫的),包含了三个值:

你应该看出来了,上面的两条命令有两种作用:

选举领头 Sentinel

当一个主服务器被判断为客观下线后,监视这个服务器的各个 Sentinel 会进行协商,选举一个领头的 Sentinel 并进行故障转移。

我的理解:

这里只有中间的 Sentinel 确定了客观下线这一事实,其他的 Sentinel 未必认同,但是即便如此,只要有一个 Sentinel 认定了客观下线的情况,其他 Sentinel 也会配合进行选举、故障转移。

选举的策略是:

sequenceDiagram participant s1 as Sentinel participant s2 as Sentinel participant s3 as Sentinel note over s1,s3: 我们都有机会成为Leader note over s1,s2: 我们都发现了目标的主观下线 loop 如果没有选出leader s1 ->>+ s2: 请在epoch任期选举我,我是S1 s2 ->>- s1: 同意 s1 ->>+ s3: 请在epoch任期选举我,我是S1 note over s3: 好的,我这里先到先得 s3 ->>- s1: 同意 s2 ->>+ s3: 请在epoch任期选举我,我是S2 s2 ->>+ s1: 请在epoch任期选举我,我是S2 s1 ->>- s2: 同意 s3 ->>- s2: 抱歉,我选过别人了 note over s1,s3: 不管结果如何,都要epoch++ end s1 -> s1: 选票超过一半,当选leader

如果在给定时限中没有选出leader,则在一段时间后再次进行选举,直到选出leader。

这么一种做法有没有可能在很长的一段时间内都发生选举失败的情况呢?

这个可能要之后学习一下Raft算法的领头选举算法

故障转移

领头 leader 将对已下线的主服务进行故障转移操作:

  1. 选一个新的主服务器
  2. 让前任主服务器的所有从服务器跟着现任服务器
  3. 将前任设置为现任的从服务器

如何选新的服务器:

  • 筛选排除:
    • 下线的、断线状态的
    • 最近5秒内都没有回复过leader的INFO命令的服务器
    • 与前任主服务器断开超过 down-after-milliseconds * 10 的服务器
  • 优先选择:
    • 优先级较高
    • 复制偏移量较大
    • ID最小
sequenceDiagram participant OM as Old Master participant s as Slave1 participant NM as Slave2 participant SL as Sentinel Leader SL ->> NM: Slaveof no one loop every second SL ->>+ NM: INFO(你小子谋反地怎么样了) end NM ->>- SL: REPL(我已经成为Master了) SL ->> s: Slaveof Slave2 note over OM: 恢复上线 SL ->> OM: Slaveof Slave2

标签:participant,s2,s1,Redis,sentinel,服务器,Sentinel,2.2
来源: https://www.cnblogs.com/lymtics/p/16206203.html