目前我們講的 Redis 還只是主從方案,最終一致性。讀者們可思考過,如果主節點凌晨3 點突發宕機怎么辦?就坐等運維從床上爬起來,然后手工進行從主切換,再通知所有的程序把地址統統改一遍重新上線么?毫無疑問,這樣的人工運維效率太低,事故發生時估計得至少 1 個小時才能緩過來。如果是一個大型公司,這樣的事故足以上新聞了。
所以我們必須有一個高可用方案來抵抗節點故障,當故障發生時可以自動進行從主切換,程序可以不用重啟,運維可以繼續睡大覺,仿佛什么事也沒發生一樣。Redis 官方提供了這樣一種方案 —— Redis Sentinel(哨兵)。

我們可以將 Redis Sentinel 集群看成是一個 ZooKeeper 集群,它是集群高可用的心臟,它一般是由 3~5 個節點組成,這樣掛了個別節點集群還可以正常運轉。
它負責持續監控主從節點的健康,當主節點掛掉時,自動選擇一個最優的從節點切換為主節點。客戶端來連接集群時,會首先連接 sentinel,通過 sentinel 來查詢主節點的地址,然后再去連接主節點進行數據交互。當主節點發生故障時,客戶端會重新向 sentinel 要地址,sentinel 會將最新的主節點地址告訴客戶端。如此應用程序將無需重啟即可自動完成節點切換。比如上圖的主節點掛掉后,集群將可能自動調整為下圖所示結構。

從這張圖中我們能看到主節點掛掉了,原先的主從復制也斷開了,客戶端和損壞的主節點也斷開了。從節點被提升為新的主節點,其它從節點開始和新的主節點建立復制關系。客戶端通過新的主節點繼續進行交互。Sentinel 會持續監控已經掛掉了主節點,待它恢復后,集群會調整為下面這張圖。

此時原先掛掉的主節點現在變成了從節點,從新的主節點那里建立復制關系。
消息丟失
Redis 主從采用異步復制,意味着當主節點掛掉時,從節點可能沒有收到全部的同步消息,這部分未同步的消息就丟失了。如果主從延遲特別大,那么丟失的數據就可能會特別多。Sentinel 無法保證消息完全不丟失,但是也盡可能保證消息少丟失。它有兩個選項可以限制主從延遲過大。
min-slaves-to-write 1
min-slaves-max-lag 10
第一個參數表示主節點必須至少有一個從節點在進行正常復制,否則就停止對外寫服務,喪失可用性。
何為正常復制,何為異常復制?這個就是由第二個參數控制的,它的單位是秒,表示如果 10s 沒有收到從節點的反饋,就意味着從節點同步不正常,要么網絡斷開了,要么一直沒有給反饋。
Sentinel 基本使用
接下來我們看看客戶端如何使用 sentinel,標准的流程應該是客戶端可以通過 sentinel 發現主從節點的地址,然后在通過這些地址建立相應的連接來進行數據存取操作。我們來看看 Python 客戶端是如何做的。
>>> from redis.sentinel import Sentinel
>>> sentinel = Sentinel([('localhost', 26379)], socket_timeout=0.1)
>>> sentinel.discover_master('mymaster')
('127.0.0.1', 6379)
>>> sentinel.discover_slaves('mymaster')
[('127.0.0.1', 6380)]
sentinel 的默認端口是 26379,不同於 Redis 的默認端口 6379,通過 sentinel 對象的discover_xxx 方法可以發現主從地址,主地址只有一個,從地址可以有多個。
>>> master = sentinel.master_for('mymaster', socket_timeout=0.1)
>>> slave = sentinel.slave_for('mymaster', socket_timeout=0.1)
>>> master.set('foo', 'bar')
>>> slave.get('foo')
'bar'
通過 xxx_for 方法可以從連接池中拿出一個連接來使用,因為從地址有多個,redis 客戶端對從地址采用輪詢方案,也就是 RoundRobin 輪着來。有個問題是,但 sentinel 進行主從切換時,客戶端如何知道地址變更了 ? 通過分析源碼,我發現 redis-py 在建立連接的時候進行了主庫地址變更判斷。連接池建立新連接時,會去查詢主庫地址,然后跟內存中的主庫地址進行比對,如果變更了,就斷開所有連接,重新使用新地址建立新連接。如果是舊的主庫掛掉了,那么所有正在使用的連接都會被關閉,然后在重連時就會用上新地址。但是這樣還不夠,如果是 sentinel 主動進行主從切換,主庫並沒有掛掉,而之前的主庫連接已經建立了在使用了,沒有新連接需要建立,那這個連接是不是一致切換不了?繼續深入研究源碼,我發現 redis-py 在另外一個點也做了控制。那就是在處理命令的時候捕獲了一個特殊的異常 ReadOnlyError,在這個異常里將所有的舊連接全部關閉了,后續指令就會進行重連。主從切換后,之前的主庫被降級到從庫,所有的修改性的指令都會拋出 ReadonlyError。如果沒有修改性指令,雖然連接不會得到切換,但是數據不會被破壞,所以即使不切換也沒關系。
