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

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

從這張圖中我們能看到主節(jié)點掛掉了,原先的主從復(fù)制也斷開了,客戶端和損壞的主節(jié)點也斷開了。從節(jié)點被提升為新的主節(jié)點,其它從節(jié)點開始和新的主節(jié)點建立復(fù)制關(guān)系??蛻舳送ㄟ^新的主節(jié)點繼續(xù)進行交互。Sentinel 會持續(xù)監(jiān)控已經(jīng)掛掉了主節(jié)點,待它恢復(fù)后,集群會調(diào)整為下面這張圖。

此時原先掛掉的主節(jié)點現(xiàn)在變成了從節(jié)點,從新的主節(jié)點那里建立復(fù)制關(guān)系。
消息丟失
Redis 主從采用異步復(fù)制,意味著當主節(jié)點掛掉時,從節(jié)點可能沒有收到全部的同步消息,這部分未同步的消息就丟失了。如果主從延遲特別大,那么丟失的數(shù)據(jù)就可能會特別多。Sentinel 無法保證消息完全不丟失,但是也盡可能保證消息少丟失。它有兩個選項可以限制主從延遲過大。
min-slaves-to-write 1
min-slaves-max-lag 10
第一個參數(shù)表示主節(jié)點必須至少有一個從節(jié)點在進行正常復(fù)制,否則就停止對外寫服務(wù),喪失可用性。
何為正常復(fù)制,何為異常復(fù)制?這個就是由第二個參數(shù)控制的,它的單位是秒,表示如果 10s 沒有收到從節(jié)點的反饋,就意味著從節(jié)點同步不正常,要么網(wǎng)絡(luò)斷開了,要么一直沒有給反饋。
Sentinel 基本使用
接下來我們看看客戶端如何使用 sentinel,標準的流程應(yīng)該是客戶端可以通過 sentinel 發(fā)現(xiàn)主從節(jié)點的地址,然后在通過這些地址建立相應(yīng)的連接來進行數(shù)據(jù)存取操作。我們來看看 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 方法可以發(fā)現(xiàn)主從地址,主地址只有一個,從地址可以有多個。
>>> 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 進行主從切換時,客戶端如何知道地址變更了 ? 通過分析源碼,我發(fā)現(xiàn) redis-py 在建立連接的時候進行了主庫地址變更判斷。
連接池建立新連接時,會去查詢主庫地址,然后跟內(nèi)存中的主庫地址進行比對,如果變更了,就斷開所有連接,重新使用新地址建立新連接。如果是舊的主庫掛掉了,那么所有正在使用的連接都會被關(guān)閉,然后在重連時就會用上新地址。
但是這樣還不夠,如果是 sentinel 主動進行主從切換,主庫并沒有掛掉,而之前的主庫連接已經(jīng)建立了在使用了,沒有新連接需要建立,那這個連接是不是一致切換不了?
繼續(xù)深入研究源碼,我發(fā)現(xiàn) redis-py 在另外一個點也做了控制。那就是在處理命令的時候捕獲了一個特殊的異常ReadOnlyError,在這個異常里將所有的舊連接全部關(guān)閉了,后續(xù)指令就會進行重連。
主從切換后,之前的主庫被降級到從庫,所有的修改性的指令都會拋出ReadonlyError。如果沒有修改性指令,雖然連接不會得到切換,但是數(shù)據(jù)不會被破壞,所以即使不切換也沒關(guān)系。