簡要記錄了Redis叢集的核心原理
這幾天工作需要研究了一下Redis叢集,将其原理的核心内容記錄下來以便以後查閱。
一個系統建立叢集主要需要解決兩個問題:資料同步問題和叢集容錯問題。
一個簡單粗暴的方案是部署多台一模一樣的Redis服務,再用負載均衡來分攤壓力以及監控服務狀态。這種方案的優勢在于容錯簡單,隻要有一台存活,整個叢集就仍然可用。但是它的問題在于保證這些Redis服務的資料一緻時,會導緻大量資料同步操作,反而影響性能和穩定性。
Redis叢集方案基于分而治之的思想。Redis中資料都是以Key-Value形式存儲的,而不同Key的資料之間是互相獨立的。是以可以将Key按照某種規則劃分成多個分區,将不同分區的資料存放在不同的節點上。這個方案類似資料結構中哈希表的結構。在Redis叢集的實作中,使用雜湊演算法(公式是<code>CRC16(Key) mod 16383</code>)将Key映射到0~16383範圍的整數。這樣每個整數對應存儲了若幹個Key-Value資料,這樣一個整數對應的抽象存儲稱為一個槽(slot)。每個Redis Cluster的節點——準确講是master節點——負責一定範圍的槽,所有節點組成的叢集覆寫了0~16383整個範圍的槽。
據說任何計算機問題都可以通過增加一個中間層來解決。槽的概念也是這麼一層。它介于資料和節點之間,簡化了擴容和收縮操作的難度。資料和槽的映射關系由固定算法完成,不需要維護,節點隻需維護自身和槽的映射關系。
上面的方案隻是解決了性能擴充的問題,叢集的故障容錯能力并沒有提升。提高容錯能力的方法一般為使用某種備份/備援手段。負責一定數量的槽的節點被稱為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配置檔案以啟動叢集模式:
然後啟動新節點。
使用用戶端發起指令<code>cluster <ip> <port></code>,節點會發送meet消息将指定IP和端口的新節點加入叢集。
上一步執行完後我們得到的是一個還沒有負責任何槽的“空”叢集。為了使叢集可用,我們需要将16384個槽都配置設定到master節點數。
在用戶端執行<code>cluster add addslots {<a>...<b>}</code>指令,将<code><a></code>~<code><b></code>範圍的槽都配置設定給目前用戶端所連接配接的節點。将所有的槽都配置設定給master節點後,執行<code>cluster nodes</code>指令,檢視各個節點負責的槽,以及節點的ID。
接下來還需要配置設定slave節點。使用用戶端連接配接待配置設定的slave節點,執行<code>cluster replicate <nodeId></code>指令,将該節點配置設定為<code><nodeId></code>指定的master節點的備份。
在Redis 5版本中<code>redis-cli</code>用戶端新增了叢集操作指令。
如下所示,直接使用指令建立一個3主3從的叢集:
如果你用的是舊版本的Redis,可以使用官方提供的<code>redis-trib.rb</code>腳本來建立叢集:
擴容操作與建立叢集操作類似,不同的在于最後一步是将槽從已有的節點遷移到新節點。
啟動新節點:同建立叢集。
将新節點加入到叢集:使用<code>redis-cli --cluster add-node</code>指令将新節點加入叢集(内部使用meet消息實作)。
遷移槽和資料:添加新節點後,需要将一些槽和資料從舊節點遷移到新節點。使用指令<code>redis-cli --cluster reshard</code>進行槽遷移操作。
為了安全删除節點,Redis叢集隻能下線沒有負責槽的節點。是以如果要下線有負責槽的master節點,則需要先将它負責的槽遷移到其他節點。
遷移槽。使用指令<code>redis-cli --cluster reshard</code>将待删除節點的槽都遷移到其他節點。
忘記節點。使用指令<code>redis-cli --cluster del-node</code>删除節點(内部使用forget消息實作)。
如果你的redis-cli版本低于5,那麼可以使用redis-trib.rb腳本來完成上面的指令。點選這裡檢視<code>redis-cli</code>和<code>redis-trib.rb</code>操作叢集的指令。
Redis有RDB和AOF兩種持久化政策。這篇文章詳細講解了RDB和AOF持久化原理。
RDB持久化神坑:
即使設定了<code>save ""</code>試圖關閉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對應的槽,導緻此節點無法再加入其它叢集。