07丨切片集群:数据增多了,是该加内存还是加实例
1. RDB持久化造成Redis緩慢
??在使用 RDB 進行持久化時,Redis 會 fork 子進程來完成,fork 操作的用時和 Redis 的數據量是正相關的,而 fork 在執行時會阻塞主線程。數據量越大,fork 操作造成的主線程阻塞的時間越長。所以,在使用 RDB 對 25GB 的數據進行持久化時,數據量較大,后臺運行的子進程在 fork 創建時阻塞了主線程,于是就導致Redis 響應變慢了。
??切片集群,也叫分片集群,就是指啟動多個 Redis 實例組成一個集群,然后按照一定的規則,把收到的數據劃分成多份,每一份用一個實例來保存。回到我們剛剛的場景中,如果把 25GB 的數據平均分成 5 份(當然,也可以不做均分),使用 5 個實例來保存,每個實例只需要保存 5GB 數據。如下圖所示:
??那么,在切片集群中,實例在為 5GB 數據生成 RDB 時,數據量就小了很多,fork 子進程一般不會給主線程帶來較長時間的阻塞。采用多個實例保存數據切片后,我們既能保存25GB 數據,又避免了 fork 子進程阻塞主線程而導致的響應突然變慢。
??在剛剛的案例里,為了保存大量數據,我們使用了大內存云主機和切片集群兩種方法。實際上,這兩種方法分別對應著 Redis 應對數據量增多的兩種方案:縱向擴展(scale up)和橫向擴展(scale out)。
- 縱向擴展:升級單個 Redis 實例的資源配置,包括增加內存容量、增加磁盤容量、使用更高配置的 CPU。就像下圖中,原來的實例內存是 8GB,硬盤是 50GB,縱向擴展后,內存增加到 24GB,磁盤增加到 150GB。
- 橫向擴展:橫向增加當前 Redis 實例的個數,就像下圖中,原來使用 1 個 8GB 內存、50GB 磁盤的實例,現在使用三個相同配置的實例。
??那么,這兩種方式的優缺點分別是什么呢?
??首先,縱向擴展的好處是,實施起來簡單、直接。不過,這個方案也面臨兩個潛在的問題。
??第一個問題是,當使用 RDB 對數據進行持久化時,如果數據量增加,需要的內存也會增加,主線程 fork 子進程時就可能會阻塞(比如剛剛的例子中的情況)。不過,如果你不要求持久化保存 Redis 數據,那么,縱向擴展會是一個不錯的選擇。
??你還要面對第二個問題:縱向擴展會受到硬件和成本的限制。這很容易理解,畢竟,把內存從 32GB 擴展到 64GB 還算容易,但是,要想擴充到 1TB,就會面臨硬件容量和成本上的限制了。
??與縱向擴展相比,橫向擴展是一個擴展性更好的方案。這是因為,要想保存更多的數據,采用這種方案的話,只用增加 Redis 的實例個數就行了,不用擔心單個實例的硬件和成本限制。在面向百萬、千萬級別的用戶規模時,橫向擴展的 Redis 切片集群會是一個非常好的選擇。
??不過,在只使用單個實例的時候,數據存在哪兒,客戶端訪問哪兒,都是非常明確的,但是,切片集群不可避免地涉及到多個實例的分布式管理問題。要想把切片集群用起來,我們就需要解決兩大問題:
- 數據切片后,在多個實例之間如何分布?
- 客戶端怎么確定想要訪問的數據在哪個實例上?
2.數據切片和實例的對應分布關系
??Redis Cluster 方案采用哈希槽(Hash Slot,接下來我會直接稱之為 Slot),來處理數據和實例之間的映射關系。在 Redis Cluster 方案中,一個切片集群共有 16384個哈希槽,這些哈希槽類似于數據分區,每個鍵值對都會根據它的 key,被映射到一個哈希槽中。
??具體的映射過程分為兩大步:首先根據鍵值對的 key,按照CRC16 算法計算一個 16 bit的值;然后,再用這個 16bit 值對 16384 取模,得到 0~16383 范圍內的模數,每個模數代表一個相應編號的哈希槽。
我們在部署 Redis Cluster 方案時,
??可以使用 cluster create 命令創建集群,此時,Redis會自動把這些槽平均分布在集群實例上。例如,如果集群中有 N 個實例,那么,每個實例上的槽個數為 16384/N 個。當然,我們也可以使用 cluster meet 命令手動建立實例間的連接,形成集群,再使用cluster addslots 命令,指定每個實例上的哈希槽個數。
??示意圖中的切片集群一共有 3 個實例,同時假設有 5 個哈希槽,我們首先可以通過下面的命令手動分配哈希槽:實例 1 保存哈希槽 0 和 1,實例 2 保存哈希槽 2 和 3,實例 3 保存哈希槽 4。
??在集群運行的過程中,key1 和 key2 計算完 CRC16 值后,對哈希槽總個數 5 取模,再根據各自的模數結果,就可以被映射到對應的實例 1 和實例 3 上了。另外,我再給你一個小提醒,在手動分配哈希槽時,需要把 16384 個槽都分配完,否則Redis 集群無法正常工作。
客戶端如何定位數據
??在定位鍵值對數據時,它所處的哈希槽是可以通過計算得到的,這個計算可以在客戶端發送請求時來執行。客戶端和集群實例建立連接后,實例就會把哈希槽的分配信息發給客戶端。Redis 實例會把自己的哈希槽信息發給和它相連接的其它實例,來完成哈希槽分配信息的擴散。當實例之間相互連接后,每個實例就有所有哈希槽的映射關系了。
??客戶端收到哈希槽信息后,會把哈希槽信息緩存在本地。當客戶端請求鍵值對時,會先計算鍵所對應的哈希槽,然后就可以給相應的實例發送請求了。
??在集群中,實例和哈希槽的對應關系并不是一成不變的,最常見的變化有兩個:
- 在集群中,實例有新增或刪除,Redis 需要重新分配哈希槽;
- 為了負載均衡,Redis 需要把哈希槽在所有實例上重新分布一遍。
??此時,實例之間還可以通過相互傳遞消息,獲得最新的哈希槽分配信息,但是,客戶端是無法主動感知這些變化的。這就會導致,它緩存的分配信息和最新的分配信息就不一致。
??Redis Cluster 方案提供了一種重定向機制,所謂的“重定向”,就是指,客戶端給一個實例發送數據讀寫操作時,這個實例上并沒有相應的數據,客戶端要再給一個新實例發送操作命令。那客戶端又是怎么知道重定向時的新實例的訪問地址呢?當客戶端把一個鍵值對的操作請求發給一個實例時,如果這個實例上并沒有這個鍵值對映射的哈希槽,那么,這個實例就會給客戶端返回下面的 MOVED 命令響應結果,這個結果中就包含了新實例的訪問地址。
??其中,MOVED 命令表示,客戶端請求的鍵值對所在的哈槽 13320,實際是在172.16.19.5 這個實例上。通過返回的 MOVED 命令,就相當于把哈希槽所在的新實例的信息告訴給客戶端了。這樣一來,客戶端就可以直接和 172.16.19.5 連接,并發送操作請求了。
??我畫一張圖來說明一下,MOVED 重定向命令的使用方法。可以看到,由于負載均衡,Slot2 中的數據已經從實例 2 遷移到了實例 3,但是,客戶端緩存仍然記錄著“Slot 2 在實例 2”的信息,所以會給實例 2 發送命令。實例 2 給客戶端返回一條 MOVED 命令,把Slot2 的最新位置(也就是在實例 3 上),返回給客戶端,客戶端就會再次向實例 3 發送請求,同時還會更新本地緩存,把 Slot 2 與實例的對應關系更新過來。
需要注意的是,在上圖中,當客戶端給實例 2 發送命令時,Slot 2 中的數據已經全部遷移到了實例 3。在實際應用時,
如果 Slot 2 中的數據比較多,就可能會出現一種情況:客戶端向實例 2 發送請求,但此時,Slot 2 中的數據只有一部分遷移到了實例 3,還有部分數據沒有遷移。在這種遷移部分完成的情況下,客戶端就會收到一條 ASK 報錯信息,如下所示:
這個結果中的 ASK 命令就表示,客戶端請求的鍵值對所在的哈希槽 13320,在172.16.19.5 這個實例上,但是這個哈希槽正在遷移。此時,客戶端需要先給 172.16.19.5這個實例發送一個 ASKING 命令。這個命令的意思是,讓這個實例允許執行客戶端接下來發送的命令。然后,客戶端再向這個實例發送 GET 命令,以讀取數據。
和 MOVED 命令不同,ASK 命令并不會更新客戶端緩存的哈希槽分配信息。所以,在上圖中,如果客戶端再次請求 Slot 2 中的數據,它還是會給實例 2 發送請求。這也就是說,ASK 命令的作用只是讓客戶端能給新實例發送一次請求,而不像 MOVED 命令那樣,會更改本地緩存,讓后續所有命令都發往新實例。
總結
以上是生活随笔為你收集整理的07丨切片集群:数据增多了,是该加内存还是加实例的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 06 | 哨兵机制: 主库挂了, 如何不
- 下一篇: 08 | 替换策略: 缓存满了怎么办?