Sentinel哨兵

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

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

启动与初始化

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

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

  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 频道(订阅的好处是可以防止消息丢失)

与服务器进行通信

获取主服务器信息

如上图所示:

  • Sentinel 默认会以每10秒一次的频率发送 INFO 命令,获取主节点的信息
  • 主节点会返回自身和其从节点的信息
  • Sentinel 接收到信息后,更新自己的 masters 字典,如果有新节点,则创建之

获取从服务器信息

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

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

并更新自己保存的信息。

发送频道信息

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

PUBLISH __sentinel__:hello "xxx"

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

即两部分:

  • 自己的信息
  • 主服务器的信息

接收频道消息

前面提到了,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; 变量(上文提到过)指向的 sentinelRedisInstance 的 sentinels 字典变量(这个变量保存了所有监视这个服务器的 Sentinel)

  • 键位Sentinel的IP和端口
  • 值指向sentinel实例

而具体的更新流程是:

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 的回复有两种:

  • 有效回复:+PING、-LOADING、-MASTERDOWN
  • 无效回复:其他内容或超时

如果一个实例在 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 有几个参数,包含了:

  • ip、port:被审判的主机的ip和端口号
  • current_epoch:当前的配置纪元,用以选举领头 Sentinel 进行故障转移
  • runid:
    • * 表示判断客观下线
    • 如果是 Sentinel 的运行 ID 则用来选举领头

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

  • down_state:是否下线
  • leader_runid:
    • * 表示仅仅用以检测服务器的下线状态
    • 如果是领头 Sentinel 的 ID 则说明用于选举领头 Sentinel
  • leader_epoch:
    • 如果leader_runid为 * ,则为0
    • 否则为配置纪元

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

  • 判断是否下线
  • 选举领头 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

Redis设计与实现3.2:Sentinel的更多相关文章

  1. 【笔记】《Redis设计与实现》chapter16 Sentinel

    16.1 启动并初始化Sentinel 初始化服务器 Sentinel本质上只是运行在特殊模式下的Redis服务器,启动第一步就是初始化一个普通的Redis服务器 使用Sentinel专用代码 使用r ...

  2. Redis | 第12章 Sentinel 哨兵模式《Redis设计与实现》

    目录 前言 1. 启动并初始化 Sentinel 2. Sentinel 与服务器间的默认通信 2.1 获取主服务器信息 2.2 获取从服务器信息 2.3 向主服务器和从服务器发送信息 3. 接受来自 ...

  3. Redis容灾部署(哨兵Sentinel)

    Redis容灾部署(哨兵Sentinel) 哨兵的作用 1. 监控:监控主从是否正常2. 通知:出现问题时,可以通知相关人员3. 故障迁移:自动主从切换4. 统一的配置管理:连接者询问sentinel ...

  4. 重读redis设计与实现

    重读了一遍redis设计与实现,这次收获也不错,把之前还有些疑惑的点:redis跳跃表的原理.redis持久化的方法.redis复制.redis sentinel.redis集群等,都重新熟悉了一遍, ...

  5. 《Redis设计与实现》

    <Redis设计与实现> 基本信息 作者: 黄健宏 丛书名: 数据库技术丛书 出版社:机械工业出版社 ISBN:9787111464747 上架时间:2014-6-3 出版日期:2014 ...

  6. 探索Redis设计与实现13:Redis集群机制及一个Redis架构演进实例

    本文转自互联网 本系列文章将整理到我在GitHub上的<Java面试指南>仓库,更多精彩内容请到我的仓库里查看 https://github.com/h2pl/Java-Tutorial ...

  7. Redis设计原理简介

    学完MySQL InnoDB之后,又开始学习和研究Redis. 首先介绍下书:<Redis设计与实现>第二版 黄健宏著,机械工业出版社,388页,基于redis3.0版本.版本有点低,这个 ...

  8. 《Redis设计与实现》知识点目录

    Redis设计与实现 第一部分 数据结构与对象 第二章 简单动态字符串 p8 简单动态字符串SDS 2.1 SDS的定义 p9 每个sds.h/sdshdr结构表示一个SDS值 2.2 SDS与C字符 ...

  9. Redis | 第11章 服务器的复制《Redis设计与实现》

    目录 前言 1. 旧版复制功能的实现 1.1 同步与命令传播 1.2 旧版复制功能的缺陷 2. 新版复制功能的实现 2.1 部分重同步的实现原理 3. PSYNC 命令的实现 4. 复制的详细步骤 4 ...

随机推荐

  1. js技术之转换为大写toUpperCase()

    案例:把所有单词以空格为分割并将首字母转为大写 <!DOCTYPE html><html lang="en"><head> <meta c ...

  2. Citus 分布式 PostgreSQL 集群 - SQL Reference(手动查询传播)

    手动查询传播 当用户发出查询时,Citus coordinator 将其划分为更小的查询片段,其中每个查询片段可以在工作分片上独立运行.这允许 Citus 将每个查询分布在集群中. 但是,将查询划分为 ...

  3. mian中的argv调用时为什么不是*argv

    c++main函数char * argv[]是个指针数组,元素是指针,为何argv[1]得到不是地址? 照我的理解char *argv[]保存的应该是一组指针,即地址,每个地址中保存的是char类型变 ...

  4. C#通过LDAP访问目录服务

    C#通过LDAP访问目录服务 本文介绍如何编写C#程序通过LDAP协议访问微软目录服务获得用户在目录中的属性信息.在开始部分先简单句介绍LDAP协议,然后是技术比较及实现部分. 目录 什么是LDAP? ...

  5. mobileSite开发Tips

    1,根节点(html,body)overflow属性会遮盖fixed定位元素 2,fixed元素点击时会弹出底部的菜单,应该是浏览器特性(safari) 3,fixed元素使用后,在body里面使用p ...

  6. 小程序的初次遇见,使用mpvue搭建模板

    由于公司业务需求的需要,在这一周需要开发小程序,加急看了下小程序的文档,发现用其原生来编写程序不是很顺手,公司前端用的技术栈是vue, 询问了谷哥和度娘发现大部分推荐了 wepy和 mpvue,对比了 ...

  7. java栈stack和堆heap的工作原理,用途及区别?举例说明

    java堆和栈的区别[新手可忽略不影响继续学习] Java中内存分成两种:一种是栈stack,一种是堆heap.函数中的一些基本类型的变量(int, float)和对象的引用变量(reference) ...

  8. java中请给出例子程序:找出n到m之间的质数。

    9.1 找出100到200之间的质数.  public class Test {     public static void main(String[] args){         for (in ...

  9. 深入研究const(es6特性)

    const  申明常量 var str = 'es6' console.log(window.str) // es6 属于顶层对象window const不属于顶层对象window const str ...

  10. vue日历(纯 js,没用任何插件和组件)

    效果图: 代码:   <template> <div class="calender"> <div class="top"> ...