前言:redis cluster是redis分佈式解決方案,集羣通過分片來進行數據共享,並提供複製和故障轉移功能;redisCluster 也是學習分佈式存儲的絕佳案例
目錄
一.數據分佈
分佈式數據庫首先要解決的問題是把整個數據集按照分區規則映射到多個節點的問題,即把數據劃分到多個節點,每個節點負責一個子集。這裏簡單介紹下分區規則
1.常見的分區規則有:
- 哈希分區:離散度好,數據分佈和業務無關,無法順序訪問
- 順序分區:離散度易傾斜,數據分佈和業務相關,可順序訪問
2.哈希分區的規則有:
- 節點取餘:
- 缺點:當節點數量變化時,如擴容或收縮節點,數據節點的映射關係需要重新計算,會導致數據重新遷移
- 擴容時通常採用翻倍擴容,這樣只需遷移50%數據
- 一致性hash分區
- 缺點:增加或減少節點時影響相鄰節點(這也是相對節點取餘的優點)
3.如何解決普通一致性hash算法的問題?
-
虛擬槽分區:採用大範圍槽的主要目的是爲了方便數據拆分和集羣擴展,當增加或者刪除節點時,把這個節點負責的槽轉移就好,這樣節點怎麼變換,key映射到 槽的 規則不會變
4.redis數據分區
-
Redis cluser採用虛擬槽,槽的範圍是0~16383,每個槽負責一部分槽以及槽所映射的鍵值數據
5.槽特點:
-
解耦數據和節點之間的關係,簡化節點擴容和收縮難度
-
節點自身維護槽的映射關係,
-
支持節點、槽、鍵之間的映射查詢,用於數據路由,在線伸縮等場景
注:數據分區是分佈式存儲的核心
二. redis集羣功能的限制
- key批量操作只支持具有相同solt值的key執行(可以使用pipeline 模擬mget等操作)
- 原因:key可能分佈在不同的槽節點
- key正常分佈在多個節點
- 由於數據遷移正進行到一半造成的key分佈在多個節點(ACK錯誤)
- 原因:key可能分佈在不同的槽節點
- 同理只支持多key在同一個節點上的事務操作
- 不支持多數據庫空間,支持數據庫0
- 複製只支持一層,即從節點只能複製主節點,不支持嵌套樹狀複製
三.搭建集羣
節點數量至少爲6個才能保證組成高可用集羣(三主三從)
流程:準備節點,節點握手,分配槽,流程圖如下:
- 準備節點
- 每個節點需要開啓配置 cluster-enabled yes,依次啓動(生成一個節點ID,用於標識集羣內唯一節點。節點ID在集羣初始化的時候只創建一次,節點重啓時會加載集羣配置文件進行重用,注,區別於運行ID,運行ID每次重啓都會變化)
- 可以通過 cluster nodes 命令查看當前集羣節點信息
- 節點握手
定義:指一批運行在集羣模式下的節點通過Gossip協議彼此通信
- 由客戶端發起命令 :cluster meet {ip} {port},如圖:
- 節點6379本地創建6380節點信息對象,併發送meet消息
- 節點6380接受到meet消息後,保存6379節點信息並回復pong消息
- 之後節點6379和6380彼此定期通過ping/pong消息進行正常的節點通信
- 之後分別讓其餘的節點加入到該集羣就好(執行上邊命令)
- 注:可以通過cluster info 查看當前集羣狀態(分配槽的狀態,集羣是否上線等)
- 分配槽
集羣會把所有數據映射到16384個槽。只有當節點分配了槽,才能響應和這些槽相關聯的鍵命令。
- 通過 cluster addslots 爲節點分配槽
- 只有把所有的槽分配給節點後,集羣纔會進入在線狀態3
-
從節點加入集羣
使用 cluster replicate {nodeId} 讓一個從節點複製主
注:必須由從節點發起
-
使用 redis-trib.rb工具搭建集羣
。。。
四. 節點通信
在分佈式存儲中需要提供維護節點元數據信息的機制,如節點負責哪些數據,故障檢測機制等;
各個節點通過彼此交互信息來知道當前集羣的狀態,如各個節點負責哪些槽,節點健康狀態
- 通信流程
- 元數據維護方式分爲
- 集中式
- P2P方式(redis使用Gossip協議,最終一致性)
- 通信過程
- 每個節點單獨開闢一個TCP通道,通信端口號在基礎端口號上+10000
- 每個節點在固定週期內通過特定規則選擇幾個節點發送ping消息
- 接收到ping消息的節點用pong消息回覆
- 注:在某一時間每個節點可能知道全部節點,也可能僅知道部分節點
- 元數據維護方式分爲
- Gossip 消息
- 類型有:
- meet消息:用於通知新節點加入
- ping消息:集羣內每個節點每秒向多個其他節點發送ping消息
- 作用:檢查節點是否在線和交換自身狀態(封裝了自身節點和部分其他節點信息)
- pong消息:
- 接收到meet,ping消息時的應答(封裝了自身狀態信息)
- 節點也可向集羣內廣播自身節點狀態改變信息
- fail消息:當一個節點判斷某節點下線時,像集羣中廣播一個fail消息,收到fail節點後把對應節點標記爲下線
- 類型有:
- 通信節點選擇:
redis集羣節點通信採用固定頻率(定時任務每秒執行10次),主要是爲了兼顧信息實時性和交換信息的成本選擇:
- 每個節點維護定時任務默認每秒執行10次,每100ms 會掃描本地節點列表,如果找到最近一次接受ping消息的時間大於cluster_node_timeout/2,則立刻發送ping消息
- 每秒會隨機選取5個節點找出最久沒有通信的節點發送ping消息
五. 集羣伸縮
集羣伸縮的本質爲 :槽和數據在節點之間的移動
注:集羣在伸縮或者擴容的時候會維護哪些槽在做遷移(遷移出去或遷移數據到本節點)
- 擴容節點
- 準備新節點
- 和現有集羣配置保持一致
- 加入集羣
- 通過 cluster meet 命令加入集羣
- 遷移槽和數據到新節點
- 槽遷移計劃
- 保證各個節點間槽數量平衡
- 遷移數據:數據遷移過程是逐個槽進行的
- 讓目標節點準備導入槽的數據
- 對目標節點發送cluster setslot {slot} importing {sourceNodeId} 命令
- 讓源節點準備遷出槽的數據
- 對源節點發送cluster setslot {slot} migrating {targerNodeId} 命令
- 從源節點取出要遷移的數據
- 循環執行命令cluster getketsinslot {slot} {count}
- 讓目標節點遷移C步驟中導出的數據
- 在源節點上執行命令:migrate {targetIp} {targetPort} "" 0 {timeout} keys {kets…},通過流水線機制批量遷移鍵到目標節點,原子的
- 重複步驟 c,d 直到槽下所有key遷移完
- 通知集羣所有節點槽的所有權變更
- 讓目標節點準備導入槽的數據
- redis-trib.rb 提供了槽分片功能,reshard 命令
- 用法。。。。。。。。。
- 槽遷移計劃
- 給新節點添加從節點
- 讓從節點加入集羣(cluster meet)
- 從節點上執行cluster replicate {masterNodeId}
- 複製流程 和普通複製流程一樣一樣的~~~
- 準備新節點
- 收縮節點
- 流程說明:
- 確定下線節點是否複製槽,如果是遷移到其他節點(過程同擴容節點的遷移數據)
- 當下線節點不在負責槽的時候,通知集羣內其他幾點忘記下線節點
- 可以是用redis-trib.rb del-node {host-port} {downNodeId},實現代碼如下
- 流程說明:
槽計算
使用hash_tag功能,可以讓不通的key映射到通一個槽中
如:set key:{hash_tag}:111 value
set key:{hash_tag}:222 value
....
在進行槽映射計算的時候只計算大括號內的key(不建議這樣,數據分佈不均勻)
六. 客戶端請求路由
redis支持在線遷移槽和數據來完成水平伸縮,當slot對應的數據從節點a遷移到節點b過程中,期間可能出現一部分數據在a節點,一部分數據出現在b節點,或者數據剛遷移到b節點,客戶端還沒來的及更新槽和節點的映射關係,這時候就需要ack 和moven 登場了:
- moven重定向:
- redis首先會計算key所在的槽的位置,在根據槽找出所對應的節點,如果節點不是自身,則回覆MOVEN重定向錯誤,通知客戶端請求正確的節點
- 注:redis集羣是客戶端負載,所以槽所有權變更可能沒有及時的通知到客戶端(客戶端遇到moven錯誤,會把請求重定向,並更新本地槽映射的關係)
- ASK重定向:
- redis支持在線遷移槽來完成水平伸縮(問題:會出現部分數據在源節點,部分數據在目標節點)。客戶端對該情況的執行流程爲:
- 客戶端根據本地slots緩存發送命令到源節點,如果該命令存在則直接返回
- 如果該命令對象不存在,該命令可能已經被遷移到目標節點,源節點返回一個ack重定向異常給客戶端
- 客戶端從ACK重定向異常中提取出目標節點的信息,發送asking命令道目標節點打開客戶端連接標識,在執行鍵命令。如果存在返回結果,不存在返回不存在
- redis支持在線遷移槽來完成水平伸縮(問題:會出現部分數據在源節點,部分數據在目標節點)。客戶端對該情況的執行流程爲:
- ASK 與 moven 的區別
- 都是對客戶端重定向控制
- ASK重定向說明集羣正在進行slot數據遷移,客戶端不知道什麼時候遷移完,只是臨時的重定向,並不會更新客戶端的slots緩存
- moven重定向說明鍵對應的槽已經不屬於當前節點且遷移完數據了,因此需要更新客戶端本地緩存
七.clusterNode介紹
集羣中每個節點都會使用一個ClusterNode結構保存自己的狀態,使用一個clusterState結構保存當前集羣狀態;需要着重介紹下以下結構:ClusterNode.clusterState.fail_reports
- clusterNode.slots:是一個長度爲16384的二進制數組,用來保存當前節點負責哪些槽,如果第 i 位是1說明當前節點負責該槽,反之就是不負責;
- ClusterNode.clusterState.nodes: 是一個字典結構,用來存儲當前集羣節點名單,key爲節點名稱,value爲ClusterNode指針
- ClusterNode.clusterState.slots[16384] : 記錄了所有槽的指派信息,每個數據項都是一個指向clusterNode的指針,在數據查找的時候知道了 一個key屬於哪個槽 就可以根據ClusterNode.clusterState.slots[x] 知道該槽屬於哪個節點
- ClusterNode.clusterState.migrating_slots_to[16384] :該數組記錄了正在遷移出的數據,第x 位代表了 x槽位遷移的節點指針,所以該數組存的是一個ClusterNode指針,指針爲槽正在遷移的數組
問題1:clusterNode.clusterState.slots 數組中保存了所有槽的指派信息,clusterNode.slots保存單個節點的槽分派信息還是有必要嗎?
- 節點間交互信息時候,只需要把clusterNode.slots發送給其他節點就可以了
- 如果不使用clusterNode.slots,每次將x節點信息發送給其他節點的時候需要遍歷整個clusterNode.clusterState.slots 數組,太低效了
問題2:在clusterNode中已經保存了槽節點的信息爲什麼還要在clusterState中保存全量的槽分配信息?
- 如果我們想知道槽i被指派給了哪個槽節點或者槽節點i是否被指派了。我們需要遍歷clusterState.nodes數組下所有clusterNode節點下的 slots信息,直到找到槽i的信息,該過程爲O(N);
- 而在clusterState.slots結構查詢,只需要O(1)
上文中我們知道集羣會把所有數據映射到16384個槽,每個主節點都負責一部分槽。那麼一個節點是如何知道key屬於哪個槽,以及這個槽屬於哪個節點的呢?如果發現ack 或者 moven錯誤當前節點是如何計算出這個key屬於哪個節點呢?
1.計算key屬於哪個槽大概僞代碼如下:
def slot_number(key):
return CRC16(key) & 16383
redis提供了命令 cluster keyslot "key",來計算一個key到底屬於哪個槽
2.獲取這個key的過程
當前節點判斷出key屬於哪個槽後,節點會檢測當前節點的ClusterNode.clusterState.slots數組中的第 i 項,並和ClusterNode.clusterState.myself 做比較:
- 如果不相等:說明該槽爲別的節點負責,然後返回給客戶端一個moven錯誤並把真正負責該槽的節點ip,port,客戶端會更新本地槽和節點的映射關係並重定向到新節點
- 如果相等:上文中我們提到redisCluster 支持水平擴展,即在槽遷移的時候redis也對外提供服務。所以槽由當前節點負責可能出現槽數據遷移一部分的情況。這個時候如果鍵在當前節點沒有查到並不能說明這個鍵在整個集羣是不存在的所以需要在查的時候判斷ClusterNode.clusterState.migrating_slots_to [i] :
- 如果該數組第 i 位爲 null,那說明這key是真的不存在
- 如果不爲null, ClusterNode.clusterState.migrating_slots_to [i] 指向的節點就是正在遷移的節點,如果ClusterNode.clusterState.migrating_slots_to [i] 代表的節點也不存在該key數據 那說明這個key是真的不存在
- 我們知道redisCluster是客戶端負載均衡,所以當前節點得返回給客戶端一個ACK錯誤用來標識下這種情況
八.故障轉移
假設現在有集羣 主節點7000,8000,9000, 從節點7001,8001,9001, 7001 爲7000的從~
一.故障發現
當集羣內某個節點出現問題時,需要通過某種方式識別出節點是否發生了故障,上文中在節點通信的時候我們知道集羣中是用過ping、pong消息實現節點通信的。消息不僅可以傳播各自負責的槽節點,還能夠知道傳播其他狀態:主從狀態,節點故障等。這裏我們主要介紹的是 :主觀下線 ,客觀下線
1.主觀下線
集羣中每個節點都會定期向其他節點發送ping消息,接收節點回復ping消息作爲響應,如果在cluster-node-timeout時間內沒有收到節點回復的pong 。當前節點就把該節點標記爲 主觀下線(pfail),並在當前的ClusterNode.fail_reports結構記錄下記錄一個下線報告,記錄內容主要有當前被標記的下線記錄節點,以及最後上報下線報告的時間
只有一個節點認爲主觀下線並不能準確判斷是否故障,如7000節點判斷9000節點爲下線,8000判斷9000位正常~
2.客觀下線
當某個節點判斷一個節點主觀下線後,相應的節點狀態會隨着ping、pong在集羣內傳播,ping、pong每次都會攜帶集羣內1/10的其他節點信息數據,當接受節點發現消息體中含有主觀下線的節點狀態是,會找到故障節點的ClusterNode.fail_reports結構,保存到下線報告鏈表中,來刷新當前節點對故障節點的認知
當半數以後持有槽的主節點都標記某個節點是主觀下線時,就觸發客觀下線流程
- 爲什麼是半數以上持有槽的主節點?爲了防止網絡分區操作的集羣分裂
3.客觀下線流程
每個下線報告都存在了有效期,每次在嘗試觸發客觀下線時,都會檢查下線報告是否過期,對應過期的下線報告進行刪除
- 有效期爲 cluster-node-timeout *2,主要是爲了防止誤報,或者過了一小段時間後被標記下線的節點又好了~
集羣中的每個節點在收到其他節點的下線報告時,都會嘗試觸發客觀下線
- 計算有效下線報告數量
- 如果大於槽節點總數的一般(不大於直接退出)
- 更新爲客觀下線
- 向集羣廣播下線節點的fail消息
- 收到fail消息的節點,標記故障幾點爲客觀下線
二.故障恢復
故障節點如果爲主節點,則需要在他的從節點中選出一個替換它
1.資質檢查
檢查超時時間是否超過 cluster-node-timeout * cluster-slave-validity-factor,cluster-slave-validity-factor用於從節點的有效因子。默認爲10
2.準備選舉時間
故障選舉爲延遲處理,這裏主要是從節點之間計算優先級,總的來說就是 延遲低的先觸發故障選舉,向持有槽的主節點發現選舉消息
3選舉投票
只有持有槽的主節點纔有投票權限,收到票最多的從節點當選爲主節點
問題:爲什麼只有主節點有投票權限?
- 從節點有投票權限的話 會造成,只有一個從節點也會成功選舉