(八)Redis 主從複製、切片叢集

冬先生發表於2024-09-04

一、主從複製

1、主從關係

都說的 Redis 具有高可靠性,這裡有兩層含義:一是資料儘量少丟失,二是服務儘量少中斷。AOF 和 RDB 保證了前者,而對於後者,Redis 的做法就是將一份資料同時儲存在多個例項上。為了保證資料一致性,Redis 提供了主從庫模式,並採用讀寫分離的方式,如圖

2、主從複製-全量

當啟動多個 Redis 例項的時候,它們相互之間就可以透過 replicaof(Redis 5.0 之前使用 slaveof)命令形成主庫和從庫的關係。例如,讓例項 1(ip:172.16.19.3)和例項 2(ip:172.16.19.5)成為主從關係的命令:replicaof 172.16.19.3 6379,當關系建立後,第一次同步分資料為三個階段:

(1)從庫給主庫傳送 psync 命令,表示要進行資料同步,包含主庫的 runID(redis 例項啟動生成的隨機 ID) 和複製進度 offset 兩個引數,初次複製runID 為 ?offset 為 -1,主庫會用 FULLRESYNC(初次為全量複製)響應命令帶上兩個引數返回給從庫,從庫收到響應後會記錄 runID、offset 兩個引數。
(2)主庫執行 bgsave 命令,生成 RDB 檔案併發給從庫,從庫會先清空當前資料庫,然後載入 RDB 檔案。這個過程中主庫不會被阻塞,為了保證主從庫的資料一致性,主庫會在記憶體中用專門的 replication buffer,記錄 RDB 檔案生成後收到的所有寫操作。
(3)主庫完成 RDB 檔案傳送後,就會把此時 replication buffer 中的修改操作發給從庫,從庫再重新執行這些操作。

3、主從複製-級聯

全量複製中,對於主庫來說,需要完成兩個耗時的操作:生成 RDB 檔案和傳輸 RDB 檔案,如果從庫數量很多,主執行緒忙於 fork 子程序生成 RDB 檔案會阻塞主執行緒處理正常請求,從而導致主庫響應應用程式的請求速度變慢。此外,傳輸 RDB 檔案也會佔用主庫的網路頻寬,同樣會給主庫的資源使用帶來壓力。

我們可以透過“主-從-從”模式將主庫生成 RDB 和傳輸 RDB 的壓力,以級聯的方式分散到從庫上,從而降低主庫的壓力。簡單來說,我們在部署主從叢集的時候,可以手動選擇一個從庫(比如選擇記憶體資源配置較高的從庫),用於級聯其他的從庫,相當於選擇一個從庫當做其他從庫的主庫,執行replicaof 所選從庫IP 6379,建立關係。

主從複製完成後,它們之間就會一直維護一個網路連線,主庫會透過這個連線將後續陸續收到的命令操作再同步給從庫,這個過程也稱為基於長連線的命令傳播,可以避免頻繁建立連線的開銷。

4、網路問題

網路中斷後,主從庫會採用增量複製的方式把主從庫網路斷連期間主庫收到的命令同步給從庫,期間命令會寫入 replication buffer 以及 repl_backlog_buffer 緩衝區。這是一個環形緩衝區,主庫會記錄自己寫到的位置,從庫則會記錄自己已經讀到的位置。

起初,兩個位置是相同的,但隨著主庫不斷接收新的寫操作,緩衝區中的寫位置會逐步偏離起始位置,通常用偏移量來衡量這個偏移距離的大小,偏移越多,master_repl_offset 越大。

當主從庫的連線恢復,從庫首先會給主庫傳送 psync 命令把自己當前的 slave_repl_offset 發給主庫,主庫根據 master_repl_offset 和 slave_repl_offset 之間的差距,形成命令傳送給從庫進行資料同步。

需要強調的是,因為 repl_backlog_buffer 是一個環形緩衝區,所以在緩衝區寫滿後,主庫會繼續寫入,此時,就會覆蓋掉之前寫入的操作。如果從庫的讀取速度比較慢,就有可能導致從庫還未讀取的操作被主庫新寫的操作覆蓋了,這會導致主從庫間的資料不一致。

我們可以調整 repl_backlog_size 這個引數來設定緩衝空間大小。計算公式是:緩衝空間大小 = 主庫寫入命令速度 * 操作大小 - 主從庫間網路傳輸命令速度 * 操作大小。在實際應用中,我們可以擴大一定倍數應對突發的請求壓力。

如果併發請求量非常大,除了適當增加 repl_backlog_size 值,就需要考慮使用切片叢集來分擔單個主庫的請求壓力了。

二、切片叢集

在使用 RDB 持久化時,Redis 會 fork 子程序來完成,fork 操作的用時和 Redis 的資料量是正相關的,而 fork 在執行時會阻塞主執行緒。資料量越大,fork 操作造成的主執行緒阻塞的時間越長,會導致 Redis 響應變慢。我們使用 INFO 命令檢視 Redis 的 latest_fork_usec 指標值(表示最近一次 fork 的耗時)。所以當資料量持續增長時,透過擴大記憶體的方式不太適用,應該採用切片叢集。

切片叢集,也叫分片叢集,就是指啟動多個 Redis 例項組成一個叢集,然後按照一定的規則,把收到的資料劃分成多份,每一份用一個例項來儲存。例如把 25GB 的資料平均分成 5 份(當然,也可以不做均分),使用 5 個例項來儲存,每個例項只需要儲存 5GB 資料。例項在為 5GB 資料生成 RDB 時,資料量就小了很多,fork 子程序一般不會給主執行緒帶來較長時間的阻塞。
加大記憶體、切片叢集,兩種方法對應的就是 縱向擴充套件(scale up)和橫向擴充套件(scale out):
(1)縱向擴充套件:升級單個 Redis 例項的資源配置,包括增加記憶體容量、增加磁碟容量、使用更高配置的 CPU。
(2)橫向擴充套件:橫向增加當前 Redis 例項的個數。
縱向擴充套件的好處是,實施起來簡單、直接,但是受限於資料量增加帶來的阻塞問題和硬體成本問題。與縱向擴充套件相比,橫向擴充套件是一個擴充套件性更好的方案,要想儲存更多的資料,只用增加 Redis 的例項個數就行了,相對的管理起來會複雜一點。

我們就需要解決兩大問題:
(1)資料切片後,在多個例項之間如何分佈?
(2)客戶端怎麼確定想要訪問的資料在哪個例項上?

Redis 切片叢集通常是透過 Redis Cluster 來實現的,用雜湊槽(Hash Slot,接下來我會直接稱之為 Slot),來處理資料和例項之間的對映關係,一個切片叢集共有 16384 個雜湊槽,這些雜湊槽類似於資料分割槽,每個鍵值對都會根據它的 key,被對映到一個雜湊槽中。先根據鍵值對的 key 按照 CRC16 演算法 計算一個 16 bit 的值,然後再用這個 16bit 值對 16384 取模,得到 0~16383 範圍內的模數,每個模數代表一個相應編號的雜湊槽。

使用 cluster create 命令建立叢集時,Redis 會自動把這些槽平均分佈在叢集例項上,每個例項上的槽個數為 16384/N 個。也可以使用 cluster meet 命令手動建立例項間的連線,形成叢集,再使用 cluster addslots 命令,指定每個例項上的雜湊槽個數。舉個例子,假設3個例項,5個雜湊槽,根據例項記憶體情況,按下圖配置:

redis-cli -h 172.16.19.3 –p 6379 cluster addslots 0,1
redis-cli -h 172.16.19.4 –p 6379 cluster addslots 2,3
redis-cli -h 172.16.19.5 –p 6379 cluster addslots 4

key1 和 key2 計算完 CRC16 值後,對雜湊槽總個數 5 取模,再根據各自的模數結果,就可以被對映到對應的例項 1 和例項 3 上了,這個過程就完成了資料分佈的問題。(注意:手動分配雜湊槽時,需要把 16384 個槽都分配完,否則 Redis 叢集無法正常工作)

叢集建立後,Redis 例項會把自己的雜湊槽資訊發給和它相連線的其它例項,來完成雜湊槽分配資訊的擴散。客戶端和叢集例項建立連線後,例項就會把雜湊槽的分配資訊發給客戶端,客戶端收到雜湊槽資訊後,會把雜湊槽資訊快取在本地。當客戶端請求鍵值對時,會先計算鍵所對應的雜湊槽,然後就可以給相應的例項傳送請求了。

但是,在叢集中,例項和雜湊槽的對應關係並不是一成不變的,最常見的變化有兩個:
(1)在叢集中,例項有新增或刪除,Redis 需要重新分配雜湊槽
(2)為了負載均衡,Redis 需要把雜湊槽在所有例項上重新分佈一遍

例項之間可以透過相互傳遞訊息,獲得最新的雜湊槽分配資訊,但是,客戶端是無法主動感知這些變化的。這就會導致,它快取的分配資訊和最新的分配資訊就不一致。Redis Cluster 方案提供了一種重定向機制,所謂的“重定向”,就是指,客戶端給一個例項傳送資料讀寫操作時,這個例項上並沒有相應的資料的話,會返回 MOVED 命令響應結果,這個結果中就包含了新例項的訪問地址,客戶端重定向到新例項,同時更新本地對應關係的快取。

GET hello:key
(error) MOVED 13320 172.16.19.5:6379

在實際應用時,如果正好趕上例項資料正在遷移,訪問的 Slot 2 中的資料只有一部分遷移到了例項 3,還有部分資料沒有遷移。這種情況下,客戶端就會收到一條 ASK 報錯資訊:

GET hello:key
(error) ASK 13320 172.16.19.5:6379

這表明客戶端請求的鍵值對所在的雜湊槽 13320,在 172.16.19.5 這個例項上,但是這個雜湊槽正在遷移,客戶端需要先給 172.16.19.5 這個例項傳送一個 ASKING 命令,讓這個例項允許執行客戶端接下來傳送的命令。然後,客戶端再向這個例項傳送 GET 命令,以讀取資料。舉個例子如圖:

Slot 2 正在從例項 2 往例項 3 遷移,key1 和 key2 已經遷移過去,key3 和 key4 還在例項 2。客戶端向例項 2 請求 key2 後,就會收到例項 2 返回的 ASK 命令。然後給例項 3 傳送 ASKING 命令,才能讀取 key2 的資料。注意,和 MOVED 命令不同,ASK 命令並不會更新客戶端快取的雜湊槽分配資訊,如果客戶端再次請求 Slot 2 中的資料,它還是會給例項 2 傳送請求,重複上述步驟。


相關文章