一文讀懂 Redis 架構演化之路

圖片

導語|近年來,Redis 變得越來越流行。Redis 持久化、主從複製、哨兵、分片集羣是開發者常遇到的、看似容易理解的概念。它們存在什麼聯繫?Redis 爲什麼會演化出幾種架構模式?騰訊雲後臺開發工程師譚帥將帶你一步步構建出穩定、高性能的 Redis 集羣。瞭解 Redis 做了哪些方案來實現穩定與高性能之後,你在日常使用 Redis 時,能夠更加遊刃有餘。

目錄

1 情境引入:單機版 Redis

2 數據持久化:有備無患

3 主從複製:多副本

4 哨兵:故障自動切換

5 分片集羣:橫向擴展

6 總結

現如今 Redis 變得越來越流行,在很多項目中被用到。開發者們在使用 Redis 時,常常思考一個問題:Redis 是如何穩定、高性能地提供服務的?各位開發者也可以嘗試回答一下以下這些問題:

● 我使用 Redis 的場景很簡單,只使用單機版 Redis 會有什麼問題嗎?

● 我的 Redis 故障宕機了,數據丟失了怎麼辦?

● 如何能保證我的業務、應用不受影響?

● 爲什麼需要主從集羣?它有什麼優勢?

● 什麼是分片集羣?我真的需要分片集羣嗎?

如果你已經對 Redis 有些許瞭解,肯定聽說過數據持久化、主從複製、哨兵這些概念。它們之間又有什麼區別和聯繫呢?如果你存在這樣的疑惑,這篇文章會從 0 到 1、再從 1 到 N,帶你一步步構建出一個穩定、高性能的 Redis 集羣。

在這個過程中,你可以瞭解到 Redis 爲了做到穩定、高性能所採取的優化方案及其原因。掌握了這些原理,你在日常使用 Redis 時,能夠做到加倍遊刃有餘。

01、情境引入:單機版 Redis

首先,我們從最簡單的場景來假設,以便各位由淺入深理解。現在你有一個業務、應用,需要引入 Redis 來提高應用的性能。你可以選擇部署一個單機版的 Redis 來使用。就像這樣:

圖片

這個架構非常簡單,你的業務、應用可以把 Redis 當做緩存來使用。從 MySQL 中查詢數據,寫入到 Redis 中。業務、應用再從 Redis 中讀取這些數據。Redis 的數據都存儲在內存中,所以速度飛快。

如果你的業務體量並不大,那這樣的架構模型基本可以滿足你的需求。是不是很簡單?隨着時間的推移,你的業務體量逐漸發展起來了,Redis 中存儲的數據也越來越多,此時你的業務、應用對 Redis 的依賴也越來越重。

突然有一天,你的 Redis 因爲某些原因宕機了,你的所有業務流量都會打到後端 MySQL 上。這會導致你的 MySQL 壓力劇增,嚴重的話甚至會壓垮 MySQL。

圖片

這時你應該怎麼辦?我猜你的方案是:趕緊重啓 Redis,讓它可以繼續提供服務。但是,因爲之前 Redis 中的數據都在內存中,儘管你現在把 Redis 重啓了,之前的數據也都丟失了。重啓後的 Redis 雖然可以正常工作,但是由於 Redis 中沒有任何數據,業務流量還是都會打到後端 MySQL 上,MySQL 的壓力還是很大

這可怎麼辦?你陷入了沉思。有沒有什麼好的辦法解決這個問題?既然 Redis 只把數據存儲在內存中,那是否可以把這些數據也寫一份到磁盤上呢?如果採用這種方式,當 Redis 重啓時,我們把磁盤中的數據快速恢復到內存中,它就可以繼續正常提供服務了。這是一個很好的解決方案。把內存數據寫到磁盤上的過程,就是「數據持久化」。

02、數據持久化:有備無患

現在,你設想的 Redis 數據持久化是這樣的:

圖片

但是,數據持久化具體應該怎麼做呢?我猜你想到的方案是:Redis 每一次執行寫操作,除了寫內存之外,同時也寫一份到磁盤上。就像這樣:

圖片

沒錯,這是最簡單直接的方案。但仔細想一下,這個方案有個問題:客戶端的每次寫操作,既需要寫內存,又需要寫磁盤。寫磁盤的耗時相比於寫內存來說要大很多。這勢必會影響到整體寫性能

如何規避這個問題?我們可以這樣優化:Redis 寫內存由主線程來做,寫內存完成後就給客戶端返回結果。Redis 用另一個線程去寫磁盤,這樣就可以避免主線程寫磁盤對性能的影響。這確實是一個好方案。除此之外,我們可以換個角度,思考一下還有什麼方式可以持久化數據?各位讀者可以結合 Redis 的使用場景來考慮。

回憶一下:我們在使用 Redis 時,通常把它用作什麼場景?那就是「緩存」。把 Redis 當做緩存來用的話,儘管 Redis 中沒有保存全量數據,我們的業務、應用依舊可以通過查詢後端數據庫得到那些不在緩存中的數據。只不過查詢後端數據的速度會慢一點,但對業務結果沒有影響。

基於這個特點, Redis 數據持久化還可以用「數據快照」的方式來做。什麼是數據快照呢?你可以這麼理解:把 Redis 想象成一個水杯,向 Redis 寫入數據,就相當於往這個杯子裏倒水;此時你拿一個相機給這個水杯拍一張照片,拍照的這一瞬間,照片記錄到這個水杯中水的容量,就是水杯的數據快照。

圖片

也就是說,Redis 的數據快照是記錄某一時刻 Redis 中的數據,之後只需要把這個數據快照寫到磁盤上就可以了。它的優勢在於:需要持久化時,把數據一次性地寫入磁盤即可,其它時間都不需要操作磁盤。基於這個方案,我們可以定時給 Redis 做數據快照,把數據持久化到磁盤上。

圖片

其實,上面說的這些持久化方案,就是 Redis 的「RDB」和「AOF」:

RDB:只持久化某一時刻的數據快照到磁盤上(創建一個子進程來做);

AOF:每一次寫操作都持久到磁盤(主線程寫內存,根據策略實際情況來決定配置由主線程還是子線程進行數據持久化)。

它們的區別除了上面講到的,還有以下特點:RDB 採用二進制 + 數據壓縮的方式寫磁盤,文件體積小、數據恢復速度也快;AOF 記錄的是每一次寫命令,數據最全但文件體積大、數據恢復速度慢。

如果讓你來選擇持久化方案,你可以這樣選擇

● 如果你的業務對於數據丟失不敏感,採用 RDB 方案持久化數據;

● 如果你的業務對數據完整性要求比較高,採用 AOF 方案持久化數據。

假設你的業務對 Redis 數據完整性要求比較高,選擇了 AOF 方案。此時你又會遇到這些問題

● AOF 記錄每一次寫操作,隨着時間增長,AOF 文件體積會越來越大;

● 巨大的 AOF 文件在數據恢復時變得非常慢。

這怎麼辦?數據完整性要求變高了,恢復數據也變困難了。有沒有什麼方法,可以縮小文件體積、提升恢復速度呢?下文我們繼續來分析 AOF 的特點。

AOF 文件中記錄的是每一次寫操作。同一個 key 可能會發生的多次修改,我們只保留其最後一次被修改的值,是不是也可以?答案是正確。這就是我們經常聽到的「AOF rewrite」,你也可以把它理解爲 「AOF 瘦身」。開發者可以對 AOF 文件定時 rewrite,避免這個文件體積持續膨脹,以保障在恢復時可以縮短恢復時間。

圖片

各位開發者們再進一步思考:還有沒有辦法繼續縮小 AOF 文件?我們前面講到 RDB 和 AOF 各自的特點是 RDB 以二進制 + 數據壓縮方式存儲,文件體積小;而且 AOF 記錄每一次寫命令,數據最全。

我們可否利用它們各自的優勢呢?答案是可以。這就是 Redis 的「混合持久化」。具體來說,當 AOF rewrite 時,Redis 先以 RDB 格式在 AOF 文件中寫入一個數據快照,再把在這期間產生的每一個寫命令,追加到 AOF 文件中。因爲 RDB 是二進制壓縮寫入的,所以 AOF 文件體積就變得更小了。

圖片

此時,你在使用 AOF 文件恢復數據時,恢復時間就會更短。Redis 4.0 以上版本才支持混合持久化。經過這麼一番優化,你的 Redis 再也不用擔心實例宕機了。當發生宕機時,你就可以用持久化文件快速恢復 Redis 中的數據。

但這樣就沒問題了嗎?實際上,雖然我們已經把持久化的文件優化到最小,但在恢復數據時依舊是需要時間的。在這期間你的業務、應用還是會受到影響,怎麼辦?我們接下來分析有沒有更好的方案。

一個實例宕機只能用恢復數據來解決,那我們是否可以部署多個 Redis 實例,讓這些實例數據保持實時同步?這樣當一個實例宕機時,我們在剩下的實例中選擇一個繼續提供服務就好了。沒錯,這個方案就是接下來要講的「主從複製:多副本」。

03、主從複製:多副本

此時,你可以部署多個 Redis 實例,架構模型就變成了這樣:

圖片

我們這裏把實時讀寫的節點叫做 「master」,另一個實時同步數據的節點叫做 「slave」。採用多副本方案的優勢是:

縮短不可用時間:master 發生宕機,我們可以手動把 slave 提升爲 master 繼續提供服務;

提升讀性能:讓 slave 分擔一部分讀請求,提升應用的整體性能。

圖片

這個方案不僅節省了數據恢復的時間,還能提升性能。那它有什麼問題嗎?其實,它的問題在於:當 master 宕機時,我們需要手動把 slave 提升爲 master,這個過程也是需要花費時間的。雖然比恢復數據要快得多,但還是需要人工介入處理。一旦需要人工介入,就必須要算上人的反應時間、操作時間。所以,在這期間你的業務、應用依舊會受到影響。

怎麼解決這個問題?我們是否可以把這個切換的過程自動化呢?對於這種情況,我們需要一個「故障自動切換」機制——這就是我們經常聽到的「哨兵」所具備的能力。

04、哨兵:故障自動切換

現在,我們可以引入一個「觀察者」,讓這個觀察者去實時監測 master 的健康狀態。這個觀察者就是「哨兵」。具體如何做:

● 第一,哨兵每間隔一段時間,詢問 master 是否正常;

● 第二,master 正常回復則表示狀態正常,回覆超時表示異常;

● 第三,哨兵發現異常,發起主從切換。

圖片

有了這個方案,就不需要人去介入處理、一切變得自動化。但這裏還有一個問題:如果 master 狀態正常,但這個哨兵在詢問 master 時,它們之間的網絡發生了問題,那這個哨兵可能會誤判。

圖片

這個問題怎麼解決?答案是:我們可以部署多個哨兵,讓它們分佈在不同的機器上、一起監測 master 的狀態。流程如下:

● 首先,多個哨兵每間隔一段時間,詢問 master 是否正常;

● 其次,master 正常回復,表示狀態正常,回覆超時表示異常;

● 再次,一旦有一個哨兵判定 master 異常(不管是否是網絡問題),就詢問其它哨兵,如果多個哨兵(設置一個閾值)都認爲 master 異常了,這才判定 master 確實發生了故障;

● 最後,多個哨兵經過協商後,判定 master 故障,則發起主從切換。

所以,我們用多個哨兵互相協商來判定 master 的狀態。這樣一來,就可以大大降低誤判的概率。哨兵協商判定 master 異常後,還有一個問題:由哪個哨兵來發起主從切換呢?答案是,選出一個哨兵「領導者」,由這個領導者進行主從切換。

那這個領導者怎麼選?想象一下,現實生活中選舉是怎麼做的?投票。在選舉哨兵領導者時,我們可以制定一個選舉規則

● 每個哨兵都詢問其它哨兵,請求對方爲自己投票;

● 每個哨兵只投票給第一個請求投票的哨兵,且只能投票一次;

● 首先拿到超過半數投票的哨兵當選爲領導者,發起主從切換。

其實,這個選舉的過程就是我們經常聽到的:分佈式系統領域中的「共識算法」。什麼是共識算法?我們在多個機器部署哨兵,它們需要共同協作完成一項任務。所以它們就組成了一個「分佈式系統」。在分佈式系統領域,多個節點如何就一個問題達成共識的算法,就叫共識算法。

在這個場景下,多個哨兵共同協商、選舉出一個都認可的領導者,就是使用共識算法完成的。這個算法規定節點的數量必須是奇數個,保證系統中即使有節點發生了故障事剩餘超過半數的節點狀態正常,從而依舊可以提供正確的結果。也就是說,這個算法兼容了存在故障節點的情況。

共識算法在分佈式系統領域有很多。例如 Paxos、Raft。哨兵選舉領導者這個場景,使用的是 「Raft 共識算法」。因爲它足夠簡單且易於實現。現在,我們用多個哨兵共同監測 Redis 的狀態。這樣一來就可以避免誤判的問題。架構模型變成了這樣:

圖片

我們先小結一下:你的 Redis 從最簡單的單機版,經過數據持久化、主從多副本、哨兵集羣的優化,你的 Redis 不管是性能還是穩定性都越來越高,就算節點發生故障也不用擔心。你的 Redis 以這樣的架構模式部署,基本可以穩定運行很長時間**。**

隨着時間的發展,你的業務體量開始迎來了爆炸性增長。此時你的架構模型,還能夠承擔這麼大的流量嗎?我們一起來分析一下:

穩定性:當Redis 故障宕機時,我們有哨兵 + 副本,可以自動完成主從切換;

性能:當讀請求量增長,我們可以再部署多個 slave,實現讀寫分離以分擔讀壓力;當寫請求量增長,我們只有一個 master 實例。這個實例達到瓶頸怎麼辦?

當你的寫請求量越來越大時,一個 master 實例可能就無法承擔這麼大的寫流量了。要想完美解決這個問題,此時你就需要考慮使用「分片集羣」了。

05、分片集羣:橫向擴展

什麼是「分片集羣」?一個實例扛不住寫壓力,我們可以部署多個實例。把這些實例按照一定規則組織起來,把它們當成一個整體來對外提供服務,就可以解決集中寫一個實例的瓶頸問題。所以,現在的架構模型就變成了這樣:

圖片

現在問題又來了:這麼多實例如何組織呢?我們制定規則如下:

● 每個節點各自存儲一部分數據,所有節點數據之和纔是全量數據;

● 制定一個路由規則,將不同的 key 路由到固定一個實例上進行讀寫。

分片集羣根據路由規則所在位置的不同,還可以分爲兩大類:「客戶端分片」、「服務端分片」。

「客戶端分片」 指的是 key 的路由規則放在客戶端來做,就是下面這樣:

圖片

這個方案的缺點是客戶端需要維護路由規則。也就是說,你需要把路由規則寫到你的業務代碼中。如何做到不把路由規則耦合在業務代碼中呢?你可以這樣優化:把這個路由規則封裝成一個模塊。當需要使用時,集成這個模塊。

這就是 Redis Cluster 的採用的方案。

圖片

Redis Cluster 內置了哨兵邏輯,無需再部署哨兵。當你使用 Redis Cluster 時,你的業務、應用需要使用配套的 Redis SDK。這個 SDK 內就集成好了路由規則,不需要你自己編寫。

再來看「服務端分片」。這種方案指的是:路由規則不放在客戶端來做,而是在客戶端和服務端之間增加一個「中間代理層」。這個代理就是我們經常聽到的 Proxy。數據的路由規則,就放在這個 Proxy 層來維護。

這樣一來,你無需關心服務端有多少個 Redis 節點,只需要和這個 「Proxy」 交互即可。Proxy 會把你的請求根據路由規則轉發到對應的 Redis 節點上。而且,當集羣實例不足以支撐更大的流量請求時,還可以橫向擴容、添加新的 Redis 實例提升性能。這一切對於你的客戶端來說都是透明無感知的。

這是業界開源的 Redis 分片集羣方案, Twemproxy、Codis 也是採用這種方案。

圖片

分片集羣在數據擴容時,還涉及到了很多細節,此處不加以贅述。如果你對相關內容感興趣,歡迎留言!至此,使用分片集羣后,你可以面對未來更大的流量壓力。

06、總結

最後我們來總結一下,我們是如何一步步構建一個穩定、高性能的 Redis 集羣的。首先,在使用最簡單的單機版 Redis 時,我們發現:當 Redis 故障宕機後,數據無法恢復。因此我們想到了「數據持久化」——把內存中的數據也持久化到磁盤上一份, Redis 重啓後就可以從磁盤上快速恢復數據。

在進行數據持久化時,我們又面臨如何更高效地將數據持久化到磁盤的問題。之後我們發現 Redis 提供了 「RDB」 和「 AOF」 兩種方案,分別對應了數據快照和實時的命令記錄。當我們對數據完整性要求不高時,可以選擇 RDB 持久化方案。如果對於數據完整性要求較高,可以選擇 AOF 持久化方案。

但是 AOF 文件體積會隨着時間增長變得越來越大。此時我們想到的優化方案是:使用 AOF rewrite 的方式對其進行瘦身,減小文件體積。再後來,我們發現可以結合 RDB 和 AOF 各自的優勢,在 AOF rewrite 時使用兩者結合的「混合持久化」方式,進一步減小 AOF 文件體積。

之後,我們發現儘管可以通過數據恢復的方式還原數據,恢復數據也需花費時間。這意味着業務、應用還是會受到影響。我們進一步採用「多副本」的方案——讓多個實例保持實時同步。當一個實例故障時,可以手動把其它實例提升上來繼續提供服務。

但是手動提升實例上來也需要人工介入,人工介入操作需要時間。我們開始想辦法把這個流程變得自動化。所以我們又引入了「哨兵」集羣——哨兵集羣通過互相協商的方式來發現故障節點,並可以自動完成切換。這大幅降低了對業務、應用的影響。

最後,我們把關注點聚焦在如何支撐更大的寫流量上。我們又引入了「分片集羣」來解決這個問題——多個 Redis 實例分攤寫壓力,從而面對未來更大的流量。我們還可以添加新的實例、橫向擴展,進一步提升集羣的性能。

至此,我們的 Redis 集羣才得以長期穩定、高性能地爲開發者的業務提供服務。最後我整理了一個思維導圖,方便各位開發者更好地去理解它們之間的關係以及演化的過程。以上便是本次分享的全部內容,歡迎各位開發者在評論區交流討論。

圖片

-End-

原創作者|譚帥

技術責編|譚帥

你可能感興趣的騰訊工程師作品

騰訊工程師解讀ChatGPT技術「精選系列文集」

國民應用QQ如何實現高可用的訂閱推送系統

| 騰訊雲開發者熱門技術乾貨彙總

7天DAU超億級,《羊了個羊》技術架構升級實戰

技術盲盒:前端後端AI與算法運維|工程師文化

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章