Redis设计与实现3.2:Sentinel
Sentinel哨兵
这是《Redis设计与实现》系列的文章,系列导航:Redis设计与实现笔记
哨兵:监视、通知、自动故障恢复
启动与初始化
Sentinel 的本质只是一个运行在特殊模式下的 Redis 服务器,所以启动 Sentinel 的步骤如下:
初始化一个普通的 Redis 服务器,不过也有一些不同:
将一部分 Redis 服务器使用的代码替换成 Sentinel 专用代码
举两个例子:
服务器端口由
redis.h/REDIS_SERVERPORT
修改为sentinel.c/REDIS_SENTINELPORT
服务器的命令表替换为
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}
};
初始化 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;
初始化 Sentinel 状态的 masters 属性
dict *masters;
是一个字典结构,键是被监视主服务器的名称,值是主服务器对应的sentinel.c/sentinelReidsInstance
结构这个初始化是根据被载入的 Sentinel 配置文件来进行的
创建网络连接
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 对被监视服务器的认知。
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实例
而具体的更新流程是:
A[/获取一条信息/] --> B{{是我发的吗}} --Y--> C[/那没事了/]
B --N--> 提取数据 --> D{{是否之前见过这个Sentinel}} --Y--> E[/更新/]
D --N--> F[/添加/]
这样做的一个好处是,可以自动发现其他 Sentinel,并形成相互连接的网络,而无需手动配置。
Sentinel 之间只会创建命令链接,而不会创建订阅链接。
因为之所以和服务器需要创建订阅链接就是用来发现未知的新的 Sentinel 的。
服务器意外状态
检测主观下线状态
Sentinel 会以每秒一次的频率向所有与他建立了命令简介的实例(包括主、从、Sentinel服务器)发送 PING 命令,并通过返回信息判断实例的状态。
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 的意见,才好决定是否是真的下线了。
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 - 否则为配置纪元
- 如果leader_runid为
你应该看出来了,上面的两条命令有两种作用:
- 判断是否下线
- 选举领头 Sentinel
选举领头 Sentinel
当一个主服务器被判断为客观下线后,监视这个服务器的各个 Sentinel 会进行协商,选举一个领头的 Sentinel 并进行故障转移。
我的理解:
这里只有中间的 Sentinel 确定了客观下线这一事实,其他的 Sentinel 未必认同,但是即便如此,只要有一个 Sentinel 认定了客观下线的情况,其他 Sentinel 也会配合进行选举、故障转移。
选举的策略是:
- 所有人都有机会当选
- 发现主观下线的会向其他选手拉选票
- 所有人都是给第一个要求投票的人
- 超过一半选票的人当选
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 将对已下线的主服务进行故障转移操作:
- 选一个新的主服务器
- 让前任主服务器的所有从服务器跟着现任服务器
- 将前任设置为现任的从服务器
如何选新的服务器:
- 筛选排除:
- 下线的、断线状态的
- 最近5秒内都没有回复过leader的INFO命令的服务器
- 与前任主服务器断开超过
down-after-milliseconds * 10
的服务器- 优先选择:
- 优先级较高
- 复制偏移量较大
- ID最小
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的更多相关文章
- 【笔记】《Redis设计与实现》chapter16 Sentinel
16.1 启动并初始化Sentinel 初始化服务器 Sentinel本质上只是运行在特殊模式下的Redis服务器,启动第一步就是初始化一个普通的Redis服务器 使用Sentinel专用代码 使用r ...
- Redis | 第12章 Sentinel 哨兵模式《Redis设计与实现》
目录 前言 1. 启动并初始化 Sentinel 2. Sentinel 与服务器间的默认通信 2.1 获取主服务器信息 2.2 获取从服务器信息 2.3 向主服务器和从服务器发送信息 3. 接受来自 ...
- Redis容灾部署(哨兵Sentinel)
Redis容灾部署(哨兵Sentinel) 哨兵的作用 1. 监控:监控主从是否正常2. 通知:出现问题时,可以通知相关人员3. 故障迁移:自动主从切换4. 统一的配置管理:连接者询问sentinel ...
- 重读redis设计与实现
重读了一遍redis设计与实现,这次收获也不错,把之前还有些疑惑的点:redis跳跃表的原理.redis持久化的方法.redis复制.redis sentinel.redis集群等,都重新熟悉了一遍, ...
- 《Redis设计与实现》
<Redis设计与实现> 基本信息 作者: 黄健宏 丛书名: 数据库技术丛书 出版社:机械工业出版社 ISBN:9787111464747 上架时间:2014-6-3 出版日期:2014 ...
- 探索Redis设计与实现13:Redis集群机制及一个Redis架构演进实例
本文转自互联网 本系列文章将整理到我在GitHub上的<Java面试指南>仓库,更多精彩内容请到我的仓库里查看 https://github.com/h2pl/Java-Tutorial ...
- Redis设计原理简介
学完MySQL InnoDB之后,又开始学习和研究Redis. 首先介绍下书:<Redis设计与实现>第二版 黄健宏著,机械工业出版社,388页,基于redis3.0版本.版本有点低,这个 ...
- 《Redis设计与实现》知识点目录
Redis设计与实现 第一部分 数据结构与对象 第二章 简单动态字符串 p8 简单动态字符串SDS 2.1 SDS的定义 p9 每个sds.h/sdshdr结构表示一个SDS值 2.2 SDS与C字符 ...
- Redis | 第11章 服务器的复制《Redis设计与实现》
目录 前言 1. 旧版复制功能的实现 1.1 同步与命令传播 1.2 旧版复制功能的缺陷 2. 新版复制功能的实现 2.1 部分重同步的实现原理 3. PSYNC 命令的实现 4. 复制的详细步骤 4 ...
随机推荐
- 领域驱动(DDD)设计和开发实战
领域驱动设计(DDD)的中心内容是如何将业务领域概念映射到软件工件中.大部分关于此主题的著作和文章都以 Eric Evans 的书<领域驱动设计>为基础,主要从概念和设计的角度探讨领域建模 ...
- 单总线协议DS1820代码
单总线协议DS1820代码 一.DS18B20初始化 (1).数据线拉到低电平"0". (2).延时480微妙(该时间的时间范围可以从480到960微妙). (3).数据线拉到高电 ...
- 一个让我很不爽的外包项目——奔驰Smart2015新官网
七月份的下半个月,有幸做了奔驰 Smart 2015年新官网,包括手机端和PC端的宣传页,地址: PC端 手机端 这里,为了证明这个是一个事实,我还特意的留存了两张截图: 这里只想说明这么几个问题: ...
- 【Android开发】URL[] 转成 bitmap[]
public static Bitmap[] getBitmapFromURL(String[] path) throws MalformedURLException { Bitmap[] b = n ...
- XXE漏洞——介绍及利用
什么是xxe XML外部实体注入,简称XXE漏洞.XML文档结构包括XML声明,DTD文档类型定义,文档元素. XML示例 <?xml version="1.0"?>X ...
- Python入门-变量与命名
在上一篇中,我们定义了很多变量,变量格式是啥?变量名字可以随意么?有啥命名规范么?下面细讲 变量格式 变量名称 = 常量 把常量赋值给变量的过程,就是定义变量. 定义变量 Python中的变量不需要声 ...
- python---反转链表
class Node: def __init__(self, data): self.data = data self.next = None class Solution: "" ...
- ubuntu连接不到WiFi
ubuntu连接不到WiFi 在软件与更新中,进入附加驱动. 搜到对应的无线网卡驱动,安装后在重启电脑.
- Oracle安装 - shmmax和shmall设置
一.概述 在Linux上安装oracle,需要对内核参数进行调整,其中有shmmax和shmall这两个参数,那这两个参数是什么意思,又该如何设置呢? 二.官方文档 在oracle的官方文档( htt ...
- HTML/CSS+JS制作一个高考倒计时页面
2020-07-09更新 修复倒计时归零后出现负数的bug 自动切换至下一年日期 ##效果展示 前言 在B站上找视频学习的,勉强搞出来了,写下此篇文章作为笔记,也希望有更多感兴趣的人能够有所收获. ( ...