上一篇我们介绍了 redis 主从节点之间的数据同步复制技术,通过一次全量复制和不间断的命令传播,可以达到主从节点数据同步备份的效果,一旦主节点宕机,我们可以选择一个工作正常的 slave 成为新的主节点,并让其他 slave 去同步它。

这是处理 redis 故障转移的一个方式,但却不具备生产实用性,因为毕竟是手动处理故障,而 redis 发生故障时间节点不可预知,我们需要一个自动监控组件帮我们自动处理故障转移。

Redis 哨兵模式(Sentinel)就是一个自动地监控处理 redis 间故障节点转移工作的一个「东西」,准确来说,Sentinel 其实是一个 redis 服务端程序,只不过运行在特殊的模式下,不提供数据存储服务,只进行普通 redis 节点监控管理。

一、什么是哨兵(Sentinel)

Sentinel 其实也是一个 redis 的服务端程序,它也会定时执行 serverCron 函数,只是里面其他的程序用不到,用到的是对普通 redis 节点的监控以及故障转移模块。

Sentinel 初始化的时候会清空原来的命令表,写入自己独有的命令进去,所以普通 redis 节点支持的数据读写命令,对 Sentinel 来说都是找不到命令,因为它根本就没有初始化这些命令的执行器。

Sentinel 会定时的对自己监控的 master 执行 info 命令,获取最新的主从关系,还会定时的给所有的 redis 节点发送 ping 心跳检测命令,如果检测到某个 master 无法响应了,就会在给其他 Sentinel 发送消息,主观认为该 master 宕机,如果 Sentinel 集群认同该 master 下线的人数达到一个值,那么大家统一意见,下线该 master。

下线之前需要做的是找 Sentinel 集群中的某一个来执行下线操作,这个步骤叫领导者选举,选出来以后会从该 master 所有的 slave 节点中挑一个合适的作为新的 master,并让其他 slave 重新同步新的 master。

其实以上我们就简单的介绍了 Sentinel 是什么,本质上做了哪些事情,等下我们会结合源码细说其中的细节实现。这里我们再看下,如何配置并启动一个 Sentinel 监控。(生产环境建议配置大于三个)

第一步,启动一个普通的 redis server 节点:

这一步没什么好说的,我们启动在一个默认的 6379 端口上。

第二步,启动三个不同的 slave 节点:

第三步,编写 sentinel 配置文件:

我们解释一下这几条配置的含义,我们说过 Sentinel 其实是运行在特殊模式下的 redis server,所以它需要运行端口。紧接着我们通过命令 sentinel monitor mymaster 配置当前 sentinel 需要监控的主节点 redis 以及触发客观下线参数,sentinel down-after-milliseconds 配置了一个参数,master 最长响应时间,超过这个时间就主观判断它下线。

sentinel parallel-syncs 配置用于限制主从切换之后,最多的并行同步数据的从节点数量,因为我们知道,主从进行全量同步阶段,从节点加载数据时是不提供服务的,如果这个参数越大,那么主从切换完成的时间就越短,当然也会导致大量从节点不可提供读服务,反之。

sentinel failover-timeout 配置了执行故障转移的最大等待时间。

第四步,启动 Sentinel:

使用命令,redis-sentinel [config],启动三个 sentinel。

这样的话,其实我们就完成了一个简单的 sentinel 集群配置,下面我们手动的让 master 宕机,看看整个 sentinel 有没有为我们做故障转移。

从结果上看来,sentinel 自动为我们把原先的从节点 7003 设置为新的 master,具体过程我们不细说,等下结合源码详细介绍,这里我们应该大致对 sentinel 的实际应用有了大概的认识。

二、Sentinel 如何工作的

当我们使用命令 redis-sentinel 启动 sentinel 的时候,

int main(int argc, char **argv) {
。。。。。
server.sentinel_mode = checkForSentinelMode(argc,argv);
。。。。。
if (server.sentinel_mode) {
initSentinelConfig();
initSentinel();
}
。。。。。
}

checkForSentinelMode 函数中会根据你的命令以及参数,检查判断是否是以 sentinel 模式启动,如果是则返回 1,反之。如果是以 sentinel 启动,则会进行一个 sentinel 的初始化操作。

void initSentinelConfig(void) {
server.port = REDIS_SENTINEL_PORT; //26379
}

initSentinelConfig 实际上就是初始化当前 sentinel 运行端口,默认是 26379。

void initSentinel(void) {
unsigned int j;
//清空普通redis-server下可用的命令表
dictEmpty(server.commands,NULL);
//加载sentinel需要的命令
for (j = 0; j < sizeof(sentinelcmds)/sizeof(sentinelcmds[0]); j++) {
int retval;
struct redisCommand *cmd = sentinelcmds+j; retval = dictAdd(server.commands, sdsnew(cmd->name), cmd);
serverAssert(retval == DICT_OK);
} sentinel.current_epoch = 0;
//根据配置文件,初始化自己需要监控的master(一个sentinel是可能监控多个 master的)
sentinel.masters = dictCreate(&instancesDictType,NULL);
sentinel.tilt = 0;
sentinel.tilt_start_time = 0;
sentinel.previous_time = mstime();
sentinel.running_scripts = 0;
sentinel.scripts_queue = listCreate();
sentinel.announce_ip = NULL;
sentinel.announce_port = 0;
sentinel.simfailure_flags = SENTINEL_SIMFAILURE_NONE;
sentinel.deny_scripts_reconfig = SENTINEL_DEFAULT_DENY_SCRIPTS_RECONFIG;
memset(sentinel.myid,0,sizeof(sentinel.myid));
}

initSentinel 主要的作用还是清空普通模式的 redis 命令表,加载独属于 sentinel 使用的命令,并初始化自己监控的 master 集合。

至此,sentinel 的初始化就算完成了,剩下的自动监控则在定时函数 serverCron 中,我们一起来看看。

    //间隔 100 毫秒执行一次 sentinelTimer
run_with_period(100) {
if (server.sentinel_mode) sentinelTimer();
}

也就是说,sentinel 启动之后,会间隔 100 毫秒在 serverCron 调用一次 sentinelTimer 函数处理一些重要事件(其实,sentinelTimer 中会修改执行间隔)。

void sentinelTimer(void) {
sentinelCheckTiltCondition();
sentinelHandleDictOfRedisInstances(sentinel.masters);
sentinelRunPendingScripts();
sentinelCollectTerminatedScripts();
sentinelKillTimedoutScripts();
server.hz = CONFIG_DEFAULT_HZ + rand() % CONFIG_DEFAULT_HZ;
}

sentinelTimer 函数体非常简短,但不要高兴太早。sentinelCheckTiltCondition 函数我们不去多说,redis 高度依赖系统时间,如果多次检测到系统时钟纪元不准确,它会判定当前系统不稳定,进入 TITL,类似一个休眠的状态,不会为我们做故障转移,仅仅收集数据,等待系统恢复稳定。

void sentinelHandleDictOfRedisInstances(dict *instances) {
dictIterator *di;
dictEntry *de;
sentinelRedisInstance *switch_to_promoted = NULL;
di = dictGetIterator(instances);
//递归遍历监控的所有 master,执行监控操作
while((de = dictNext(di)) != NULL) {
sentinelRedisInstance *ri = dictGetVal(de);
//这是监控的核心逻辑,下文细说
sentinelHandleRedisInstance(ri);
if (ri->flags & SRI_MASTER) {
//不论是 slave 还是其他 sentinel,都视作一个redisInstance
sentinelHandleDictOfRedisInstances(ri->slaves);
sentinelHandleDictOfRedisInstances(ri->sentinels);
if (ri->failover_state == SENTINEL_FAILOVER_STATE_UPDATE_CONFIG) {
switch_to_promoted = ri;
}
}
}
if (switch_to_promoted)
sentinelFailoverSwitchToPromotedSlave(switch_to_promoted);
dictReleaseIterator(di);
}

sentinelHandleRedisInstance 主要两个部分组成,监控和故障转移。

void sentinelHandleRedisInstance(sentinelRedisInstance *ri) {
sentinelReconnectInstance(ri);
sentinelSendPeriodicCommands(ri); if (sentinel.tilt) {
if (mstime()-sentinel.tilt_start_time < SENTINEL_TILT_PERIOD) return;
sentinel.tilt = 0;
sentinelEvent(LL_WARNING,"-tilt",NULL,"#tilt mode exited");
} sentinelCheckSubjectivelyDown(ri); if (ri->flags & (SRI_MASTER|SRI_SLAVE)) {
/* Nothing so far. */
} if (ri->flags & SRI_MASTER) {
sentinelCheckObjectivelyDown(ri);
if (sentinelStartFailoverIfNeeded(ri))
sentinelAskMasterStateToOtherSentinels(ri,SENTINEL_ASK_FORCED);
sentinelFailoverStateMachine(ri);
sentinelAskMasterStateToOtherSentinels(ri,SENTINEL_NO_FLAGS);
}
}

sentinelReconnectInstance 函数做两件事,因为当前是一个 sentinel 实例,所以第一件事就是与当前遍历的 instance 建立连接,不论它是 master、slave 或是 sentinel,并在成功建立连接后发送 ping 命令。第二,如果当前遍历的是一个 master 或 slave,则会订阅它的 sentinel_hello 频道,当这个频道上有消息更新,则会广播所有订阅的该频道的客户端。(订阅这个频道的主要作用还是用于发现其他 sentinel 以及与其他 sentinel 交流自己对监控的节点的看法)

sentinelSendPeriodicCommands 函数默认每间隔十秒给 master 和 slave 发送 info 命令,了解他们的主从关系,如果此 instance 被自己主观下线了,那么会加快发送 info 命令的频率,以保证自己最快知道主从关系变化,还会每间隔一秒 ping 所有类型的实例。

以上其实是 sentinelHandleRedisInstance 中监控节点的部分,下面我们继续看其故障转移怎么做的。

void sentinelHandleRedisInstance(sentinelRedisInstance *ri) {
sentinelReconnectInstance(ri);
sentinelSendPeriodicCommands(ri); //判断是否需要进入 tilt 模式
if (sentinel.tilt) {
if (mstime()-sentinel.tilt_start_time < SENTINEL_TILT_PERIOD) return;
sentinel.tilt = 0;
sentinelEvent(LL_WARNING,"-tilt",NULL,"#tilt mode exited");
}
//判断是否需要主观下线该节点
sentinelCheckSubjectivelyDown(ri); if (ri->flags & (SRI_MASTER|SRI_SLAVE)) {
/* Nothing so far. */
} if (ri->flags & SRI_MASTER) {
//判断是否需要客户下线该节点
sentinelCheckObjectivelyDown(ri);
//如果确定该节点客观下线,进行领导者选举
if (sentinelStartFailoverIfNeeded(ri))
sentinelAskMasterStateToOtherSentinels(ri,SENTINEL_ASK_FORCED);
//故障转移
sentinelFailoverStateMachine(ri);
sentinelAskMasterStateToOtherSentinels(ri,SENTINEL_NO_FLAGS);
}
}

sentinelCheckSubjectivelyDown 检测当前节点是否需要主观下线,判断条件是此节点对于自己的配置,如果当前这个实例超过配置的时间段没有回复自己的 ping,那么判断它下线,设置主观下线标志位。

sentinelCheckObjectivelyDown 检测当前是否达到客观下线的条件,检测逻辑是这样的,遍历所有的兄弟 sentinel 结构,看看他们有没有把当前节点主观下线,统计数量,如果达到 quorum,则判定该 master 客观下线,设置标志位并通过频道通知到其他 兄弟 sentinel。

sentinelStartFailoverIfNeeded 判断当前是否已有 sentinel 在进行故障转移(通过 master 的一个标志位,如果有 sentinel 正在进行故障转移,这个标志位会被设置),如果有,则自己不参与,什么都不做。

sentinelAskMasterStateToOtherSentinels 会去给其他 sentinel 发送消息,要求它同意自己作为领导者对 master 进行故障转移。具体怎么做的呢,首先会拿到自己这边关于所有兄弟 sentinel 的信息进行一个遍历,并给他们发送命令 is-master-down-by-addr 要求他们同意自己成为领导者,并设置回调函数 sentinelReceiveIsMasterDownReply 处理回复。

如果某个 sentinel 收到别人发来的领导者投票,且自己没有给其他人投过票的话就会同意,反之不予理睬。

当某个 sentinel 收到足够的票数,则它认为自己就是 leader,标志 master 为故障转移中,并进行真正的故障转移操作。

void sentinelFailoverStateMachine(sentinelRedisInstance *ri) {
serverAssert(ri->flags & SRI_MASTER); if (!(ri->flags & SRI_FAILOVER_IN_PROGRESS)) return; switch(ri->failover_state) {
//故障转移开始
case SENTINEL_FAILOVER_STATE_WAIT_START:
sentinelFailoverWaitStart(ri);
break;
//选择一个要晋升的从节点
case SENTINEL_FAILOVER_STATE_SELECT_SLAVE:
sentinelFailoverSelectSlave(ri);
break;
//发送slaveof no one命令,使从节点变为主节点
case SENTINEL_FAILOVER_STATE_SEND_SLAVEOF_NOONE:
sentinelFailoverSendSlaveOfNoOne(ri);
break;
//等待被选择的从节点晋升为主节点,如果超时则重新选择晋升的从节点
case SENTINEL_FAILOVER_STATE_WAIT_PROMOTION:
sentinelFailoverWaitPromotion(ri);
break;
//给所有的从节点发送slaveof命令,同步新的主节点
case SENTINEL_FAILOVER_STATE_RECONF_SLAVES:
sentinelFailoverReconfNextSlave(ri);
break;
}
}

sentinelFailoverStateMachine 故障转移包括五个步骤,分五个 sentinelTimer 执行周期处理。当新 master 选举完成,会给其他兄弟 sentinel 广播,告知他们新的 master 已经出现,他们收到后,会撤销对原 master 的主观下线,并重新开始监控新的 master。

至此,我们对 Sentinel 的介绍与源码分析就结束了,它本质上就是一个运行在特殊模式下的 redis-server,通过不断 ping 主从节点,在感知他们可能出现故障之后,集体进行一个投票认定并选举出一个人去执行 master 的客观下线。

下一篇,我们看 redis 中更牛逼的 cluster。


关注公众不迷路,一个爱分享的程序员。

公众号回复「1024」加作者微信一起探讨学习!

每篇文章用到的所有案例代码素材都会上传我个人 github

https://github.com/SingleYam/overview_java

欢迎来踩!

Redis 哨兵模式(Sentinel)的更多相关文章

  1. Redis高可用之哨兵模式Sentinel配置与启动(五)

    0.Redis目录结构 1)Redis介绍及部署在CentOS7上(一) 2)Redis指令与数据结构(二) 3)Redis客户端连接以及持久化数据(三) 4)Redis高可用之主从复制实践(四) 5 ...

  2. 【转】Redis哨兵(Sentinel)模式

    主从切换技术的方法是:当主服务器宕机后,需要手动把一台从服务器切换为主服务器,这就需要人工干预,费事费力,还会造成一段时间内服务不可用.这不是一种推荐的方式,更多时候,我们优先考虑哨兵模式. 一.哨兵 ...

  3. Redis哨兵(Sentinel)模式

    Redis哨兵(Sentinel)模式   主从切换技术的方法是:当主服务器宕机后,需要手动把一台从服务器切换为主服务器,这就需要人工干预,费事费力,还会造成一段时间内服务不可用.这不是一种推荐的方式 ...

  4. 【集群】Redis哨兵(Sentinel)模式

    主从切换技术的方法是:当主服务器宕机后,需要手动把一台从服务器切换为主服务器,这就需要人工干预,费事费力,还会造成一段时间内服务不可用.这不是一种推荐的方式,更多时候,我们优先考虑哨兵模式. 一.哨兵 ...

  5. Redis 单机模式,主从模式,哨兵模式(sentinel),集群模式(cluster),第三方模式优缺点分析

    Redis 的几种常见使用方式包括: 单机模式 主从模式 哨兵模式(sentinel) 集群模式(cluster) 第三方模式 单机模式 Redis 单副本,采用单个 Redis 节点部署架构,没有备 ...

  6. Springboot2.x集成Redis哨兵模式

    Springboot2.x集成Redis哨兵模式 说明 Redis哨兵模式是Redis高可用方案的一种实现方式,通过哨兵来自动实现故障转移,从而保证高可用. 准备条件 pom.xml中引入相关jar ...

  7. Spring Boot 入门(十):集成Redis哨兵模式,实现Mybatis二级缓存

    本片文章续<Spring Boot 入门(九):集成Quartz定时任务>.本文主要基于redis实现了mybatis二级缓存.较redis缓存,mybaits自带缓存存在缺点(自行谷歌) ...

  8. [Redis] Redis哨兵模式部署 - zz胖的博客

    1. 部署Redis集群 redis的安装及配置参考[redis部署] 本文以创建一主二从的集群为例. 1.1 部署与配置 先创建sentinel目录,在该目录下创建8000,8001,8002三个以 ...

  9. 搭建redis哨兵模式

    搭建redis哨兵模式,一主两从三哨兵   1.从官网下载redis安装包:此处是redis-5.0.7.tar.gz 2.上传到目录 /utxt/soft 3.解压 4.cd /utxt/soft/ ...

随机推荐

  1. 微信小程序之登录连接django,以及用户的信息授权认证

    小结: 1 如何自定义组件 - 组件和页面一样,也是由四个文件组成,所以我们自定义组件的时候,模拟pages文件夹,把所有的所有的组件都放在一个文件夹中,每个组件又由一个文件夹包裹,方便管理,在对应目 ...

  2. C++ 标准模板库(STL)-stack

    主要介绍一下C++11版本中标准模板库中栈的用法,希望可以帮到需要用的人. #include <iostream> #include <stack> #include < ...

  3. Ubuntu16.04 desktop 设置共享文件夹 -- 图形界面配置

    1. 安装 安装samba 直接采用 Ubuntu16.04 desktop 里面的安装向导来完成: 选中需要共享的文件夹 -> 右键 “local Network Share” -> 安 ...

  4. throttle工具函数

    // fn是我们需要包装的事件回调, delay是时间间隔的阈值 export function throttle(fn, delay) { // last为上一次触发回调的时间, timer是定时器 ...

  5. 从火箭发场景来学习Java多线程并发闭锁对象

    从火箭发场景来学习Java多线程并发闭锁对象 倒计时器场景 在我们开发过程中,有时候会使用到倒计时计数器.最简单的是:int size = 5; 执行后,size—这种方式来实现.但是在多线程并发的情 ...

  6. 【JAVA进阶架构师指南】之一:如何进行架构设计

    前言   本博客是长篇系列博客,旨在帮助想提升自己,突破技术瓶颈,但又苦于不知道如何进行系统学习从而提升自己的童鞋.笔者假设读者具有3-5年开发经验,java基础扎实,想突破自己的技术瓶颈,成为一位优 ...

  7. express第三方中间件研究之bodyParser中间件

    转载至:http://www.cnblogs.com/lianer/p/5178693.html 接触nodejs已有一段时间了,但最近才开始落实项目,于是使用express应用生成器生成了一个应用. ...

  8. 测试必知必会系列- Linux常用命令 - ps(重点)

    21篇测试必备的Linux常用命令,每天敲一篇,每次敲三遍,每月一循环,全都可记住!! https://www.cnblogs.com/poloyy/category/1672457.html 查看所 ...

  9. 面试官再问我如何保证 RocketMQ 不丢失消息,这回我笑了!

    最近看了 @JavaGuide 发布的一篇『面试官问我如何保证Kafka不丢失消息?我哭了!』,这篇文章承接这个主题,来聊聊如何保证 RocketMQ 不丢失消息. 0x00. 消息的发送流程 一条消 ...

  10. 记录一次云主机部署openstack的血泪史

    看见这个部署成功的留下了激动的泪水 经过长时间的BUG苦肝终于成功部署成功  部署的环境2vCPU 8GB 阿里云主机,部署成功以后内存占用确实蛮高的 记录这一次踩坑,给后来者避免踩坑时间,个人踩坑踩 ...