乾貨:一文詳解Redis集群原理核心內容

  • 2019 年 12 月 2 日
  • 筆記

集群原理

一個系統建立集群主要需要解決兩個問題:數據同步問題和集群容錯問題。

Naive方案

一個簡單粗暴的方案是部署多台一模一樣的Redis服務,再用負載均衡來分攤壓力以及監控服務狀態。這種方案的優勢在於容錯簡單,只要有一台存活,整個集群就仍然可用。但是它的問題在於保證這些Redis服務的數據一致時,會導致大量數據同步操作,反而影響性能和穩定性。

Redis集群方案

Redis集群方案基於分而治之的思想。Redis中數據都是以Key-Value形式存儲的,而不同Key的數據之間是相互獨立的。因此可以將Key按照某種規則劃分成多個分區,將不同分區的數據存放在不同的節點上。這個方案類似數據結構中哈希表的結構。在Redis集群的實現中,使用哈希演算法(公式是CRC16(Key) mod 16383)將Key映射到0~16383範圍的整數。這樣每個整數對應存儲了若干個Key-Value數據,這樣一個整數對應的抽象存儲稱為一個槽(slot)。每個Redis Cluster的節點——準確講是master節點——負責一定範圍的槽,所有節點組成的集群覆蓋了0~16383整個範圍的槽。

據說任何電腦問題都可以通過增加一個中間層來解決。槽的概念也是這麼一層。它介於數據和節點之間,簡化了擴容和收縮操作的難度。數據和槽的映射關係由固定演算法完成,不需要維護,節點只需維護自身和槽的映射關係。

Slave

上面的方案只是解決了性能擴展的問題,集群的故障容錯能力並沒有提升。提高容錯能力的方法一般為使用某種備份/冗餘手段。負責一定數量的槽的節點被稱為master節點。為了增加集群穩定性,每個master節點可以配置若干個備份節點——稱為slave節點。Slave節點一般作為冷備份保存master節點的數據,在master節點宕機時替換master節點。在一些數據訪問壓力比較大的情況下,slave節點也可以提供讀取數據的功能,不過slave節點的數據實時性會略差一下。而寫數據的操作則只能通過master節點進行。

請求重定向

當Redis節點接收到對某個key的命令時,如果這個key對應的槽不在自己的負責範圍內,則返回MOVED重定向錯誤,通知客戶端到正確的節點去訪問數據。

如果頻繁出現重定向錯誤,勢必會影響訪問的性能。由於從key映射到槽的演算法是固定公開的,客戶端可以在內部維護槽到節點的映射關係,訪問數據時可以自己通過key計算出槽,然後找到正確的節點,減少重定向錯誤。目前大部分開發語言的Redis客戶端都會實現這個策略。這個地址https://redis.io/clients可以查看主流語言的Redis客戶端。

節點通訊

儘管不同節點存儲的數據相互獨立,這些節點仍然需要相互通訊以同步節點狀態資訊。Redis集群採用P2P的Gossip協議,節點之間不斷地通訊交換資訊,最終所有節點的狀態都會達成一致。常用的Gossip消息有下面幾種:

  • ping消息:每個節點不斷地向其他節點發起ping消息,用於檢測節點是否在線和交換節點狀態資訊。
  • pong消息:收到ping、meet消息時的響應消息。
  • meet消息:新節點加入消息。
  • fail消息:節點下線消息。
  • forget消息:忘記節點消息,使一個節點下線。這個命令必須在60秒內在所有節點執行,否則超過60秒後該節點重新參與消息交換。實踐中不建議直接使用forget命令來操作節點下線。

節點下線

當某個節點出現問題時,需要一定的傳播時間讓多數master節點認為該節點確實不可用,才能標記標記該節點真正下線。Redis集群的節點下線包括兩個環節:主觀下線(pfail)和客觀下線(fail)。

  • 主觀下線:當節點A在cluster-node-timeout時間內和節點B通訊(ping-pong消息)一直失敗,則節點A認為節點B不可用,標記為主觀下線,並將狀態消息傳播給其他節點。
  • 客觀下線:當一個節點被集群內多數master節點標記為主觀下線後,則觸發客觀下線流程,標記該節點真正下線。

故障恢復

一個持有槽的master節點客觀下線後,集群會從slave節點中選出一個提升為master節點來替換它。Redis集群使用選舉-投票的演算法來挑選slave節點。一個slave節點必須獲得包括故障的master節點在內的多數master節點的投票後才能被提升為master節點。假設集群規模為3主3從,則必須至少有2個主節點存活才能執行故障恢復。如果部署時將2個主節點部署到同一台伺服器上,則該伺服器不幸宕機後集群無法執行故障恢復。

默認情況下,Redis集群如果有master節點不可用,即有一些槽沒有負責的節點,則整個集群不可用。也就是說當一個master節點故障,到故障恢復的這段時間,整個集群都處於不可用的狀態。這對於一些業務來說是不可忍受的。可以在配置中將cluster-require-full-coverage配置為no,那麼master節點故障時只會影響訪問它負責的相關槽的數據,不影響對其他節點的訪問。

搭建集群

啟動新節點

修改Redis配置文件以啟動集群模式:

# 開啟集群模式  cluster-enabled yes  # 節點超時時間,單位毫秒  cluster-node-timeout 15000  # 集群節點資訊文件  cluster-config-file "nodes-6379.conf"

然後啟動新節點。

發送meet消息將節點組成集群

使用客戶端發起命令cluster <ip> <port>,節點會發送meet消息將指定IP和埠的新節點加入集群。

分配槽

上一步執行完後我們得到的是一個還沒有負責任何槽的「空」集群。為了使集群可用,我們需要將16384個槽都分配到master節點數。

在客戶端執行cluster add addslots {<a>...<b>}命令,將<a>~<b>範圍的槽都分配給當前客戶端所連接的節點。將所有的槽都分配給master節點後,執行cluster nodes命令,查看各個節點負責的槽,以及節點的ID。

接下來還需要分配slave節點。使用客戶端連接待分配的slave節點,執行cluster replicate <nodeId>命令,將該節點分配為<nodeId>指定的master節點的備份。

使用命令直接創建集群

在Redis 5版本中redis-cli客戶端新增了集群操作命令。

如下所示,直接使用命令創建一個3主3從的集群:

redis-cli --cluster create 127.0.0.1:7000 127.0.0.1:7001   127.0.0.1:7002 127.0.0.1:7003 127.0.0.1:7004 127.0.0.1:7005   --cluster-replicas 1

如果你用的是舊版本的Redis,可以使用官方提供的redis-trib.rb腳本來創建集群:

./redis-trib.rb create --replicas 1 127.0.0.1:7000 127.0.0.1:7001   127.0.0.1:7002 127.0.0.1:7003 127.0.0.1:7004 127.0.0.1:7005

集群伸縮

擴容

擴容操作與創建集群操作類似,不同的在於最後一步是將槽從已有的節點遷移到新節點。

  1. 啟動新節點:同創建集群。
  2. 將新節點加入到集群:使用redis-cli --cluster add-node命令將新節點加入集群(內部使用meet消息實現)。
  3. 遷移槽和數據:添加新節點後,需要將一些槽和數據從舊節點遷移到新節點。使用命令redis-cli --cluster reshard進行槽遷移操作。

收縮

為了安全刪除節點,Redis集群只能下線沒有負責槽的節點。因此如果要下線有負責槽的master節點,則需要先將它負責的槽遷移到其他節點。

  1. 遷移槽。使用命令redis-cli --cluster reshard將待刪除節點的槽都遷移到其他節點。
  2. 忘記節點。使用命令redis-cli --cluster del-node刪除節點(內部使用forget消息實現)。

集群配置工具

如果你的redis-cli版本低於5,那麼可以使用redis-trib.rb腳本來完成上面的命令。點擊這裡查看redis-cliredis-trib.rb操作集群的命令。

持久化

Redis有RDB和AOF兩種持久化策略。

一個RDB持久化的坑

RDB持久化神坑:

  • 即使設置了save ""試圖關閉RDB,然而RDB持久化仍然有可能會觸發。
  • 從節點全量複製(比如新增從節點時),主節點觸發RDB持久化產生RDB文件。然後發送RDB文件給從節點。最後該從節點和對應的主節點都會有RDB文件。
  • 執行shutdown時,如果沒有開啟AOF,也會觸發RDB持久化。
  • 不管save如何設置,只要RDB文件存在,redis啟動時就會去載入該文件。

後果:

  • 如果關閉了RDB持久化(以及AOF持久化),那麼當Redis重啟時,則會載入上一次從節點全量複製或者執行shutdown時保存的RDB文件。而這個RDB文件很可能是一份過時已久的數據。
  • Cluster模式下,Redis重啟並從RDB文件恢複數據後,如果沒有讀取到cluster-config-file中nodes的配置,則標記自己為單獨的master並佔用從RDB中恢復的數據的Key對應的槽,導致此節點無法再加入其它集群