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; 变量(上文提到过)指向的 sentinelRedisInstancesentinels 字典变量(这个变量保存了所有监视这个服务器的 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. vue2与vue3的区别

    template <template> <div class="wrap"> <div>{{ num }}</div> <Bu ...

  2. 总结一下各种0.5px的线

    在PC端用1px的边框线,看起来还好,但在手机端看起来就很难看了,而0.5px的分割线会有种精致的感觉.用普通写法border:solid 0.5px red;iPhone可以正常显示,android ...

  3. webpack系列——webpack3导入jQuery的新方案

    本文的目的 拒绝全局导入jQuery!! 拒绝script导入jQuery!! 找到一种只在当前js组件中引入jQuery,并且使用webpack切割打包的方案! 测试环境 以下测试在webpack3 ...

  4. c++语法拾遗,一些细节与特性

    写了2年多的C+STL的acmer,在学习<C++ primer>时总结的一些少见的语法特性与细节.总体还是和题目说的一样这是一篇 c++ 拾遗. 1 变量和基本类型 1.1 基本类型 1 ...

  5. String和int、long、double等基本数据类型的转换

    学习目标: 掌握Java的基本数据类型与String的转换 学习内容: 1.转化规则 String 转 基本数据类型 基本数据类型 变量 = 包装类.Parse基本数据类型(String); // c ...

  6. 阿里云申请SSL证书 并部署到SpringBoot项目

    前提 有一台阿里云的服务器(安装了java环境) 有已经备案的域名,并且域名绑定上面的服务器 申请SSL证书 申请教程:https://blog.csdn.net/yunweifun/article/ ...

  7. 实现深拷贝还在用JSON.parse(JSON.stringify(obj))?带你用JS实现一个完整版深拷贝函数

    使用JavaScript实现深拷贝 1.JSON序列化实现深拷贝 在JS中,想要对某一个对象(引用类型)进行一次简单的深拷贝,可以使用JSON提供给我们的两个方法. JSON.stringfy():可 ...

  8. Mysql入门学习day2随笔2

    事务 什么是事务 要么都成功,要么都失败 事务原则 原子性:针对一个事务,两个步骤一起成功或一起失败 一致性:最终一致性,例如A.B之间的转账,无论两个账户如何操作,两账户的总价值不会变 隔离性:针对 ...

  9. JavaScript学习总结2-对象

    JavaScript中对象除了最后一个属性以外都在结尾加逗号,同时所有属性都要在{ }内 1 <!DOCTYPE html> 2 <html lang="en"& ...

  10. vue动态绑定属性--基本用法及动态绑定class

    动态绑定属性v-bind:,语法糖形式:省略v-bind,仅写一个冒号. 一.动态绑定基本属性 1 <body> 2 <!-- v-bind 动态绑定属性-基本用法 --> 3 ...