本文講述Redis高可用方案中的哨兵模式——Sentinel,RedisClient中的Jedis如何使用以及使用原理。html
Redis主從複製是Sentinel模式的基石,在學習Sentinel模式前,須要理解主從複製的過程。git
Redis主從複製的含義和Mysql的主從複製同樣,即利用Slave從服務器同步Master服務器數據的副本。主從複製的最爲關鍵的點在於主從數據的一致性,在Redis中主要經過如下三點:github
利用以上三點,Redis的主從複製保證數據的最終一致性。redis
假設有兩臺服務器,一臺是Master,另外一臺是Slave。如今需求是保證Master和Slave的數據一致性。
若是要保證精確的一致性,最好的方式是實時的進行全量同步,基於全量確定是一致的。可是這樣形成的性能損耗必然不可估計。
增量同步即同步變化的數據,不一樣步未發生變化的數據,雖然實現程度比全量複雜,可是能讓性能提高。
Redis中實現主從複製是全量結合增量實現。sql
增量同步,必須獲取主從服務器之間的數據差別,對於數據同一份數據的差別獲取,最多見的方式即版本控制。如常見的版本控制系統:svn、git等。在Redis主從關係中,數據的最初來源於Master,因此數據版本控制由Master控制。數據庫
Notes:
同一份數據的演變記錄,最好的方式即版本控制安全
在Redis中,每一個Master都有一個RelicationID,標識一個給定的歷史數據集,是一串僞隨機串。同時還有一個OffsetID,當Master將變化的數據發送給Slave時,發送多少個字節,相應的offsetID就增加多少,依據此作數據集的版本控制。即便沒有Slave,Master也會增加OffsetID,一個RelicationID和OffsetID的組合都會標識一個數據集版本。服務器
當Slave鏈接到Master時,Slave會向Master主動發送本身的RelicationID和OffsetID,Master依此判斷Slave當前的數據版本,將變化的數據發送給Slave。當Slave發送的是一個未知的RelicationID和OffsetID,Master則會進行一次全同步。網絡
Master會開啓另外一個複製進程。複製進程會建立一個持久化的RDB快照文件,並將新的請求命令緩衝在緩衝區中,達到Copy-On-Write的效果。在RDB文件建立完成後,會將RDB文件發送給Slave,Slave接收到後,將文件保存至磁盤,而後再載入內存。最後Master再將緩衝區的命令流發送給Slave,完成最終的數據同步。多線程
對於主從複製還有不少特性,如:主從同步中的過時鍵處理,主從之間的認證,容許N個附加的副本,Slave只讀模式等,能夠參考:複製
Redis主從複製的配置比較簡單,分爲兩種方式:靜態文件配置和動態命令行配置。redis.conf中提供:
slaveof 192.168.1.1 6379
配置項用於配置Slave節點的Master節點,表示是誰的Slave。
同時還能夠在redis-cli命令行中使用slaveof 192.168.1.1 6379格式的命令配置一個Slave的Master節點。可使用slaveof no one取消其從節點的身份。
Redis已經具有了主從複製的功能,爲何仍然須要Sentinel模式?
Redis的主從模式從必定從程度上的確解決了可用性問題,這毋庸置疑。可是隻僅僅主從複製來完成可用性,就比較簡陋,靈活性不夠,操做複雜。更不用說高可用!
基於以上的需求,Redis Sentinel是Redis提供的高可用的一種模型,在Sentinel模式下,無需人員的干預,Sentinel可以幫助完成如下工做:
Sentinel自己就是一個分佈式系統。Sentinel基於一個配置運行多個進程協同工做,這些進程能夠在一個服務器實例上,也能夠分佈在多個不一樣實例上。多個Sentinel工做有以下特色:
在Sentinel體系中,Sentinel、Redis實例和鏈接到Sentinel和Redis實例的應用這三者也共同組成了一個完整的分佈式系統。
Redis中提供了搭建Sentinel的相關命令:redis-sentinel。其中Redis包中也包含了sentinel.conf的示例配置。
啓動Sentinel實例,能夠直接運行:
redis-sentinel sentinel.conf
可是在配置sentinel模式前,現須要作些準備工做:
關於Sentinel系統的其餘關注點,請參考:Fundamental things to know about Sentinel before deploying
下面看下Sentinel的配置文件:
sentinel monitor mymaster 127.0.0.1 6379 2 sentinel down-after-milliseconds mymaster 60000 sentinel parallel-syncs mymaster 1 sentinel failover-timeout mymaster 180000
sentinel monitor
用於配置sentinel的名稱,master節點,仲裁數。
舉個例子,假設有5個sentinel進程:
down-after-milliseconds
用於配置sentinel認爲Redis實例不可用的至少時間時多少,以毫秒爲單位
sentinel parallel-syncs
用於配置故障轉移後,同時進行從新配置slave節點的個數。從新配置slave時,則slave將沒法處理客戶端的查詢請求。若是同時配置全部的slave,則將會出現,整個Redis不可用。可是若是該值較小,又會致使從新配置時間過長。須要trade off。
sentinel failover-timeout
用於配置故障轉移的超時時間
接下來實際演示配置Redis Sentinel過程:
準備環境,因爲筆者沒有如此多的服務器,雖然可使用Docker,可是爲了簡單,直接使用一臺機器,監聽不一樣端口實現。
#sentinel實例 127.0.0.1:26379 127.0.0.1:26380 127.0.0.1:26381 127.0.0.1:26382 127.0.0.1:26383 #redis實例 127.0.0.1:6379 127.0.0.1:6380 127.0.0.1:6381
編寫sentinel的配置:
port 26379 dir "/Users/xxx/redis/sentinel/data" logfile "/Users/xxx/redis/sentinel/log/sentinel_26379.log" sentinel monitor mymaster 127.0.0.1 6379 2 sentinel down-after-milliseconds mymaster 60000 sentinel parallel-syncs mymaster 1 sentinel failover-timeout mymaster 180000
其餘的sentinel實例配置依次類推,分別使用26380,26381,26382,26383端口,日誌文件名稱也作相應更換。主機節點使用127.0.0.1:6379。
配置6379端口的Redis實例以下:
port 6379 daemonize yes logfile "/Users/xxx/redis/sentinel/log/6379.log" dbfilename "dump-6379.rdb" dir "/Users/xxx/redis/sentinel/data"
6380和6381端口另外再加上一行配置:slaveof 127.0.0.1 6379,表示slave節點。
再分別啓動Redis實例和Sentinel實例:
redis-server redis6379.conf .... redis-sentinel sentinel26379.conf &
啓動結束後能夠查找Redis的相關進程有:
501 2165 1 0 7:47下午 ?? 0:00.55 redis-server *:6379 501 2167 1 0 7:47下午 ?? 0:00.58 redis-server *:6380 501 2171 1 0 7:47下午 ?? 0:00.59 redis-server *:6381 501 2129 1890 0 7:39下午 ttys000 0:02.03 redis-sentinel *:26379 [sentinel] 501 2130 1890 0 7:39下午 ttys000 0:01.99 redis-sentinel *:26380 [sentinel] 501 2131 1890 0 7:39下午 ttys000 0:02.02 redis-sentinel *:26381 [sentinel] 501 2132 1890 0 7:39下午 ttys000 0:01.97 redis-sentinel *:26382 [sentinel] 501 2133 1890 0 7:39下午 ttys000 0:01.93 redis-sentinel *:26383 [sentinel]
表示整個Redis Sentinel模式搭建完畢!
可使用redis-cli命令行鏈接到Sentinel查詢相關信息
redis-cli -p 26379 #查詢sentinel中的master節點信息和狀態,考慮篇幅,這裏只展現部分 127.0.0.1:26379> sentinel master mymaster 1) "name" 2) "mymaster" 3) "ip" 4) "127.0.0.1" 5) "port" 6) "6379" 7) "runid" 8) "67065dc606ffeb58d1b11e336bc210598743b676" 9) "flags" 10) "master" 11) "link-pending-commands" #查詢sentinel中的slaves節點信息和狀態,考慮篇幅,這裏只展現部分 127.0.0.1:26379> sentinel slaves mymaster 1) 1) "name" 2) "127.0.0.1:6381" 3) "ip" 4) "127.0.0.1" 5) "port" 6) "6381" 7) "runid" 8) "728f17ca3786e46cd28d76b94a1c62c7d7475d08" 9) "flags" 10) "slave"
這裏能夠將master節點的進程kill,sentinel會自動進行故障轉移。
kill -9 2165 #再查詢master時,sentinel已經進行了故障轉移 127.0.0.1:26379> sentinel master mymaster 1) "name" 2) "mymaster" 3) "ip" 4) "127.0.0.1" 5) "port" 6) "6381" 7) "runid" 8) "728f17ca3786e46cd28d76b94a1c62c7d7475d08" 9) "flags" 10) "master"
sentinel get-master-addr-by-name mymaster命令用於獲取master節點
Notes:
以上的sentinel配置中並無配置slave相關的信息,只配置master節點。sentinel能夠根據master節點獲取全部的slave節點。
最後再來看下Sentinel中的Pub/Sub,Sentinel堆外提供了事件通知機制。Client能夠訂閱Sentinel的指定通道獲取特定事件類型的通知。通道名稱和事件名稱相同,例如redis-cli - 23679登陸sentinel,訂閱subcribe +sdown通道,而後kill監聽6379的Redis實例,則會收到以下通知:
1) "pmessage" 2) "*" 3) "+sdown" 4) "slave 127.0.0.1:6380 127.0.0.1 6380 @ mymaster 127.0.0.1 6381"
Redis Sentinel模式下的Client都是利用其特色,實現應用的故障自動轉移。
關於Sentinel還有不少其餘的功能特性,如:增長移除一個sentinel,增長移除slave等,更多細節,請參靠Redis Sentinel Documentation
前文中提到Redis Sentinel模式須要應用客戶端的支持才能實現故障自動轉移,切換至新提高的master節點上。同時也講解Redis Sentinel系統提供了Pub/Sub的API供應用客戶端訂閱Sentinel的特定通道獲取相應的事件類型的通知。
在Jedis中就是利用這些特色完成對Redis Sentinel模式的支持。下面按部就班的探索Jedis中的Sentinel源碼實現。
Jedis中實現Sentinel只有一個核心類JedisSentinelPool,該類實現了:
JedisSentinelPool直接提供了構造函數API,能夠直接利用sentinel的信息集合構造JedisSentinelPool,其中的getResource直接返回與當前master相關的Jedis對象。
@Test public void sentinel() { Set<String> sentinels = new HashSet<>(); sentinels.add(new HostAndPort("localhost", 26379).toString()); sentinels.add(new HostAndPort("localhost", 26380).toString()); sentinels.add(new HostAndPort("localhost", 26381).toString()); sentinels.add(new HostAndPort("localhost", 26382).toString()); sentinels.add(new HostAndPort("localhost", 26383).toString()); String sentinelName = "mymaster"; JedisSentinelPool pool = new JedisSentinelPool(sentinelName, sentinels); Jedis redisInstant = pool.getResource(); System.out.println("current host:" + redisInstant.getClient().getHost() + ", current port:" + redisInstant.getClient().getPort()); redisInstant.set("testK", "testV"); // 故障轉移 Jedis sentinelInstant = new Jedis("localhost", 26379); sentinelInstant.sentinelFailover(sentinelName); System.out.println("current host:" + redisInstant.getClient().getHost() + ", current port:" + redisInstant.getClient().getPort()); Assert.assertEquals(redisInstant.get("testK"), "testV"); }
public class JedisSentinelPool extends JedisPoolAbstract { // 鏈接池配置 protected GenericObjectPoolConfig poolConfig; // 默認創建tcp鏈接的超時時間 protected int connectionTimeout = Protocol.DEFAULT_TIMEOUT; // socket讀寫超時時間 protected int soTimeout = Protocol.DEFAULT_TIMEOUT; // 認證密碼 protected String password; // Redis中的數據庫 protected int database = Protocol.DEFAULT_DATABASE; protected String clientName; // 故障轉移器,用於實現master節點切換 protected Set<MasterListener> masterListeners = new HashSet<MasterListener>(); protected Logger log = LoggerFactory.getLogger(getClass().getName()); // 建立與Redis實例的鏈接的工廠,使用volatile,保證多線程下的可見性 private volatile JedisFactory factory; // 當前正在使用的master節點,使用volatile,保證多線程下的可見性 private volatile HostAndPort currentHostMaster; }
JedisSentinelPool的構造函數被重載不少,可是其中最核心的構造函數以下:
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; // 初始化sentinel HostAndPort master = initSentinels(sentinels, masterName); // 初始化redis實例鏈接池 initPool(master); }
繼續看initSentinels過程
// sentinels是sentinel配置:ip/port // masterName是sentinel名稱 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,尋找master節點 for (String sentinel : sentinels) { // 解析字符串ip:port -> HostAndPort對象 final HostAndPort hap = HostAndPort.parseString(sentinel); log.debug("Connecting to Sentinel {}", hap); Jedis jedis = null; try { // 建立與sentinel對應的jedis對象 jedis = new Jedis(hap); // 從sentinel獲取master節點 List<String> masterAddr = jedis.sentinelGetMasterAddrByName(masterName); // connected to sentinel... sentinelAvailable = true; // 若是爲空,或者不是ip和port組成的size爲2的list,則處理下一個sentinel if (masterAddr == null || masterAddr.size() != 2) { log.warn("Can not get master addr, master name: {}. Sentinel: {}", masterName, hap); continue; } // 構形成表示master的HostAndPort對象 master = toHostAndPort(masterAddr); log.debug("Found Redis master at {}", master); // 尋找到master,跳出循環 break; } catch (JedisException e) { // resolves #1036, it should handle JedisException there's another chance // of raising JedisDataException log.warn( "Cannot get master address from sentinel running @ {}. Reason: {}. Trying next one.", hap, e.toString()); } finally { if (jedis != null) { jedis.close(); } } } // 若是master爲空,則sentinel異常,throws ex 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集合,對每一個sentinel建立相應的監視器 // sentinel自己是集羣高可用,這裏須要爲每一個sentinel建立監視器,監視相應的sentinel // 即便sentinel掛掉一部分,仍然可用 for (String sentinel : sentinels) { final HostAndPort hap = HostAndPort.parseString(sentinel); // 建立sentinel監視器 MasterListener masterListener = new MasterListener(masterName, hap.getHost(), hap.getPort()); // whether MasterListener threads are alive or not, process can be stopped // sentinel設置爲守護線程 masterListener.setDaemon(true); masterListeners.add(masterListener); // 啓動線程監聽sentinel的事件通知 masterListener.start(); } return master; }
初始化sentinel中的主要邏輯分爲兩部分:
下面繼續探索initPool方法,該方法以初始化setntinel中尋找的master節點爲參數,進行初始化jedis與redis的master節點的JedisFactory。
// 該過程主要是爲了初始化jedis與master節點的JedisFactory對象 // 一旦JedisFactory被初始化,應用就能夠用其建立操做master節點相關的Jedis對象 private void initPool(HostAndPort master) { // 判斷當前的master節點是否與要設置的master相同,currentHostMaster是volatile變量 // 保證線程可見性 if (!master.equals(currentHostMaster)) { // 若是不相等,則從新設置當前的master節點 currentHostMaster = master; // 若是factory是空,則利用新的master建立factory if (factory == null) { factory = new JedisFactory(master.getHost(), master.getPort(), connectionTimeout, soTimeout, password, database, clientName); initPool(poolConfig, factory); } else { // 不然更新factory中的master節點 factory.setHostAndPort(currentHostMaster); // although we clear the pool, we still have to check the // returned object // in getResource, this call only clears idle instances, not // borrowed instances internalPool.clear(); } log.info("Created JedisPool to master at " + master); } }
initPool中完成了應用於redis的master節點的鏈接建立,Jedis對象工廠的建立。
這樣應用就可使用JedisSentinelPool的getResource方法獲取與master節點對應的Jedis對象對master節點進行讀寫。這些步驟主要用於應用啓動時執行與master節點的初始化操做。可是在應用運行期間,若是sentinel的master發生故障轉移,應用如何實現自動切換至新的master節點,這樣的功能主要是sentinel監視器MasterListener完成。接下來主要分析MasterListener的實現。
// MasterListener自己是一個線程對象的實現,因此sentinel模式中有幾個sentinel進程 // 應用就會爲其建立多少個相對應的線程監聽,這樣主要是爲了保證sentinel自己的高可用 protected class MasterListener extends Thread { // sentinel的名稱,應用一樣的Redis實例羣體能夠組建不一樣的sentinel protected String masterName; // 對應的sentinel host protected String host; // 對應的端口 protected int port; // 訂閱重試的等待時間,前文中介紹,實現自動故障轉移的核心是利用sentinel提供的 // pub/sub API,實現訂閱相應類型通道,接受相應的事件通知 protected long subscribeRetryWaitTimeMillis = 5000; // 與sentinel鏈接操做的Jedis protected volatile Jedis j; // 表示對應的sentinel是否正在運行 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; } }
實現自動轉移至新提高的master節點的邏輯在run方法中
@Override public void run() { // 線程第一次啓動時,設置sentinel運行標識爲true running.set(true); // 若是該sentinel仍然活躍,則循環 while (running.get()) { // 建立與該sentinel對應的jedis對象,用於操做該sentinel j = new Jedis(host, port); try { // 再次檢查,由於在以上的操做期間,該sentinel可能會銷燬,能夠查看shutdown方法 // double check that it is not being shutdown if (!running.get()) { break; } /* * Added code for active refresh */ // 獲取sentinel中的master節點 List<String> masterAddr = j.sentinelGetMasterAddrByName(masterName); if (masterAddr == null || masterAddr.size() != 2) { log.warn("Can not get master addr, master name: {}. Sentinel: {}:{}.",masterName,host,port); }else{ // 若是master合法,則調用initPoolf方法初始化與master節點的JedisFactory initPool(toHostAndPort(masterAddr)); } // 訂閱該sentinel的+switch-master通道。+switch-master通道的事件類型爲故障轉移,切換新的master的事件類型 j.subscribe(new JedisPubSub() { // redis sentinel中一旦發生故障轉移,切換master。就會收到消息,消息內容爲新提高的master節點 @Override public void onMessage(String channel, String message) { log.debug("Sentinel {}:{} published: {}.", host, port, message); // 解析消息獲取新提高的master節點 String[] switchMasterMsg = message.split(" "); if (switchMasterMsg.length > 3) { if (masterName.equals(switchMasterMsg[0])) { // 將應用的當前master改成新提高的master,初始化。實現應用端的故障轉移 initPool(toHostAndPort(Arrays.asList(switchMasterMsg[3], switchMasterMsg[4]))); } else { log.debug( "Ignoring message on +switch-master for master name {}, our master name is {}", switchMasterMsg[0], masterName); } } else { log.error( "Invalid message received on Sentinel {}:{} on channel +switch-master: {}", host, port, message); } } }, "+switch-master"); } catch (JedisException e) { // 若是繁盛異常,判斷對應的sentinel是否仍然處於運行狀態 if (running.get()) { // 若是是處於運行,則是鏈接問題,線程睡眠subscribeRetryWaitTimeMillis毫秒,而後while循環繼續訂閱+switch-master通道 log.error("Lost connection to Sentinel at {}:{}. Sleeping 5000ms and retrying.", host, port, e); try { Thread.sleep(subscribeRetryWaitTimeMillis); } catch (InterruptedException e1) { log.error("Sleep interrupted: ", e1); } } else { log.debug("Unsubscribing from Sentinel at {}:{}", host, port); } } finally { j.close(); } } }
以上的應用端實現故障發生時自動切換master節點的邏輯,註釋已經講述的很是清晰。這裏須要關注的幾點問題:
由於sentinel進程可能有多個,保證自身高可用。因此這裏MasterListener對應也有多個,因此對於實現切換master節點是多線程環境。其中優秀的地方在於沒有使用任何的同步,只是利用volatile保證可見性。由於對currentMaster和factory變量的操做,都只是賦值操做;
由於是多線程,因此initPool會被調用屢次。一個是應用啓動的main線程,還有就是N個sentinel對應的MasterListener監聽線程。因此initPool被調用N+1次,同時發生故障轉移時,將會被調用N次。可是即便是屢次初始化,master的參數都是同樣,基本上不會出現線程安全問題;
到這裏,Redis的Sentinel模式和Jedis中實現應用端的故障自動轉移就探索結束。下面再總結下Redis Sentinel模式在保證高可用的前提下的缺陷。
Redis Setninel模式當然結局了Redis單機的單點問題,實現高可用。可是它是基於主從模式,不管任何主從的實現,其中最爲關鍵的點就是數據一致性。在軟件架構中二者數據一致性的實現方式可謂五花八門:
在主從模式中,實現一致性,大多數是利用異步複製的方式,如:binlog、dumpfile、commandStream等等,且又分爲全量和增量方式結合使用。
通過以上描述,提出的問題:
在使用主從模式中,不少狀況下爲保證性能,常將master的持久化關閉,因此常常會出現主從所有宕機,當主從自啓動後,出現master的鍵空間爲空,從又異步同步主,致使從同步空的過來,致使主從數據都出現丟失!
在Redis Sentinel模式中儘可能設置主從禁止自啓動,或者主開啓持久化功能。