Redis - 如何通过 Sentinel (哨兵) 进行主从切换

[TOC]

Redis Sentinel

Redis Sentinel 是用于监控Redis集群中Master状态的工具 , Sentinel 只在 Server 端做主从切换 , 客户端需要额外的开发 , 所以我们的介绍 , 会分为Redis服务端 , 和 客户端两部分.

Sentinel的作用 :

1.Master的状态检测

2.如果Master出现异常 , 则会进行Master-Slave切换, 将其中一个Slave作为Master , 而将之前的Master实例切换为Slave .

3.Master-Slave切换后 , master_redis.conf , slave_redis.conf 和 sentinel.conf的内容都会发生改变 , 即 master_redis.conf中会多出一行slaveof的配置 , sentinel.conf的监控目标会随着变更

服务端工作方式:

Sentinel 对于不可用有两种定义

主观不可用(SDOWN) : SDOWN 是单个Sentinel实例检测到redis实例的状态 , 自己主观上判断为不可用

客观不可用(ODOWN) : ODOWN 需要Sentinel集群内一定数量(配置文件)的实例达成一致 , 才会认为Master节点不可用 , 然后执行failover策略 .

1.每个Sentinel实例以每秒一次的频率向自己所监控的Master,Slave以及其他Sentinel实例发送一个PING命令

2.如果一个实例距离最后一次有效回复PING命令的时间超过 down-after-millisenconds配置所指定的值 , 则这个实例会被Sentinel标记为主观不可用(SDOWN) .

3.如果一个Master实例被标记为主观不可用 , 则Sentinel 集群会进行投票 , 通过SENTINEL is_master_down_by_addr 命令 来获得其他Sentinel对Master的检测结果 , 如果超过指定数量的Sentinel认为该实例不可用 , 则Master会被标记为客观不可用(ODOWN) .

4.从SDOWN切换到ODOWN状态 , 不需要使用一致性算法 , 只使用gossip协议.

5.ODOWN状态只适用于Master节点 , Slave节点和Sentinel节点不会存在ODOWN状态 , 也不需要进行投票

客户端工作方式:

对于客户端的工作方式 , 我们以分析Java版本客户端 Jedis的源码为主。

构造器 :

public JedisSentinelPool(String masterName, Set<String> sentinels,    
    final GenericObjectPoolConfig poolConfig, 
    final int connectionTimeout, final int soTimeout,    
    final String password, 
    final int database, 
    final String clientName) {  
    this.poolConfig = poolConfig;  
    this.connectionTimeout = connectionTimeout;  
    this.soTimeout = soTimeout;  
    this.password = password;  
    this.database = database;  
    this.clientName = clientName;  
    //初始化 Sentinels
    HostAndPort master = initSentinels(sentinels, masterName);  
    // 初始化连接池
    initPool(master);
}

初始化方法 (initSentinels) :

private HostAndPort initSentinels(Set<String> sentinels, final String masterName) {  
    HostAndPort master = null;  
    boolean sentinelAvailable = false;  
    log.info("Trying to find master from available Sentinels...");  
    // 遍历Sentinel集群
    for (String sentinel : sentinels) {    
      // 解析 Sentinel 地址
      final HostAndPort hap = toHostAndPort(Arrays.asList(sentinel.split(":")));    
      log.fine("Connecting to Sentinel " + hap);    
      Jedis jedis = null;   
      try {
          //  创建Sentinel 连接
          jedis = new Jedis(hap.getHost(), hap.getPort());
          //  根据MasterName获取Master地址 , 返回一个集合 , 下标 0 是地址 , 下标 1 是端口
          List<String> masterAddr = jedis.sentinelGetMasterAddrByName(masterName);      
          // connected to sentinel...      
          sentinelAvailable = true;      
          if (masterAddr == null || masterAddr.size() != 2) {        
              log.warning("Can not get master addr, master name: " + masterName + ". Sentinel: " + hap            + ".");        
              continue;      
          }
          // 实例化地址
          master = toHostAndPort(masterAddr);      
          log.fine("Found Redis master at " + master); 
          // 如果在任何一个Sentinel中找到了master , 跳出循环
          break;    
      } catch (JedisException e) {      
          // resolves #1036, it should handle JedisException there's another chance      
          // of raising JedisDataException      
          log.warning("Cannot get master address from sentinel running @ " + hap + ". Reason: " + e + ". Trying next one.");    
      }  finally  {      
          if (jedis != null) {        
            jedis.close();      
          }    
      }  
  }  
  if (master == null) {    
      if (sentinelAvailable) {      
      // can connect to sentinel, but master name seems to not      
      // monitored      
     throw new JedisException("Can connect to sentinel, but " + masterName + " seems to be not monitored...");    
  } else {      
      throw new JedisConnectionException("All sentinels down, cannot determine where is " + masterName + " master is running...");    
  }  
}  
log.info("Redis master running at " + master + ", starting Sentinel listeners..."); 
// 遍历Sentinel集群地址 , 针对每一个实例 , 启动一个监听器
for (String sentinel : sentinels) {
    final HostAndPort hap = toHostAndPort(Arrays.asList(sentinel.split(":")));    
    MasterListener masterListener = new MasterListener(masterName, hap.getHost(), hap.getPort());    
    // whether MasterListener threads are alive or not, process can be stopped      
    masterListener.setDaemon(true);
    masterListeners.add(masterListener);      
    masterListener.start();  
  }  
  return master;
}

在initSentinels方法中 , 遍历Sentinel集群 , 并通过与Jedis绑定的客户端 , 发送一个 get-master-addr-by-name命令 ,来询问master节点的地址 。 直到找到master节点的地址 , 或者确认不存在指定名字的master节点。

在这段代码的最后 , 我们可以看到针对每一个Sentinel实例 都启动了一个监听器 , 我们来分析一下 MasterListener做了什么。

MasterListener

protected class MasterListener extends Thread {  
        protected String masterName;  
        protected String host;  
        protected int port;  
        protected long subscribeRetryWaitTimeMillis = 5000;
        // 因监听器可能被多个线程访问 , 所以jedis对象被修饰为**可见的** , 
        // 即一个线程修改了Jedis实例 , 其他的线程也可以得到最新的实例
        protected volatile Jedis j;  
        protected AtomicBoolean running = new AtomicBoolean(false);  
        protected MasterListener() {  }  
        public MasterListener(String masterName, String host, int port) {
            super(String.format("MasterListener-%s-[%s:%d]", masterName, host, port));
            this.masterName = masterName;    
            this.host = host;    
            this.port = port;  
        }  
        public MasterListener(String masterName, String host, int port, long subscribeRetryWaitTimeMillis) {
            this(masterName, host, port);    
            this.subscribeRetryWaitTimeMillis = subscribeRetryWaitTimeMillis;  
        }  
        public void run() {    
            running.set(true);    
            while (running.get()) {      
                j = new Jedis(host, port);      
                try {        
                // double check that it is not being shutdown       
                    if (!running.get()) {          
                        break;        
                    }        
                    // 订阅 channelName 为 "+switch-master" 的消息
                    j.subscribe(new JedisPubSub() {          
                        @Override          
                        public void onMessage(String channel, String message) {            
                            log.fine("Sentinel " + host + ":" + port + " published: " + message + ".");            
                            String[] switchMasterMsg = message.split(" ");            
                            if (switchMasterMsg.length > 3) {            
                                if (masterName.equals(switchMasterMsg[0])) {
                                    //  初始连接池
                                    initPool(toHostAndPort(Arrays.asList(switchMasterMsg[3], switchMasterMsg[4]))); 
                                } else {                
                                    log.fine("Ignoring message on +switch-master for master name "+ switchMasterMsg[0] + ", our master name is " + masterName);              
                                }            
                            } else {              
                                log.severe("Invalid message received on Sentinel " + host + ":" + port + " on channel +switch-master: " + message);            
                            }          
                        }
                    //channelName   
                    }, "+switch-master");      
                } catch (JedisConnectionException e) {        
                    if (running.get()) {          
                        log.log(Level.SEVERE, "Lost connection to Sentinel at " + host + ":" + port + ". Sleeping 5000ms and retrying.",e);
                            try {            
                                    Thread.sleep(subscribeRetryWaitTimeMillis);          
                            } catch (InterruptedException e1) {            
                                    log.log(Level.SEVERE, "Sleep interrupted: ", e1);          
                            }        
                    } else {          
                        log.fine("Unsubscribing from Sentinel at " + host + ":" + port);        
                    }      
                } finally {        
                        j.close();      
                }    
        } 
}
protected volatile Jedis j;  
protected AtomicBoolean running = new AtomicBoolean(false);  

从成员变量中我们可以看到 为了保障线程安全 , 代码中使用了volatile(可见性关键字) , 和布尔的原子变量 , 我会在另外的文章里 , 描述他们在使用上的区别

在 MasterListener 这个监听器里 , 我们可以看到这里订阅了一个名为 "+switch-master" 的事件 , 当得到这个事件的时候,调用 initPool 方法 , 用来更新Master节点的地址 , 并且初始化连接池 。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,588评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,456评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,146评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,387评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,481评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,510评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,522评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,296评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,745评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,039评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,202评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,901评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,538评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,165评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,415评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,081评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,085评论 2 352

推荐阅读更多精彩内容

  • 概述 Redis-Sentinel是Redis官方推荐的高可用性(HA)解决方案,当用Redis做Master-s...
    神秘者007阅读 751评论 0 3
  • 1.1 资料 ,最好的入门小册子,可以先于一切文档之前看,免费。 作者Antirez的博客,Antirez维护的R...
    JefferyLcm阅读 17,041评论 1 51
  • sentinel:上一篇提到了主从切换,sentinel的作用是将这个过程自动化,实现高可用。它的主要功能有以下几...
    米刀灵阅读 7,678评论 0 6
  • 想到这几年来好像自己一直是孑自一人,做什么事情看什么东西都是可以独力的。不能说离不开别人的帮助,而只是在某个时间段...
    蒙古海军上将阅读 196评论 0 1
  • 当黑夜降临 零碎的星光 是你 思念过的温存 我总是在遥望 简单的 干净的 如你漆黑的瞳孔 你却不知 我眼中 是如何...
    张婠阅读 79评论 0 1