高可用 Redis 服務架構分析與搭建

基於內存的 Redis 應該是目前各種 web 開發業務中最爲常用的 key-value 數據庫了,我們經常在業務中用其存儲用戶登陸態(Session 存儲),加速一些熱數據的查詢(相比較 mysql 而言,速度有數量級的提升),做簡單的消息隊列(LPUSH 和 BRPOP)、訂閱發佈(PUB/SUB)系統等等。規模比較大的互聯網公司,一般都會有專門的團隊,將 Redis 存儲以基礎服務的形式提供給各個業務調用。

不過任何一個基礎服務的提供方,都會被調用方問起的一個問題是:你的服務是否具有高可用性?最好不要因爲你的服務經常出問題,導致我這邊的業務跟着遭殃。最近我所在的項目中也自己搭了一套小型的 “高可用”Redis 服務,在此做一下自己的總結和思考。

首先我們要定義一下對於 Redis 服務來說怎樣纔算是高可用,即在各種出現異常的情況下,依然可以正常提供服務。或者寬鬆一些,出現異常的情況下,只經過很短暫的時間即可恢復正常服務。所謂異常,應該至少包含了以下幾種可能性:

【異常 1】某個節點服務器的某個進程突然 down 掉(例如某開發手殘,把一臺服務器的 redis-server 進程 kill 了)

【異常 2】某臺節點服務器 down 掉,相當於這個節點上所有進程都停了(例如某運維手殘,把一個服務器的電源拔了;例如一些老舊機器出現硬件故障)

【異常 3】任意兩個節點服務器之間的通信中斷了(例如某臨時工手殘,把用於兩個機房通信的光纜挖斷了)

其實以上任意一種異常都是小概率事件,而做到高可用性的基本指導思想就是:多個小概率事件同時發生的概率可以忽略不計。只要我們設計的系統可以容忍短時間內的單點故障,即可實現高可用性。

對於搭建高可用 Redis 服務,網上已有了很多方案,例如 Keepalived,Codis,Twemproxy,Redis Sentinel。其中 Codis 和 Twemproxy 主要是用於大規模的 Redis 集羣中,也是在 Redis 官方發佈 Redis Sentinel 之前 twitter 和豌豆莢提供的開源解決方案。我的業務中數據量並不大,所以搞集羣服務反而是浪費機器了。最終在 Keepalived 和 Redis Sentinel 之間做了個選擇,選擇了官方的解決方案 Redis Sentinel。

Redis Sentinel 可以理解爲一個監控 Redis Server 服務是否正常的進程,並且一旦檢測到不正常,可以自動地將備份(slave)Redis Server 啓用,使得外部用戶對 Redis 服務內部出現的異常無感知。我們按照由簡至繁的步驟,搭建一個最小型的高可用的 Redis 服務。

方案 1:單機版 Redis Server,無 Sentinel

一般情況下,我們搭的個人網站,或者平時做開發時,會起一個單實例的 Redis Server。調用方直接連接 Redis 服務即可,甚至 Client 和 Redis 本身就處於同一臺服務器上。這種搭配僅適合個人學習娛樂,畢竟這種配置總會有單點故障的問題無法解決。一旦 Redis 服務進程掛了,或者服務器 1 停機了,那麼服務就不可用了。並且如果沒有配置 Redis 數據持久化的話,Redis 內部已經存儲的數據也會丟失。

方案 2:主從同步 Redis Server,單實例 Sentinel

爲了實現高可用,解決方案 1 中所述的單點故障問題,我們必須增加一個備份服務,即在兩臺服務器上分別各啓動一個 Redis Server 進程,一般情況下由 master 提供服務,slave 只負責同步和備份。與此同時,在額外啓動一個 Sentinel 進程,監控兩個 Redis Server 實例的可用性,以便在 master 掛掉的時候,及時把 slave 提升到 master 的角色繼續提供服務,這樣就實現了 Redis Server 的高可用。這基於一個高可用服務設計的依據,即單點故障本身就是個小概率事件,而多個單點同時故障(即 master 和 slave 同時掛掉),可以認爲是(基本)不可能發生的事件。

對於 Redis 服務的調用方來說,現在要連接的是 Redis Sentinel 服務,而不是 Redis Server 了。常見的調用過程是,client 先連接 Redis Sentinel 並詢問目前 Redis Server 中哪個服務是 master,哪些是 slave,然後再去連接相應的 Redis Server 進行操作。當然目前的第三方庫一般都已經實現了這一調用過程,不再需要我們手動去實現(例如 Nodejs 的 ioredis,PHP 的 predis,Golang 的 go-redis/redis,JAVA 的 jedis 等)。

然而,我們實現了 Redis Server 服務的主從切換之後,又引入了一個新的問題,即 Redis Sentinel 本身也是個單點服務,一旦 Sentinel 進程掛了,那麼客戶端就沒辦法鏈接 Sentinel 了。所以說,方案 2 的配置並無法實現高可用性。

方案 3:主從同步 Redis Server,雙實例 Sentinel

爲了解決方案 2 的問題,我們把 Redis Sentinel 進程也額外啓動一份,兩個 Sentinel 進程同時爲客戶端提供服務發現的功能。對於客戶端來說,它可以連接任何一個 Redis Sentinel 服務,來獲取當前 Redis Server 實例的基本信息。通常情況下,我們會在 Client 端配置多個 Redis Sentinel 的鏈接地址,Client 一旦發現某個地址連接不上,會去試圖連接其他的 Sentinel 實例,這當然也不需要我們手動實現,各個開發語言中比較熱門的 redis 連接庫都幫我們實現了這個功能。我們預期是:即使其中一個 Redis Sentinel 掛掉了,還有另外一個 Sentinel 可以提供服務。

然而,願景是美好的,現實卻是很殘酷的。如此架構下,依然無法實現 Redis 服務的高可用。方案 3 示意圖中,紅線部分是兩臺服務器之間的通信,而我們所設想的異常場景(【異常 2】)是,某臺服務器整體 down 機,不妨假設服務器 1 停機,此時,只剩下服務器 2 上面的 Redis Sentinel 和 slave Redis Server 進程。這時,Sentinel 其實是不會將僅剩的 slave 切換成 master 繼續服務的,也就導致 Redis 服務不可用,因爲 Redis 的設定是隻有當超過 50% 的 Sentinel 進程可以連通並投票選取新的 master 時,纔會真正發生主從切換。本例中兩個 Sentinel 只有一個可以連通,等於 50% 並不在可以主從切換的場景中。

你可能會問,爲什麼 Redis 要有這個 50% 的設定?假設我們允許小於等於 50% 的 Sentinel 連通的場景下也可以進行主從切換。試想一下【異常 3】,即服務器 1 和服務器 2 之間的網絡中斷,但是服務器本身是可以運行的。如下圖所示:

實際上對於服務器 2 來說,服務器 1 直接 down 掉和服務器 1 網絡連不通是一樣的效果,反正都是突然就無法進行任何通信了。假設網絡中斷時我們允許服務器 2 的 Sentinel 把 slave 切換爲 master,結果就是你現在擁有了兩個可以對外提供服務的 Redis Server。Client 做任何的增刪改操作,有可能落在服務器 1 的 Redis 上,也有可能落在服務器 2 的 Redis 上(取決於 Client 到底連通的是哪個 Sentinel),造成數據混亂。即使後面服務器 1 和服務器 2 之間的網絡又恢復了,那我們也無法把數據統一了(兩份不一樣的數據,到底該信任誰呢?),數據一致性完全被破壞。

方案 4:主從同步 Redis Server,三實例 Sentinel

鑑於方案 3 並沒有辦法做到高可用,我們最終的版本就是上圖所示的方案 4 了。實際上這就是我們最終搭建的架構。我們引入了服務器 3,並且在 3 上面又搭建起一個 Redis Sentinel 進程,現在由三個 Sentinel 進程來管理兩個 Redis Server 實例。這種場景下,不管是單一進程故障、還是單個機器故障、還是某兩個機器網絡通信故障,都可以繼續對外提供 Redis 服務。

實際上,如果你的機器比較空閒,當然也可以把服務器 3 上面也開啓一個 Redis Server,形成 1 master + 2 slave 的架構,每個數據都有兩個備份,可用性會提升一些。當然也並不是 slave 越多越好,畢竟主從同步也是需要時間成本的。

在方案 4 中,一旦服務器 1 和其他服務器的通信完全中斷,那麼服務器 2 和 3 會將 slave 切換爲 master。對於客戶端來說,在這麼一瞬間會有 2 個 master 提供服務,並且一旦網絡恢復了,那麼所有在中斷期間落在服務器 1 上的新數據都會丟失。如果想要部分解決這個問題,可以配置 Redis Server 進程,讓其在檢測到自己網絡有問題的時候,立即停止服務,避免在網絡故障期間還有新數據進來(可以參考 Redis 的 min-slaves-to-write 和 min-slaves-max-lag 這兩個配置項)。

至此,我們就用 3 臺機器搭建了一個高可用的 Redis 服務。其實網上還有更加節省機器的辦法,就是把一個 Sentinel 進程放在 Client 機器上,而不是服務提供方的機器上。只不過在公司裏面,一般服務的提供方和調用方並不來自同一個團隊。兩個團隊共同操作同一個機器,很容易因爲溝通問題導致一些誤操作,所以出於這種人爲因素的考慮,我們還是採用了方案 4 的架構。並且由於服務器 3 上面只跑了一個 Sentinel 進程,對服務器資源消耗並不多,還可以用服務器 3 來跑一些其他的服務。 

易用性:像使用單機版 Redis 一樣使用 Redis Sentinel

作爲服務的提供方,我們總是會講到用戶體驗問題。在上述方案當中始終有一個讓 Client 端用的不是那麼舒服的地方。對於單機版 Redis,Client 端直接連接 Redis Server,我們只需要給一個 ip 和 port,Client 就可以使用我們的服務了。而改造成 Sentinel 模式之後,Client 不得不採用一些支持 Sentinel 模式的外部依賴包,並且還要修改自己的 Redis 連接配置,這對於 “矯情” 的用戶來講顯然是不能接收的。有沒有辦法還是像在使用單機版的 Redis 那樣,只給 Client 一個固定的 ip 和 port 就可以提供服務呢?

答案當然是肯定的。這可能就要引入虛擬 IP(Virtual IP,VIP),如上圖所示。我們可以把虛擬 IP 指向 Redis Server master 所在的服務器,在發生 Redis 主從切換的時候,會觸發一個回調腳本,回調腳本中將 VIP 切換至 slave 所在的服務器。這樣對於 Client 端來說,他彷彿在使用的依然是一個單機版的高可用 Redis 服務。

結語

搭建任何一個服務,做到 “能用” 其實是非常簡單的,就像我們運行一個單機版的 Redis。不過一旦要做到“高可用”,事情就會變得複雜起來。業務中使用了額外的兩臺服務器,3 個 Sentinel 進程 + 1 個 Slave 進程,只是爲了保證在那小概率的事故中依然做到服務可用。在實際業務中我們還啓用了 supervisor 做進程監控,一旦進程意外退出,會自動嘗試重新啓動。

關注作者微信公衆號 —《JAVA 爛豬皮》

作者:HorstXu

出處:https://www.cnblogs.com/xuning/p/8464625.html

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/1ouxZMgohHU7TiHOxdT63g