微信搜索關注「水滴與銀彈」公衆號,第一時間獲取優質技術乾貨。7年資深後端研發,給你呈現不同的技術視角。程序員
你們好,我是 Kaito。算法
這篇文章我想和你聊一聊 Redis 的架構演化之路。數據庫
現現在 Redis 變得愈來愈流行,幾乎在不少項目中都要被用到,不知道你在使用 Redis 時,有沒有思考過,Redis 究竟是如何穩定、高性能地提供服務的?後端
你也能夠嘗試回答一下如下這些問題:緩存
若是你對 Redis 已經有些瞭解,確定也據說過數據持久化、主從複製、哨兵這些概念,它們之間又有什麼區別和聯繫呢?微信
若是你存在這樣的疑惑,這篇文章,我會從 0 到 1,再從 1 到 N,帶你一步步構建出一個穩定、高性能的 Redis 集羣。markdown
在這個過程當中,你能夠了解到 Redis 爲了作到穩定、高性能,都採起了哪些優化方案,以及爲何要這麼作?網絡
掌握了這些原理,這樣平時你在使用 Redis 時,就可以作到「遊刃有餘」。架構
這篇文章乾貨不少,但願你能夠耐心讀完。負載均衡
首先,咱們從最簡單的場景開始。
假設如今你有一個業務應用,須要引入 Redis 來提升應用的性能,此時你能夠選擇部署一個單機版的 Redis 來使用,就像這樣:
這個架構很是簡單,你的業務應用能夠把 Redis 當作緩存來使用,從 MySQL 中查詢數據,而後寫入到 Redis 中,以後業務應用再從 Redis 中讀取這些數據,因爲 Redis 的數據都存儲在內存中,因此這個速度飛快。
若是你的業務體量並不大,那這樣的架構模型基本能夠知足你的需求。是否是很簡單?
隨着時間的推移,你的業務體量逐漸發展起來了,Redis 中存儲的數據也愈來愈多,此時你的業務應用對 Redis 的依賴也愈來愈重。
可是,忽然有一天,你的 Redis 由於某些緣由宕機了,這時你的全部業務流量,都會打到後端 MySQL 上,這會致使你的 MySQL 壓力劇增,嚴重的話甚至會壓垮 MySQL。
這時你應該怎麼辦?
我猜你的方案確定是,趕忙重啓 Redis,讓它能夠繼續提供服務。
可是,由於以前 Redis 中的數據都在內存中,儘管你如今把 Redis 重啓了,以前的數據也都丟失了。重啓後的 Redis 雖然能夠正常工做,可是因爲 Redis 中沒有任何數據,業務流量仍是都會打到後端 MySQL 上,MySQL 的壓力仍是很大。
這可怎麼辦?你陷入了沉思。
有沒有什麼好的辦法解決這個問題?
既然 Redis 只把數據存儲在內存中,那是否能夠把這些數據也寫一份到磁盤上呢?
若是採用這種方式,當 Redis 重啓時,咱們把磁盤中的數據快速恢復到內存中,這樣它就能夠繼續正常提供服務了。
是的,這是一個很好的解決方案,這個把內存數據寫到磁盤上的過程,就是「數據持久化」。
如今,你設想的 Redis 數據持久化是這樣的:
可是,數據持久化具體應該怎麼作呢?
我猜你最容易想到的一個方案是,Redis 每一次執行寫操做,除了寫內存以外,同時也寫一份到磁盤上,就像這樣:
沒錯,這是最簡單直接的方案。
但仔細想一下,這個方案有個問題:客戶端的每次寫操做,既須要寫內存,又須要寫磁盤,而寫磁盤的耗時相比於寫內存來講,確定要慢不少!這勢必會影響到 Redis 的性能。
如何規避這個問題?
咱們能夠這樣優化:Redis 寫內存由主線程來作,寫內存完成後就給客戶端返回結果,而後 Redis 用另外一個線程去寫磁盤,這樣就能夠避免主線程寫磁盤對性能的影響。
這確實是一個好方案。除此以外,咱們能夠換個角度,思考一下還有什麼方式能夠持久化數據?
這時你就要結合 Redis 的使用場景來考慮了。
回憶一下,咱們在使用 Redis 時,一般把它用做什麼場景?
是的,緩存。
把 Redis 當作緩存來用,意味着儘管 Redis 中沒有保存全量數據,對於不在緩存中的數據,咱們的業務應用依舊能夠經過查詢後端數據庫獲得結果,只不過查詢後端數據的速度會慢一點而已,但對業務結果實際上是沒有影響的。
基於這個特色,咱們的 Redis 數據持久化還能夠用「數據快照」的方式來作。
那什麼是數據快照呢?
簡單來說,你能夠這麼理解:
也就是說,Redis 的數據快照,是記錄某一時刻下 Redis 中的數據,而後只須要把這個數據快照寫到磁盤上就能夠了。
它的優點在於,只在須要持久化時,把數據「一次性」寫入磁盤,其它時間都不須要操做磁盤。
基於這個方案,咱們能夠定時給 Redis 作數據快照,把數據持久化到磁盤上。
其實,上面說的這些持久化方案,就是 Redis 的「RDB」和「AOF」:
它們的區別除了上面講到的,還有如下特色:
若是讓你來選擇持久化方案,你能夠這樣選擇:
假設你的業務對 Redis 數據完整性要求比較高,選擇了 AOF 方案,那此時你又會遇到這些問題:
這怎麼辦?數據完整性要求變高了,恢復數據也變困難了?有沒有什麼方法,能夠縮小文件體積?提高恢復速度呢?
咱們繼續來分析 AOF 的特色。
因爲 AOF 文件中記錄的都是每一次寫操做,但對於同一個 key 可能會發生屢次修改,咱們只保留最後一次被修改的值,是否是也能夠?
是的,這就是咱們常常聽到的「AOF rewrite」,你也能夠把它理解爲 AOF 「瘦身」。
咱們能夠對 AOF 文件定時 rewrite,避免這個文件體積持續膨脹,這樣在恢復時就能夠縮短恢復時間了。
再進一步思考一下,還有沒有辦法繼續縮小 AOF 文件?
回顧一下咱們前面講到的,RDB 和 AOF 各自的特色:
咱們能否利用它們各自的優點呢?
固然能夠,這就是 Redis 的「混合持久化」。
具體來講,當 AOF rewrite 時,Redis 先以 RDB 格式在 AOF 文件中寫入一個數據快照,再把在這期間產生的每個寫命令,追加到 AOF 文件中。由於 RDB 是二進制壓縮寫入的,這樣 AOF 文件體積就變得更小了。
此時,你在使用 AOF 文件恢復數據時,這個恢復時間就會更短了!
Redis 4.0 以上版本才支持混合持久化。
這麼一番優化,你的 Redis 不再用擔憂實例宕機了,當發生宕機時,你就能夠用持久化文件快速恢復 Redis 中的數據。
但這樣就沒問題了嗎?
仔細想一下,雖然咱們已經把持久化的文件優化到最小了,但在恢復數據時依舊是須要時間的,在這期間你的業務應用仍是會受到影響,這怎麼辦?
咱們來分析有沒有更好的方案。
一個實例宕機,只能用恢復數據來解決,那咱們是否能夠部署多個 Redis 實例,而後讓這些實例數據保持實時同步,這樣當一個實例宕機時,咱們在剩下的實例中選擇一個繼續提供服務就行了。
沒錯,這個方案就是接下來要講的「主從複製:多副本」。
此時,你能夠部署多個 Redis 實例,架構模型就變成了這樣:
咱們這裏把實時讀寫的節點叫作 master,另外一個實時同步數據的節點叫作 slave。
採用多副本的方案,它的優點是:
這個方案不錯,不只節省了數據恢復的時間,還能提高性能,那它有什麼問題嗎?
你能夠思考一下。
其實,它的問題在於:當 master 宕機時,咱們須要「手動」把 slave 提高爲 master,這個過程也是須要花費時間的。
雖然比恢復數據要快得多,但仍是須要人工介入處理。一旦須要人工介入,就必需要算上人的反應時間、操做時間,因此,在這期間你的業務應用依舊會受到影響。
怎麼解決這個問題?咱們是否能夠把這個切換的過程,變成自動化呢?
對於這種狀況,咱們須要一個「故障自動切換」機制,這就是咱們常常聽到的「哨兵」所具有的能力。
如今,咱們能夠引入一個「觀察者」,讓這個觀察者去實時監測 master 的健康狀態,這個觀察者就是「哨兵」。
具體如何作?
有了這個方案,就不須要人去介入處理了,一切就變得自動化了,是否是很爽?
但這裏還有一個問題,若是 master 狀態正常,但這個哨兵在詢問 master 時,它們之間的網絡發生了問題,那這個哨兵可能會誤判。
這個問題怎麼解決?
答案是,咱們能夠部署多個哨兵,讓它們分佈在不一樣的機器上,它們一塊兒監測 master 的狀態,流程就變成了這樣:
因此,咱們用多個哨兵互相協商來斷定 master 的狀態,這樣一來,就能夠大大下降誤判的機率。
哨兵協商斷定 master 異常後,這裏還有一個問題:由哪一個哨兵來發起主從切換呢?
答案是,選出一個哨兵「領導者」,由這個領導者進行主從切換。
問題又來了,這個領導者怎麼選?
想象一下,在現實生活中,選舉是怎麼作的?
是的,投票。
在選舉哨兵領導者時,咱們能夠制定這樣一個選舉規則:
其實,這個選舉的過程就是咱們常常聽到的:分佈式系統領域中的「共識算法」。
什麼是共識算法?
咱們在多個機器部署哨兵,它們須要共同協做完成一項任務,因此它們就組成了一個「分佈式系統」。
在分佈式系統領域,多個節點如何就一個問題達成共識的算法,就叫共識算法。
在這個場景下,多個哨兵共同協商,選舉出一個都承認的領導者,就是使用共識算法完成的。
這個算法還規定節點的數量必須是奇數個,這樣能夠保證系統中即便有節點發生了故障,剩餘超過「半數」的節點狀態正常,依舊能夠提供正確的結果,也就是說,這個算法還兼容了存在故障節點的狀況。
共識算法在分佈式系統領域有不少,例如 Paxos、Raft,哨兵選舉領導者這個場景,使用的是 Raft 共識算法,由於它足夠簡單,且易於實現。
如今,咱們用多個哨兵共同監測 Redis 的狀態,這樣一來,就能夠避免誤判的問題了,架構模型就變成了這樣:
好了,到這裏咱們先小結一下。
你的 Redis 從最簡單的單機版,通過數據持久化、主從多副本、哨兵集羣,這一路優化下來,你的 Redis 無論是性能仍是穩定性,都愈來愈高,就算節點發生故障,也不用擔憂了。
你的 Redis 以這樣的架構模式部署,基本上就能夠穩定運行很長時間了。
...
隨着時間的發展,你的業務體量開始迎來了爆炸性增加,此時你的架構模型,還可以承擔這麼大的流量嗎?
咱們一塊兒來分析一下:
看到了麼,當你的寫請求量愈來愈大時,一個 master 實例可能就沒法承擔這麼大的寫流量了。
要想完美解決這個問題,此時你就須要考慮使用「分片集羣」了。
什麼是「分片集羣」?
簡單來說,一個實例扛不住寫壓力,那咱們是否能夠部署多個實例,而後把這些實例按照必定規則組織起來,把它們當成一個總體,對外提供服務,這樣不就能夠解決集中寫一個實例的瓶頸問題嗎?
因此,如今的架構模型就變成了這樣:
如今問題又來了,這麼多實例如何組織呢?
咱們制定規則以下:
而分片集羣根據路由規則所在位置的不一樣,還能夠分爲兩大類:
客戶端分片指的是,key 的路由規則放在客戶端來作,就是下面這樣:
這個方案的缺點是,客戶端須要維護這個路由規則,也就是說,你須要把路由規則寫到你的業務代碼中。
如何作到不把路由規則耦合在業務代碼中呢?
你能夠這樣優化,把這個路由規則封裝成一個模塊,當須要使用時,集成這個模塊就能夠了。
這就是 Redis Cluster 的採用的方案。
Redis Cluster 內置了哨兵邏輯,無需再部署哨兵。
當你使用 Redis Cluster 時,你的業務應用須要使用配套的 Redis SDK,這個 SDK 內就集成好了路由規則,不須要你本身編寫了。
再來看服務端分片。
這種方案指的是,路由規則不放在客戶端來作,而是在客戶端和服務端之間增長一個「中間代理層」,這個代理就是咱們常常聽到的 Proxy。
而數據的路由規則,就放在這個 Proxy 層來維護。
這樣一來,你就無需關心服務端有多少個 Redis 節點了,只須要和這個 Proxy 交互便可。
Proxy 會把你的請求根據路由規則,轉發到對應的 Redis 節點上,並且,當集羣實例不足以支撐更大的流量請求時,還能夠橫向擴容,添加新的 Redis 實例提高性能,這一切對於你的客戶端來講,都是透明無感知的。
業界開源的 Redis 分片集羣方案,例如 Twemproxy、Codis 就是採用的這種方案。
分片集羣在數據擴容時,還涉及到了不少細節,這塊內容不是本文章重點,因此暫不詳述。
至此,當你使用分片集羣后,對於將來更大的流量壓力,均可以從容面對了!
好了,咱們來總結一下,咱們是如何一步步構建一個穩定、高性能的 Redis 集羣的。
首先,在使用最簡單的單機版 Redis 時,咱們發現當 Redis 故障宕機後,數據沒法恢復的問題,所以咱們想到了「數據持久化」,把內存中的數據也持久化到磁盤上一份,這樣 Redis 重啓後就能夠從磁盤上快速恢復數據。
在進行數據持久化時,咱們又面臨如何更高效地將數據持久化到磁盤的問題。以後咱們發現 Redis 提供了 RDB 和 AOF 兩種方案,分別對應了數據快照和實時的命令記錄。當咱們對數據完整性要求不高時,能夠選擇 RDB 持久化方案。若是對於數據完整性要求較高,那麼能夠選擇 AOF 持久化方案。
可是咱們又發現,AOF 文件體積會隨着時間增加變得愈來愈大,此時咱們想到的優化方案是,使用 AOF rewrite 的方式對其進行瘦身,減少文件體積,再後來,咱們發現能夠結合 RDB 和 AOF 各自的優點,在 AOF rewrite 時使用二者結合的「混合持久化」方式,又進一步減少了 AOF 文件體積。
以後,咱們發現儘管能夠經過數據恢復的方式還原數據,但恢復數據也是須要花費時間的,這意味着業務應用仍是會受到影響。咱們進一步優化,採用「多副本」的方案,讓多個實例保持實時同步,當一個實例故障時,能夠手動把其它實例提高上來繼續提供服務。
可是這樣也有問題,手動提高實例上來,須要人工介入,人工介入操做也須要時間,咱們開始想辦法把這個流程變得自動化,因此咱們又引入了「哨兵」集羣,哨兵集羣經過互相協商的方式,發現故障節點,並能夠自動完成切換,這樣就大幅下降了對業務應用的影響。
最後,咱們把關注點聚焦在如何支撐更大的寫流量上,因此,咱們又引入了「分片集羣」來解決這個問題,讓多個 Redis 實例分攤寫壓力,將來面對更大的流量,咱們還能夠添加新的實例,橫向擴展,進一步提高集羣的性能。
至此,咱們的 Redis 集羣才得以長期穩定、高性能的爲咱們的業務提供服務。
這裏我畫了一個思惟導圖,方便你更好地去理解它們之間的關係,以及演化的過程。
看到這裏,我想你對如何構建一個穩定、高性能的 Redis 集羣問題時,應該會有本身的看法了。
其實,這篇文章所講的優化思路,圍繞的主題就是「架構設計」的核心思想:
當咱們講到哨兵集羣、分片集羣時,這還涉及到了「分佈式系統」相關的知識:
固然,除了 Redis 以外,對於構建任何一個數據集羣,你均可以沿用這個思路去思考、去優化,看看它們究竟是如何作的。
例如當你在使用 MySQL 時,你能夠思考一下 MySQL 與 Redis 有哪些不一樣?MySQL 爲了作到高性能、高可用,又是如何作的?其實思路都是相似的。
咱們如今處處可見分佈式系統、數據集羣,我但願經過這篇文章,你能夠理解這些軟件是如何一步步演化過來的,在演化過程當中,它們遇到了哪些問題,爲了解決這些問題,這些軟件的設計者設計了怎樣的方案,作了哪些取捨?
你只有瞭解了其中的原理,掌握了分析問題、解決問題的能力,這樣在之後的開發過程當中,或是學習其它優秀軟件時,就能快速地找到「重點」,在最短的時間掌握它,並能在實際應用中發揮它們的優點。
其實這個思考過程,也是作「架構設計」的思路。在作軟件架構設計時,你面臨的場景就是發現問題、分析問題、解決問題,一步步去演化、升級你的架構,最後在性能、可靠性方面達到一個平衡。雖然各類軟件層出不窮,但架構設計的思想不會變,我但願你真正吸取的是這些思想,這樣才能夠作到以不變應萬變。
想看更多硬核技術文章?歡迎關注個人公衆號「水滴與銀彈」。
我是 Kaito,是一個對於技術有思考的資深後端程序員,在個人文章中,我不只會告訴你一個技術點是什麼,還會告訴你爲何這麼作?我還會嘗試把這些思考過程,提煉成通用的方法論,讓你能夠應用在其它領域中,作到觸類旁通。