Redis 中常見的叢集部署方案

Rick.lz發表於2022-02-20

Redis 的高可用叢集

前言

這裡來了解一下,Redis 中常見的叢集方案

幾種常用的叢集方案

  • 主從叢集模式

  • 哨兵機制

  • 切片叢集(分片叢集)

主從叢集模式

主從叢集,主從庫之間採用的是讀寫分離

  • 主庫:所有的寫操作都在讀庫發生,然後主庫同步資料到從庫,同時也可以進行讀操作;

  • 從庫:只負責讀操作;

redis

主庫需要複製資料到從庫,主從雙方的資料庫需要儲存相同的資料,將這種情況稱為"資料庫狀態一致"

來看下如何同步之前先來了解下幾個概念

  • 1、伺服器的執行ID(run ID):每個 Redis 伺服器在執行期間都有自己的run IDrun ID在伺服器啟動的時候自動生成。

從伺服器會記錄主伺服器的run ID,這樣如果發生斷網重連,就能判斷新連線上的主伺服器是不是上次的那一個,這樣來決定是否進行資料部分重傳還是完整重新同步。

  • 2、複製偏移量 offset:主伺服器和從伺服器都會維護一個複製偏移量

主伺服器每次向從伺服器中傳遞 N 個位元組的時候,會將自己的複製偏移量加上 N。

從伺服器中收到主伺服器的 N 個位元組的資料,就會將自己額複製偏移量加上 N。

通過主從伺服器的偏移量對比可以很清楚的知道主從伺服器的資料是否處於一致。

如果不一致就需要進行增量同步了,具體參加下文的增量同步

全量同步

從伺服器首次加入主伺服器中發生的是全量同步

如何進行第一次同步?

redis

1、從伺服器連線到主伺服器,然後傳送 psync 到主伺服器,因為第一次複製,不知道主庫run ID,所以run ID為?;

2、主伺服器接收到同步的響應,回覆從伺服器自己的run ID和複製進行進度 offset;

3、主伺服器開始同步所有資料到從庫中,同步依賴 RDB 檔案,主庫會通過 bgsave 命令,生成 RDB 檔案,然後將 RDB 檔案傳送到從庫中;

4、從庫收到 RDB 檔案,清除自己的資料,然後載入 RDB 檔案;

5、主庫在同步的過程中不會被阻塞,仍然能接收到命令,但是新的命令是不能同步到從庫的,所以主庫會在記憶體中用專門的 replication buffer,記錄 RDB 檔案生成後收到的所有寫操作,然後在 RDB 檔案,同步完成之後,再將replication buffer中的命令傳送到從庫中,這樣就保證了從庫的資料同步。

增量同步

如果主從伺服器之間發生了網路閃斷,從從服務將會丟失一部分同步的命令。

在舊版本,Redis 2.8之前,如果發生了網路閃斷,就會進行一次全量複製。

在 2.8 版本之後,引入了增量同步的技術,這裡主要是用到了 repl_backlog_buffer

Redis 主庫接收到寫操作的命令,首先會寫入replication buffer(主要用於主從資料傳輸的資料緩衝),同時也會把這些操作命令也寫入repl_backlog_buffer這個緩衝區。

redis

這裡可能有點疑惑,已經有了replication buffer為什麼還多餘引入一個repl_backlog_buffer呢?

  • repl_backlog_buffer一個主庫對應一個repl_backlog_buffer,也就是所有從庫對應一個repl_backlog_buffer,從庫自己記錄自己的slave_repl_offset

  • replication buffer用於主節點與各個從節點間,資料的批量互動。主節點為各個從節點分別建立一個緩衝區,由於各個從節點的處理能力差異,各個緩衝區資料可能不同。

如何主從斷開了,當然對應的replication buffer也就沒有了。這時候就依賴repl_backlog_buffer進行資料的增量同步了。

repl_backlog_buffer是一個環形緩衝區,主庫會記錄自己寫到的位置,從庫則會記錄自己已經讀到的位置。

這裡借用Redis核心技術與實戰的一張圖片

redis

剛開始主伺服器的 master_repl_offset 和從伺服器 slave_repl_offset 的位置是一樣的,在從庫因為網路原因斷連之後,隨著主庫寫操作的進行,主從偏移量會出現偏移距離。

當從伺服器連上主伺服器之後,從服務把自己當前的 slave_repl_offset 告訴主伺服器,然後主伺服器根據自己的 master_repl_offset 計算出和從伺服器之間的差距,然後把兩者之間相差的命令操作同步給從伺服器。

舉個例子

比如這裡從伺服器1,剛剛由於網路原因斷連了一會,然後又恢復了連線,這時候,可能缺失了一段時間的命令同步,repl_backlog_buffer的增量同步機制就登場了。

repl_backlog_buffer會根據主伺服器的master_repl_offset和從伺服器slave_repl_offset,計算出兩者命令之間的差距,之後把差距同步給replication buffer,然後傳送到從伺服器中。

redis

repl_backlog_buffer中的緩衝空間要設定的大一點,如果從庫讀的過慢,因為是環形緩衝區,可能出現命令覆蓋的情況,如果出現命令被覆蓋了,從庫的增量同步就無法進行了,這時候會進行一次全量的複製。

緩衝空間的計算公式是:緩衝空間大小 = 主庫寫入命令速度 * 操作大小 - 主從庫間網路傳輸命令速度 * 操作大小。在實際應用中,考慮到可能存在一些突發的請求壓力,我們通常需要把這個緩衝空間擴大一倍,即 repl_backlog_size = 緩衝空間大小 * 2,這也就是 repl_backlog_size 的最終值。

哨兵機制

對於主從叢集模式,如果從庫發生了故障,還有主庫和其它的從庫可以接收請求,但是如果主庫掛了,就不能進行正常的資料寫入,同時資料同步也不能正常的進行了,當然這種情況,我們需要想辦法避免,於是就引入了下面的哨兵機制。

什麼是哨兵機制

sentinel(哨兵機制):是 Redis 中叢集的高可用方式,哨兵節點是特殊的 Redis 服務,不提供讀寫,主要來監控 Redis 中的例項節點,如果監控服務的主伺服器下線了,會從所屬的從伺服器中重新選出一個主伺服器,代替原來的主伺服器提供服務。

redis

核心功能就是:監控,選主,通知。

監控:哨兵機制,會週期性的給所有主伺服器發出 PING 命令,檢測它們是否仍然線上執行,如果在規定的時間內響應了 PING 通知則認為,仍線上執行;如果沒有及時回覆,則認為服務已經下線了,就會進行切換主庫的動作。

選主:當主庫掛掉的時候,會從從庫中按照既定的規則選出一個新的的主庫,

通知:當一個主庫被新選出來,會通知其他從庫,進行連線,然後進行資料的複製。當客戶端試圖連線失效的主庫時,叢集也會向客戶端返回新主庫的地址,使得叢集可以使用新的主庫。

如何保證選主的準確性

哨兵會通過 PING 命令檢測它和從庫,主庫之間的連線情況,如果發現響應超時就會認為給服務已經下線了。

當然這會存在誤判的情況,如果叢集的網路壓力比較大,網路堵塞,這時候會存在誤判的情況。

如果誤判的節點是從節點,影響不會很大,拿掉一個從節點,對整體的服務,影響不大,還是會不間斷的對外提供服務。

如果誤判的節點是主節點,影響就很大了,主節點被標註下線了,就會觸發後續的選主,資料同步,等一連串的動作,這一連串的動作很很消耗效能的。所以對於誤判,應該去規避。

如何減少誤判呢?

引入哨兵叢集,一個哨兵節點可能會進行誤判,引入多個少哨兵節點一起做決策,就能減少誤判了。

當有多個哨兵節點的時候,大多數哨兵節點認為主庫下線了,主庫才會真正的被標記為下線了,一般來講當有 N 個哨兵例項時,最好要有N/2 + 1個例項判斷主庫下線了,才能最終判定主庫的下線狀態。當然這個數值在 Redis 中是可以配置的。

如何選主

選舉主節點的規則

1、過濾掉已經下線的伺服器;

2、過濾掉最近5秒鐘沒有回覆過主節點的 INFO(用於觀察伺服器的角色) 命令的伺服器,保證選中的伺服器都是最近成功通過信的;

3、過濾掉和下線主伺服器連線超過down-after-milliseconds*10毫秒的從伺服器,down-after-milliseconds是主伺服器下線的時間,這一操作避免從伺服器與主伺服器過早的斷開,影響到從庫中資料同步,因為斷開時間越久,從庫裡面的資料就越老舊過時。

然後對這些伺服器根據slave-priority優先順序(這個優先順序是手動設定的,比如希望那個從伺服器優先變成主伺服器,優先順序就設定的高一點) 進行排序。

如果幾臺從伺服器優先順序相同,然後根據複製偏移量從大到小進行排序,如果還有相同偏移量的從伺服器,然後按照 runID 從小到大進行排序,直到選出一臺從伺服器。

哨兵進行主節點切換

當根據選舉規則,選出了可以成為主節點的從節點,如何進行切換呢?

在哨兵中也是有一個 Leader 節點的,當一個從庫被選舉出來,從庫的切換是由 Leader 節點完成的。

Leader 節點的選舉用的是 Raft 演算法,關於什麼是 Raft 演算法可參考Raft一致性演算法原理

在raft演算法中,在任何時刻,每一個伺服器節點都處於這三個狀態之一:

  • Follower:追隨者,跟隨者都是被動的:他們不會傳送任何請求,只是簡單的響應來自領導者或者候選人的請求;

  • Candidate:候選人,如果跟隨者接收不到訊息,那麼他就會變成候選人併發起一次選舉,獲得叢集中大多數選票的候選人將成為領導者。

  • Leader:領導者,系統中只有一個領導人並且其他的節點全部都是跟隨者,領導人處理所有的客戶端請求(如果一個客戶端和跟隨者聯絡,那麼跟隨者會把請求重定向給領導人)

哨兵節點的選舉總結起來就是:

1、每個做主觀下線的sentinel節點向其他sentinel節點傳送命令,要求將自己設定為領導者;

2、接收到的sentinel可以同意或者拒絕;

3、如果該sentinel節點發現自己的票數已經超過半數並且超過了 quorum,quorum 用來配置判斷主節點當機的哨兵節點數。簡單點講就是:如果 Sentinel 叢集有 quorum 個哨兵認為 master 當機了,就「客觀」的認為 master 當機了;

4、如果此過程選舉出了多個領導者,那麼將等待一段時重新進行選舉;

故障轉移

  • sentinel的領導者從從機中選舉出合適的叢機進行故障轉移;

  • 對選取的從節點進行slave of no one命令,(這個命令用來讓從機關閉複製功能,並從從機變為主機);

  • 更新應用程式端的連結到新的主節點;

  • 對其他從節點變更 master 為新的節點;

  • 修復原來的 master 並將其設定為新的 master 的從機。

訊息通知

哨兵和哨兵之前,哨兵和從庫之間,哨兵和客戶端是如何相互發現,進行訊息傳遞?

哨兵和哨兵之間的相互發現,通過 Redis 提供的pub/sub機制實現,因為每個哨兵節點都會和主庫進行連線,通過在主庫中釋出資訊,訂閱資訊,就能找到其他例項的連線資訊。

哨兵節點和從庫,通過哨兵向主庫傳送 INFO 命令來完成,哨兵給主庫傳送 INFO 命令,主庫接受到這個命令後,就會把從庫列表返回給哨兵。接著,哨兵就可以根據從庫列表中的連線資訊,和每個從庫建立連線,並在這個連線上持續地對從庫進行監控。

哨兵和客戶端之間:每個哨兵例項也提供pub/sub機制,客戶端可以從哨兵訂閱訊息,來獲知主從庫切換過程中的不同關鍵事件。

哨兵提升一個從庫為新主庫後,哨兵會把新主庫的地址寫入自己例項的 pubsub(switch-master) 中。客戶端需要訂閱這 個pubsub,當這個 pubsub 有資料時,客戶端就能感知到主庫發生變更,同時可以拿到最新的主庫地址,然後把寫請求寫到這個新主庫即可,這種機制屬於哨兵主動通知客戶端。

如果客戶端因為某些原因錯過了哨兵的通知,或者哨兵通知後客戶端處理失敗了,安全起見,客戶端也需要支援主動去獲取最新主從的地址進行訪問。

切片叢集

對於資料庫我們知道,如果資料量大會進行分庫分表,一般有兩種方案縱向拆分和橫向拆分。這在 Redis 中,同樣適用。

Redis 中的擴充套件

  • 縱向擴充套件:更改節點型別以調整叢集大小,升級單個Redis例項的資源配置,包括增加記憶體容量、增加磁碟容量、使用更高配置的CPU。

  • 橫向擴充套件:通過新增或刪除節點組(分片)來更改複製組中的節點組(分片)數量。

redis

簡單點講就是:垂直擴容就是增加自身的容量,橫向擴容就是加機器。

缺點對比

縱向擴容:

1、如果一味的增加自身的容量,意味著自身儲存的資料將會越來越大,過大的資料,持久化時間將會變得很長,影響自身的響應速度;

2、同樣堆硬體總歸是有上線,達到一定量之後,還是要考慮進行橫向擴容;

橫向擴容:

橫向擴容要面臨的問題,如果發生了分片的擴容,就需要考慮資料的遷移,同時資料切片後,在多個例項之間如何分佈?,客戶端如何知道訪問的資料在哪個例項中。。。

雖然有這些問題的存在,好在已經有一些成熟的方案來處理橫向擴容所遇到的問題了

官方的叢集解決方案就是Redis Cluster;社群的解決方案有 Codis 和 Twemproxy,Codis 是由我國的豌豆莢團隊開源的,Twemproxy 是 Twitter 團隊的開源的。

這裡主要看下Redis Cluster是如何進行處理的

Redis Cluster方案

1、Redis Cluster方案採用雜湊槽來處理 KEY 在不同例項中的分佈,一個切片叢集共有16384個雜湊槽,這些雜湊槽類似於資料分割槽,每個鍵值對都會根據它的key,被對映到一個雜湊槽中。

2、一個 KEY ,首先會根據CRC16演算法計算一個16 bit的值;然後,再用這個 16bit 值對 16384 取模,得到0~16383範圍內的模數,每個模數代表一個相應編號的雜湊槽。

3、然後把雜湊槽分配到所有的例項中,例如,如果叢集中有N個例項,那麼,每個例項上的槽個數為16384/N個。

當然這是平均分配的,如果平均分配額雜湊槽中,某一個例項中 KEY,儲存的資料比較大,造成某一個例項的記憶體過大,這時候可以通過cluster addslots手動調節雜湊槽的分配。

當手動分配雜湊槽時,需要把16384個槽都分配完,否則Redis叢集無法正常工作。

客戶端中的 KEY 如何找到對應的例項

在叢集剛剛建立的時候,每個例項只知道自己被分配了哪些雜湊槽,是不知道其他例項擁有的雜湊槽資訊的。但是,Redis 例項會把自己的雜湊槽資訊發給和它相連線的其它例項,來完成雜湊槽分配資訊的擴散。

所以當客戶端和叢集例項連線後,就可以知道所有的雜湊槽的對映,客戶端會把雜湊槽的對映儲存在本地,這樣如果客戶端響應一個 KEY ,計算出雜湊槽,然後就可以向對應的例項傳送請求了。

雜湊槽重新分配

資料在可能發生遷移,這時候雜湊槽就會重新分配了

慄如:

1、叢集中的例項,有增加或減少;

2、引入了負載均衡,需要重新分配雜湊槽;

因為重新分配了雜湊槽,所以原來的對映關係可能發生了改變,例項之間可以通過相互通知,快速的感知到對映關係的變化。但是,客戶端無法主動感知這些變化,客戶端對 KEY 的響應,可能依舊對映到了之前的例項節點,面對這種情況,如何處理呢?

1、如果資料已經遷移完了

Redis Cluster中提供重定向機制,如果一個例項接收到客戶端的請求,但是對應的 KEY 已經轉移到別的例項節點中了,這時候會計算出 KEY 當前所處例項的地址,然後返回給客戶端,客戶端拿到最新的例項地址,重新傳送請求就可以了。

$ GET hello
(error) MOVED 12320 172.168.56.111:6379
2、資料遷移了一半

如果在遷移的過程中,只遷移了一半的資料,這時候伺服器端就會返回 ASK 告知客戶端

GET hello
(error) ASK 1332 012320 172.168.56.111:6379

ASK 就表示當前正在遷移中,客戶端需要訪問資料,就還需要向返回的地址資訊,傳送一條 ASKING 命令,讓這個例項允許客戶端的訪問請求,然後客戶端再傳送具體的業務操作命令。

避免 Hot Key

Hot Key就是採用切片叢集部署的 Redis ,出現的叢集訪問傾斜。

切片叢集中的 Key 最終會儲存到叢集中的一個固定的 Redis 例項中。某一個 Key 在一段時間內訪問遠高於其它的 Key,也就是該 Key 對應的 Redis 例項,會收到過大的流量請求,該例項容易出現過載和卡頓現象,甚至還會被打掛掉。

常見引發熱點 Key 的情況:

1、新聞中的熱點事件;

2、秒殺活動中的,價效比高的商品;

如何發現 Hot Key
  • 1、提現預判;

根據業務經驗進行提前預判;

  • 2、在客戶端進行收集;

通過在客戶端增加命令的採集,來統計發現熱點 Key;

  • 3、使用 Redis 自帶的命令排查;

使用monitor命令統計熱點key(不推薦,高併發條件下會有造成redis 記憶體爆掉的隱患);

hotkeys引數,redis 4.0.3提供了redis-cli的熱點key發現功能,執行redis-cli時加上–hotkeys選項即可。但是該引數在執行的時候,如果key比較多,執行起來比較慢。

  • 4、在Proxy層做收集

如果叢集架構引入了 proxy,可以在 proxy 中做統計

  • 5、自己抓包評估

Redis客戶端使用TCP協議與服務端進行互動,通訊協議採用的是RESP。自己寫程式監聽埠,按照RESP協議規則解析資料,進行分析。缺點就是開發成本高,維護困難,有丟包可能性。

Hot Key 如何解決

知道了Hot Key如何來應對呢

  • 1、對 Key 進行分散處理;

舉個例子

有一個熱 Key 名字為Hot-key-test,可以將其分散為Hot-key-test1Hot-key-test2...然後將這些 Key 分散到多個例項節點中,當客戶端進行訪問的時候,隨機一個下標的 Key 進行訪問,這樣就能將流量分散到不同的例項中了,避免了一個快取節點的過載。

一般來講,可以通過新增字尾或者字首,把一個 hotkey 的數量變成 redis 例項個數 N 的倍數 M,從而由訪問一個redis key變成訪問N * M個redis key。 N*Mredis key經過分片分佈到不同的例項上,將訪問量均攤到所有例項。

const M = N * 2
//生成隨機數
random = GenRandom(0, M)
//構造備份新key
bakHotKey = hotKey + “_” + random
data = redis.GET(bakHotKey)
if data == NULL {
    data = GetFromDB()
    redis.SET(bakHotKey, expireTime + GenRandom(0,5))
}
  • 2、使用本地快取;

業務端還可以使用本地快取,將這些熱 key 記錄在本地快取,來減少對遠端快取的衝擊。

避免 Big Key

什麼是 Big Key:我們將含有較大資料或含有大量成員、列表數的Key稱之為大Key。

  • 一個STRING型別的Key,它的值為5MB(資料過大)

  • 一個LIST型別的Key,它的列表數量為20000個(列表數量過多)

  • 一個ZSET型別的Key,它的成員數量為10000個(成員數量過多)

  • 一個HASH格式的Key,它的成員數量雖然只有1000個但這些成員的value總大小為100MB(成員體積過大)

Big Key 存在問題

  • 記憶體空間不均勻:如果採用切片叢集的部署方案,容易造成某些例項節點的記憶體分配不均勻;

  • 造成網路擁塞:讀取 bigkey 意味著需要消耗更多的網路流量,可能會對 Redis 伺服器造成影響;

  • 過期刪除:big key 不單讀寫慢,刪除也慢,刪除過期 big key 也比較耗時;

  • 遷移困難:由於資料龐大,備份和還原也容易造成阻塞,操作失敗;

如何發現 Big Key

  • 使用 redis-cli 客戶端的命令 --bigkeys;

  • 生成 rdb 檔案,離線分析 rdb 檔案。比如:redis-rdb-cli,rdbtools;

  • 通過 scan 命令,對掃描出來的key進行型別判斷,例如:string長度大於10K,list長度大於10240認為是big bigkeys;

Big Key 如何避免

對於Big Key可以從以下兩個方面進行處理

  • 合理優化資料結構:

1、對較大的資料進行壓縮處理;

2、拆分集合:將大的集合拆分成小集合(如以時間進行分片)或者單個的資料。

  • 選擇其他的技術來儲存 big key:

使用其他的儲存形式,考慮使用 cdn 或者文件性資料庫 MongoDB。

Big Key 如何刪除

直接使用 DEL 命令會發生什麼?危險:同步刪除 bigkey 會阻塞 Redis 其他命令,造成 Redis 阻塞。

推薦使用 UNLINK 命令,非同步刪除 bigkey,不影響主執行緒執行其他命令。

在業務的低峰期使用 scan 命令查詢 big key,對於型別為集合的key,可以使用指令碼逐一刪除裡面的元素。

參考

【Redis核心技術與實戰】https://time.geekbang.org/column/intro/100056701
【Redis設計與實現】https://book.douban.com/subject/25900156/
【估算兩臺伺服器同時故障的概率】https://disksing.com/failure-probability-analysis/
【Redis中哨兵選舉演算法】https://blog.csdn.net/weixin_44324174/article/details/108939199
【如何處理redis叢集中hot key和big key】https://juejin.cn/post/6844903743083773959
【談談redis的熱key問題如何解決】https://www.cnblogs.com/rjzheng/p/10874537.html
【Redis 中常見的叢集部署方案】https://boilingfrog.github.io/2022/02/20/redis中常見的叢集部署方案/#主從叢集模式
【Redis學習筆記】https://github.com/boilingfrog/Go-POINT/tree/master/redis

相關文章