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 ...
随机推荐
- 前端规范(ES6BEMOOCSSSMACSS)
前端规范 在实际开发中,由于团队成员编码习惯不一,技术层次不同,开发前定制并遵循一种代码规范能提高代码质量,增加开发效率. Javascript Javascript规范直接参考airbnb: ES6 ...
- ES6-11学习笔记--扩展运算符与rest参数
1.符号都是使用:... 2.扩展运算符:把数组或者类数组展开成用逗号隔开的值 3.rest参数:把逗号隔开的值组合成一个数组 扩展运算符: function foo(a, b, c) { con ...
- mysql获取表的列名
DESC test4 SHOW COLUMNS FROM test4 SELECT COLUMN_NAME FROM information_schema.columns WHERE table_n ...
- 获取MCCMNC号
public static boolean isColombiaSpanishSimCard(){ TelephonyManager telManager = (TelephonyMan ...
- 大数据学习之路之ambari配置(二)
按照网上的教程配置,发现配置到hadoop虚拟机内存就开始不够了,心累
- scrapy爬虫简单案例(简单易懂 适合新手)
爬取所有的电影名字,类型,时间等信息 1.准备工作 爬取的网页 https://www.ddoutv.com/f/27-1.html 创建项目 win + R 打开cmd输入 scrapy start ...
- Windows 10搭建FTP服务器
1 开启FTP服务 控制面板 -> 程序和功能 -> 启用或关闭Windows功能 找到下面选项的勾选 2 添加FTP站点 在开始菜单里面输入 IIS 搜索并打开 IIS管理器 展开左侧菜 ...
- SpringMVC注解环境搭建
基本步骤 新建Maven项目(Web) 导入依赖 配置web.xml 配置springmvc配置文件 编写Controller 创建view页面 部署并启动Tomcat 开始搭建 新建Maven项目( ...
- Struts2-获取值栈对象与结构
1.获取值栈对象有多种方式 (1)使用ActionContext类里面的方法获取值栈对象 @Override public String execute(){ //1.获取ActionContext类 ...
- 邮件任务-springboot
邮件任务-springboot springboot可以很容易实现邮件的发送 具体实现步骤: 导入jar包 <dependency> <groupId>org.springfr ...