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節點的地址 , 并且初始化連接池 。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 概述 Redis-Sentinel是Redis官方推薦的高可用性(HA)解決方案,當用Redis做Master-s...
    神秘者007閱讀 769評論 0 3
  • 1.1 資料 ,最好的入門小冊子,可以先于一切文檔之前看,免費。 作者Antirez的博客,Antirez維護的R...
    JefferyLcm閱讀 17,120評論 1 51
  • sentinel:上一篇提到了主從切換,sentinel的作用是將這個過程自動化,實現高可用。它的主要功能有以下幾...
    米刀靈閱讀 7,713評論 0 6
  • 想到這幾年來好像自己一直是孑自一人,做什么事情看什么東西都是可以獨力的。不能說離不開別人的幫助,而只是在某個時間段...
    蒙古海軍上將閱讀 206評論 0 1
  • 當黑夜降臨 零碎的星光 是你 思念過的溫存 我總是在遙望 簡單的 干凈的 如你漆黑的瞳孔 你卻不知 我眼中 是如何...
    張婠閱讀 95評論 0 1