圖解 Redis

什麼是 Redis?

Redis(REmote DIctionary Service)是一個開源的鍵值對數據庫服務器。

Redis 更準確的描述是一個數據結構服務器。Redis 的這種特殊性質讓它在開發人員中很受歡迎。

Redis 不是通過迭代或者排序方式處理數據,而是一開始就按照數據結構方式組織。早期,它的使用很像 Memcached,但隨着 Redis 的改進,它在許多其他用例中變得可行,包括髮布 - 訂閱機制、流(streaming)和隊列。

主要來說,Redis 是一個內存數據庫,用作另一個 “真實” 數據庫(如 MySQL 或 PostgreSQL)前面的緩存,以幫助提高應用程序性能。它通過利用內存的高速訪問速度,從而減輕核心應用程序數據庫的負載,例如:

上述數據的示例可以包括會話或數據緩存以及儀表板的排行榜或彙總分析。

但是,對於許多用例場景,Redis 都可以提供足夠的保證,可以將其用作成熟的主數據庫。再加上 Redis 插件及其各種高可用性(HA)設置,Redis 作爲數據庫對於某些場景和工作負載變得非常有用。

另一個重要方面是 Redis 模糊了緩存和數據存儲之間的界限。這裏要理解的重要一點是,相比於使用 SSD 或 HDD 作爲存儲的傳統數據庫,讀取和操作內存中數據的速度要快得多。

最初,Redis 最常被比作 Memcached,後者當時缺乏任何非易失性持久化。

這是當前兩個緩存之間的功能細分。

IUNxhW

雖然現在擁有多種配置方式將數據持久化到磁盤,但當時首次引入持久化時,Redis 是使用快照方式,通過異步拷貝內存中的數據方式來做持久化。不幸的是,這種機制的缺點是可能會在快照之間丟失數據。

Redis 自 2009 年成立到現在已經變的很成熟。我們將介紹它的大部分架構和拓撲,以便你可以將 Redis 添加到你的數據存儲系統庫中。

Redis 架構

在開始討論 Redis 內部結構之前,讓我們先討論一下各種 Redis 部署及其權衡取捨。

我們將主要關注以下這些設置:

根據你的用例和規模,決定使用哪一種設置。

單個 Redis 實例

單個 Redis 實例是最直接的 Redis 部署方式。它允許用戶設置和運行小型實例,從而幫助他們快速發展和加速服務。但是,這種部署並非沒有缺點。例如,如果此實例失敗或不可用,則所有客戶端對 Redis 的調用都將失敗,從而降低系統的整體性能和速度。

如果有足夠的內存和服務器資源,這個實例可以很強大。主要用於緩存的場景可能會以最少的設置獲得顯著的性能提升。給定足夠的系統資源,你可以在應用程序運行的同一機器上部署此 Redis 服務。

在管理系統內的數據方面,瞭解一些 Redis 概念是必不可少的。發送到 Redis 的命令首先在內存中處理。然後,如果在這些實例上設置了持久性,則在某個時間間隔上會有一個 fork 進程,來生成數據持久化 RDB(Redis 數據的非常緊湊的時間點表示)快照或 AOF(僅附加文件)。

這兩個流程可以讓 Redis 擁有長期存儲,支持各種複製策略,並啓用更復雜的拓撲。如果 Redis 未設置爲持久化數據,則在重新啓動或故障轉移時數據會丟失。如果在重啓時啓用了持久化,它會將 RDB 快照或 AOF 中的所有數據加載回內存,然後實例可以支持新的客戶端請求。

話雖如此,讓我們看看你可能會用到的更多分佈式 Redis 設置。

Redis 高可用性

Redis 的另一個流行設置是主從部署方式,從部署保持與主部署之間數據同步。當數據寫入主實例時,它會將這些命令的副本發送到從部署客戶端輸出緩衝區,從而達到數據同步的效果。從部署可以有一個或多個實例。這些實例可以幫助擴展 Redis 的讀取操作或提供故障轉移,以防 main 丟失。

我們現在已經進入了一個分佈式系統,因此需要在此拓撲中考慮許多新事物。以前簡單的事情現在變得複雜了。

Redis 複製

Redis 的每個主實例都有一個複製 ID 和一個偏移量。這兩條數據對於確定副本可以繼續其複製過程的時間點或確定它是否需要進行完整同步至關重要。對於主 Redis 部署上發生的每個操作,此偏移量都會增加。

更明確地說,當 Redis 副本實例僅落後於主實例幾個偏移量時,它會從主實例接收剩餘的命令,然後在其數據集上重放,直到同步完成。如果兩個實例無法就複製 ID 達成一致,或者主實例不知道偏移量,則副本將請求全量同步。這時主實例會創建一個新的 RDB 快照並將其發送到副本。

在此傳輸之間,主實例會緩衝快照截止和當前偏移之間的所有中間更新指令,這樣在快照同步完後,再將這些指令發送到副本實例。這樣完成後,複製就可以正常繼續。

如果一個實例具有相同的複製 ID 和偏移量,則它們具有完全相同的數據。現在你可能想知道爲什麼需要複製 ID。當 Redis 實例被提升爲主實例或作爲主實例從頭開始重新啓動時,它會被賦予一個新的複製 ID。

這用於推斷此新提升的副本實例是從先前哪個主實例複製出來的。這允許它能夠執行部分同步(與其他副本節點),因爲新的主實例會記住其舊的複製 ID。

例如,兩個實例(主實例和從實例)具有相同的複製 ID,但偏移量相差幾百個命令,這意味着如果在實例上重放這些偏移量後面的命令,它們將具有相同的數據集。現在,如果複製 ID 完全不同,並且我們不知道新降級(或重新加入)從節點的先前複製 ID(沒有共同祖先)。我們將需要執行昂貴的全量同步。

相反,如果我們知道以前的複製 ID,我們就可以推斷如何使數據同步,因爲我們能夠推斷出它們共享的共同祖先,並且偏移量對於部分同步再次有意義。

Redis 哨兵(Sentinel)

Sentinel 是一個分佈式系統。與所有分佈式系統一樣,Sentinel 有幾個優點和缺點。Sentinel 的設計方式是,一組哨兵進程協同工作以協調狀態,從而爲 Redis 提供高可用性。畢竟,你不希望保護你免受故障影響的系統有自己的單點故障。

Sentinel 負責一些事情。首先,它確保當前的主實例和從實例正常運行並做出響應。這是必要的,因爲哨兵(與其他哨兵進程)可以在主節點和 / 或從節點丟失的情況下發出警報並採取行動。其次,它在服務發現中發揮作用,就像其他系統中的 Zookeeper 和 Consul 一樣。所以當一個新的客戶端嘗試向 Redis 寫東西時,Sentinel 會告訴客戶端當前的主實例是什麼。

因此,哨兵不斷監控可用性並將該信息發送給客戶端,以便他們能夠在他們確實進行故障轉移時對其做出反應。

以下是它的職責:

以這種方式使用 Redis Sentinel 可以進行故障檢測。此檢測涉及多個哨兵進程同意當前主實例不再可用。這個協議過程稱爲 Quorum。這可以提高魯棒性並防止一臺機器行爲異常導致無法訪問主 Redis 節點。

此設置並非沒有缺點,因此我們將在使用 Redis Sentinel 時介紹一些建議和最佳實踐。

你可以通過多種方式部署 Redis Sentinel。老實說,要提出任何明智的建議,我需要有關你的系統的更多背景信息。作爲一般指導,我建議在每個應用程序服務器旁邊運行一個哨兵節點(如果可能的話),這樣你也不需要考慮哨兵節點和實際使用 Redis 的客戶端之間的網絡可達性差異。

你可以將 Sentinel 與 Redis 實例一起運行,甚至可以在獨立節點上運行,只不過它會按照別的方式處理,從而會讓事情變得更復雜。我建議至少運行三個節點,並且至少具有兩個法定人數(quorum)。這是一個簡單的圖表,分解了集羣中的服務器數量以及相關的法定人數和可容忍的可持續故障。

XbYB3i

這會因系統而異,但總體思路是不變的。

讓我們花點時間思考一下這樣的設置會出現什麼問題。如果你運行這個系統足夠長的時間,你會遇到所有這些。

  1. 如果哨兵節點超出法定人數怎麼辦?

  2. 如果網絡分裂將舊的主實例置於少數羣體中怎麼辦?這些寫入會發生什麼?(劇透:當系統完全恢復時它們會丟失)

  3. 如果哨兵節點和客戶端節點(應用程序節點)的網絡拓撲錯位會發生什麼?

沒有持久性保證,特別是持久化到磁盤的操作(見下文)是異步的。還有一個麻煩的問題,當客戶發現新的 primary 時,我們失去了多少寫給一個不知道的 primary?Redis 建議在建立新連接時查詢新的主節點。根據系統配置,這可能意味着大量數據丟失。

如果你強制主實例將寫入複製到至少一個副本實例,有幾種方法可以減輕損失程度。請記住,所有 Redis 複製都是異步的,這是有其權衡的考慮。因此,它需要獨立跟蹤確認,如果至少有一個副本實例沒有確認它們,主實例將停止接受寫入。

Redis 集羣

我相信很多人都想過當你無法將所有數據存儲在一臺機器上的內存中時會發生什麼。目前,單個服務器中可用的最大 RAM 爲 24TIB,這是目前 AWS 線上列出來的。當然,這很多,但對於某些系統來說,這還不夠,即使對於緩存層也是如此。

Redis Cluster 允許 Redis 的水平擴展。

首先,讓我們擺脫一些術語約束;一旦我們決定使用 Redis 集羣,我們就決定將我們存儲的數據分散到多臺機器上,這稱爲分片。所以集羣中的每個 Redis 實例都被認爲是整個數據的一個分片。

這帶來了一個新的問題。如果我們向集羣推送一個 key,我們如何知道哪個 Redis 實例(分片)保存了該數據?有幾種方法可以做到這一點,但 Redis Cluster 使用算法分片。

爲了找到給定 key 的分片,我們對 key 進行哈希處理,並通過對總分片數量取模。然後,使用確定性哈希函數,這意味着給定的 key 將始終映射到同一個分片,我們可以推斷將來讀取特定 key 的位置。

當我們之後想在系統中添加一個新的分片時會發生什麼?這個過程稱爲重新分片。

假設鍵'foo' 之前映射到分片 0, 在引入新分片後它可能會映射到分片 5。但是,如果我們需要快速擴展系統,移動數據來達到新的分片映射,這將是緩慢且不切實際的。它還對 Redis 集羣的可用性產生不利影響。

Redis Cluster 爲這個問題設計了一種解決方案,稱爲 Hashslot,所有數據都映射到它。有 16K 哈希槽。這爲我們提供了一種在集羣中傳播數據的合理方式,當我們添加新的分片時,我們只需在系統之間移動哈希槽。通過這樣做,我們只需要將 hashlot 從一個分片移動到另一個分片,並簡化將新的主實例添加到集羣中的過程。

這可以在沒有任何停機時間和最小的性能影響的情況下實現。讓我們通過一個例子來談談。

因此,爲了映射 “foo”,我們採用一個確定性的鍵(foo)散列,並通過散列槽的數量(16K)對其進行修改,從而得到 M2 的映射。現在假設我們添加了一個新實例 M3。新的映射將是:

現在映射到 M2 的 M1 中映射哈希槽的所有鍵都需要移動。但是散列槽的各個鍵的散列不需要移動,因爲它們已經被劃分到散列槽中。因此,這一級別的誤導(misdirection)解決了算法分片的重新分片問題。

Gossiping 協議

Redis Cluster 使用 gossiping 來確定整個集羣的健康狀況。在上圖中,我們有 3 個 M 個節點和 3 個 S 節點。所有這些節點不斷地進行通信以瞭解哪些分片可用並準備好爲請求提供服務。

如果足夠多的分片同意 M1 沒有響應,他們可以決定將 M1 的副本 S1 提升爲主節點以保持集羣健康。觸發此操作所需的節點數量是可配置的,並且必須正確執行此操作。如果操作不當並且在分區的兩邊相等時無法打破平局,則可能會導致集羣被拆分。這種現象稱爲裂腦。作爲一般規則,必須擁有奇數個主節點和兩個副本,以實現最穩健的設置。

Redis 持久化模型

如果我們要使用 Redis 存儲任何類型的數據同時要求安全保存,瞭解 Redis 是如何做到這一點很重要。在許多用例中,如果你丟失了 Redis 存儲的數據,這並不是世界末日。將其用作緩存或在其支持實時分析的情況下,如果發生數據丟失,則並非世界末日。

在其他場景中,我們希望圍繞數據持久性和恢復有一些保證。

無持久化

無持久化:如果你願意,可以完全禁用持久化。這是運行 Redis 的最快方式,並且沒有持久性保證。

RDB 文件

RDB(Redis 數據庫):RDB 持久化以指定的時間間隔執行數據集的時間點快照。

這種機制的主要缺點是快照之間的數據會丟失。此外,這種存儲機制還依賴於主進程的 fork,在更大的數據集中,這可能會導致服務請求的瞬間延遲。話雖如此,RDB 文件在內存中的加載速度要比 AOF 快得多。

AOF

AOF(Append Only File):AOF 持久化記錄服務器接收到的每個寫入操作,這些操作將在服務器啓動時再次被執行,重建原始數據集。

這種持久性的方法能夠確保比 RDB 快照更持久,因爲它是一個僅附加文件。隨着操作的發生,我們將它們緩衝到日誌中,但它們還沒有被持久化。該日誌與我們運行的實際命令一致,以便在需要時進行重放。

然後,如果可能,我們使用 fsync 將其刷新到磁盤(當此運行可配置時),它將被持久化。缺點是格式不緊湊,並且比 RDB 文件使用更多的磁盤。

爲什麼不兼得?

RDB + AOF:可以將 AOF 和 RDB 組合在同一個 Redis 實例中。如果你願意的話,可以以速度換取持久化是一種折衷方法。我認爲這是設置 Redis 的一種可接受的方式。在重啓的情況下,請記住如果兩者都啓用,Redis 將使用 AOF 來重建數據,因爲它是最完整的。

Forking

現在我們瞭解了持久化的類型,讓我們討論一下我們如何在像 Redis 這樣的單線程應用程序中實際執行它。

在我看來,Redis 最酷的部分是它如何利用 forking 和寫時複製來高效地促進數據持久化。

Forking 是操作系統通過創建自身副本來創建新進程的一種方式。這樣,你將獲得一個新的進程 ID 和一些其他信息和句柄,因此新 forking 的進程(子進程)可以與原始進程父進程通信。

現在事情變得有趣了。Redis 是一個分配了大量內存的進程,那麼它如何在不耗盡內存的情況下進行復制呢?

當你 fork 一個進程時,父進程和子進程共享內存,並且在該子進程中 Redis 開始快照(Redis)進程。這是通過一種稱爲寫時複製的內存共享技術實現的——該技術在創建分叉時傳遞對內存的引用。如果在子進程持久化到磁盤時沒有發生任何更改,則不會進行新的分配。

在發生更改的情況下,內核會跟蹤對每個頁面的引用,如果某個頁面有多個更改,則將更改寫入新頁面。子進程完全不知道更改以及具有一致的內存快照的事情。因此,在只使用了一小部分內存的情況下,我們能夠非常快速有效地獲得潛在千兆字節內存的時間點快照!

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