一、伸縮原理
- Redis集羣提供了靈活的節點擴容和收縮方案。在不影響集羣對外服務的情況下,可以爲集羣添加節點進行擴容也可以下線部分節點進行縮容,如下圖所示:
- 從上圖看出,Redis集羣可以實現對節點的靈活上下線控制。其中原理可抽象爲槽和對應數據在不同節點之間靈活移動。首先來看我們之前搭建的集羣槽和數據與節點的對應關係,如下圖所示:
- 三個主節點分別維護自己負責的槽和對應的數據,如果希望加入1個節點實現集羣擴容時,需要通過相關命令把一部分槽和數據遷移給新節點,如下圖所示:
- 圖中每個節點把一部分槽和數據遷移到新的節點6385,每個節點負責的槽和數據相比之前變少了從而達到了集羣擴容的目的。這裏我們故意忽略了槽和數據在節點之間遷移的細節,目的是想讓讀者重點關注在上層槽和節點分配上來,理解集羣的水平伸縮的上層原理:集羣伸縮=槽和數據在節點之間的移動
- 下面將介紹集羣擴容和收縮的細節
二、集羣擴容
- 擴容是分佈式存儲最常見的需求,Redis集羣擴容操作可分爲如下步驟:
- 準備新節點
- 加入集羣
- 遷移槽和數據
①開啓集羣
- 現在我們先開啓一個集羣,集羣中有6個節點,其中前3個爲主節點,後3個爲複製節點
sudo redis-server /opt/redis/conf/redis-6379.conf sudo redis-server /opt/redis/conf/redis-6380.conf sudo redis-server /opt/redis/conf/redis-6381.conf sudo redis-server /opt/redis/conf/redis-6382.conf sudo redis-server /opt/redis/conf/redis-6383.conf sudo redis-server /opt/redis/conf/redis-6384.conf
- 由於輸入命令的時候複製關係有點不同,可能與上面的圖片不一致,但是都是3對3的複製
- 然後查看集羣信息,可以看到:
- 槽0-5460屬於6379,6383複製6379
- 槽5461-10922屬於6380,6384複製6380
- 槽10923-16383屬於6381,6382複製6381
- 其中的集羣開啓在這裏省略了,詳情可參閱前面的文章:https://blog.csdn.net/qq_41453285/article/details/106451296
②準備新節點
- 下面我們新增兩個新節點,一個爲6385,一個爲6386,其中6385作爲主節點,6386複製6385
- 新節點建議跟集羣內的節點配置保持一致,便於管理統一,準備好配置後啓動兩個節點命令如下:
sudo redis-server /opt/redis/conf/redis-6385.conf sudo redis-server /opt/redis/conf/redis-6386.conf
- 啓動後的新節點作爲孤兒節點運行,並沒有其他節點與之通信,集羣結構如下圖所示:
③加入集羣
- 新節點依然採用cluster meet命令加入到現有集羣中。在集羣內任意節點執行cluster meet命令讓6385和6386節點加入進來,命令如下:
redis-cli -p 6379 cluster meet 127.0.0.1 6385 redis-cli -p 6379 cluster meet 127.0.0.1 6386
- 新節點加入後集羣結構如下圖所示:
- 集羣內新舊節點經過一段時間的ping/pong消息通信之後,所有節點會發現新節點並將它們的狀態保存到本地。例如我們在6380節點上執行cluster nodes命令可以看到新節點信息,如下所示:
redis-cli -p 6380 cluster nodes
- 新節點剛開始都是主節點狀態,但是由於沒有負責的槽,所以不能接受任何讀寫操作。對於新節點的後續操作我們一般有兩種選擇:
- 爲它遷移槽和數據實現擴容
- 作爲其他主節點的從節點負責故障轉移
使用redis-cli --cluster命令實現加入集羣
- 上面我們使用cluster meet命令將兩個新節點加入集羣,我們還可以使用redis-cli --cluster命令來加入新節點,還實現了直接 添加爲從節點的支持,內部同樣採用cluster meet命令實現加入集羣功能
- 命令如下:
- --cluster-slave和--cluster-master-id是可選的,在設置從節點的時候纔會用。如果不指定--cluster-master-id會隨機分配到任意一個主節點
redis-cli --cluster add-node new_host:new_port existing_host:existing_port --cluster-slave --cluster-master-id <arg> # 例如下面將6385加入到6379所屬的集羣中,並且作爲117457eab5071954faab5e81c3170600d5192270的從節點 redis-cli --cluster add-node 127.0.0.1:6385 127.0.0.1:6379 --cluster-slave --cluster-master-id 117457eab5071954faab5e81c3170600d5192270
- 例如對於上面加入集羣的操作,我們還可以採取下面的命令:
# 將6385添加到6379所在的集羣中 redis-cli --cluster add-node 127.0.0.1:6385 127.0.0.1:6379 # 將6386添加到6379所在的集羣中 redis-cli --cluster add-node 127.0.0.1:6386 127.0.0.1:6379
- 備註:正式環境建議使用redis-cli cluster命令加入新節點,該命令內部會執行新節點狀態檢查,如果新節點已經加入其他集羣或者包含數據,則放棄集羣加入操作並打印如下信息:
- 如果我們手動執行cluster meet命令加入已經存在於其他集羣的節點,會造成被加入節點的集羣合併到現有集羣的情況,從而造成數據丟失和錯亂, 後果非常嚴重,線上謹慎操作
④遷移槽和數據
- 上面我們添加了兩個新節點:6385、6386。其中6385作爲主節點存儲數據,6386作爲從節點複製6385。下面我們要把其他節點的槽和數據遷移到6386這個節點中
槽遷移計劃
- 槽是Redis集羣管理數據的基本單位,首先需要爲新節點制定槽的遷移 計劃,確定原有節點的哪些槽需要遷移到新節點。遷移計劃需要確保每個節點負責相似數量的槽,從而保證各節點的數據均勻
- 例如,在集羣中加入 6385節點,如下圖所示,加入6385節點後,原有節點負責的槽數量從6380變爲4096個
- 槽遷移計劃確定後開始逐個把槽內數據從源節點遷移到目標節點,如下圖所示
遷移數據
- 數據遷移過程是逐個槽進行的,每個槽數據遷移的流程如下圖所示。流程說明:
- 1)對目標節點發送cluster setslot {slot} importing {sourceNodeId}命令,讓 目標節點準備導入槽的數據
- 2)對源節點發送cluster setslot {slot} migrating {targetNodeId}命令,讓源 節點準備遷出槽的數據
- 3)源節點循環執行cluster getkeysinslot {slot} {count}命令,獲取count個屬於槽{slot}的鍵
- 4)在源節點上執行migrate {targetIp} {targetPort} "" 0 {timeout} keys {keys...}命令,把獲取的鍵通過流水線(pipeline)機制批量遷移到目標節點,批量 遷移版本的migrate命令在Redis3.0.6以上版本提供,之前的migrate命令只能 單個鍵遷移。對於大量key的場景,批量鍵遷移將極大降低節點之間網絡IO次數
- 5)重複執行步驟3)和步驟4)直到槽下所有的鍵值數據遷移到目標節點
- 6)向集羣內所有主節點發送cluster setslot {slot} node {targetNodeId}命令,通知槽分配給目標節點。爲了保證槽節點映射變更及時傳播,需要遍歷發送給所有主節點更新被遷移的槽指向新節點
- 使用僞代碼模擬遷移過程如下:
def move_slot(source,target,slot): # 目標節點準備導入槽 target.cluster("setslot",slot,"importing",source.nodeId); # 目標節點準備全出槽 source.cluster("setslot",slot,"migrating",target.nodeId); while true : # 批量從源節點獲取鍵 keys = source.cluster("getkeysinslot",slot,pipeline_size); if keys.length == 0: # 鍵列表爲空時,退出循環 break; # 批量遷移鍵到目標節點 source.call("migrate",target.host,target.port,"",0,timeout,"keys",keys); # 向集羣所有主節點通知槽被分配給目標節點 for node in nodes: if node.flag == "slave": continue; node.cluster("setslot",slot,"node",target.nodeId);
- 第一步:我們現在6379所在槽中添加3個鍵,然後使用“cluster keyslot”命令查看這3個鍵屬於哪些槽中,可以看到都在4096這個槽中
set key:test:5028 value:5028 set key:test:68253 value:68253 set key:test:79212 value:79212
- 第二步:在目標節點(6385)上準備導入槽4096的數據,並且通過cluster nodes命令確認一下導入狀態
# 最後的id爲6379的id cluster setslot 4096 importing fe7a63f5ff36e04446d1427ef2a262c940642d3a # 查看一下導入狀態 cluster nodes
- 第三步:源節點(6379)上準備導出4096數據,並且通過cluster nodes命令確認一下導出狀態
# 最後的id爲6385的id cluster setslot 4096 migrating e993fc8bb9e80cac7948e83a190e087bbe58f3aa # 查看一下導入狀態 cluster nodes
- 第四步:源節點(6379)上批量獲取4096對應的鍵,然後使用migrate命令批量遷移出這3個鍵
# 批量獲取4096對應的鍵 cluster getkeysinslot 4096 100 # 遷移之前確認一下三個鍵存在於源節點中 mget key:test:5028 key:test:68253 key:test:79212 # 批量遷移出這3個鍵 migrate 127.0.0.1 6385 "" 0 5000 keys key:test:5028 key:test:68253 key:test:79212
- 出於演示目的,我們繼續查詢這三個鍵,發現已經不在源節點中, Redis返回ASK轉向錯誤,ASK轉向負責引導客戶端找到數據所在的節點, 細節將在後面“請求路由”文章中說明
mget key:test:5028 key:test:68253 key:test:79212
- 第五步:通知所有主節點槽4096指派給目標節點6385
redis-cli -p 6379 cluster setslot 4096 node e993fc8bb9e80cac7948e83a190e087bbe58f3aa redis-cli -p 6380 cluster setslot 4096 node e993fc8bb9e80cac7948e83a190e087bbe58f3aa redis-cli -p 6381 cluster setslot 4096 node e993fc8bb9e80cac7948e83a190e087bbe58f3aa redis-cli -p 6385 cluster setslot 4096 node e993fc8bb9e80cac7948e83a190e087bbe58f3aa
- 第六步:確認源節點6379不再負責槽4096改爲目標節點6385負責
- 此處我們只將6379所屬的槽4096遷移給6385負責,但是其他槽還沒有遷移,下面我們使用reids-lic cluster將剩餘的槽遷移給6385
⑤使用redis-cli cluster進行遷移
- 在④中我們使用了各種命令進行遷移,但是這種遷移每次需要自己手動輸入需要遷移的鍵,如果鍵太多,那麼上面的方法顯然不適合,因此我們還可以使用redis-cli cluster命令進行遷移
- 命令如下:
- host:port:必傳參數,集羣內任意節點地址,用來獲取整個集羣信息
- --from:制定源節點的id,如果有多個源節點,使用逗號分隔,如果是all源節點變爲集羣內所有主節點,在遷移過程中提示用戶輸入
- --to:需要遷移的目標節點的id,目標節點只能填寫一個,在遷移過程 中提示用戶輸入
- --slots:需要遷移槽的總數量,在遷移過程中提示用戶輸入
- --yes:當打印出reshard執行計劃時,是否需要用戶輸入yes確認後再執行reshard
- --timeout:控制每次migrate操作的超時時間,默認爲60000毫秒
- ·--pipeline:控制每次批量遷移鍵的數量,默認爲10
redis-cli --cluster reshard host:port --from <arg> --to <arg> --slots <arg> --yes --timeout <arg> --pipeline <arg>
- reshard命令簡化了數據遷移的工作量,其內部針對每個槽的數據遷移同樣使用之前的流程
遷移數據
- 第一步:在④中已經新節點6395遷移了一個槽4096,剩下的槽數據遷移使用redis-cli cluster完成,命令如下:
redis-cli --cluster reshard 127.0.0.1:6379
- 輸入上面的命令之後會讓你輸入一系列的信息,如下面所示
- 第二步:首先讓你輸入需要遷移的槽數量,此處我們輸入4096
- 第三步:然後讓你輸入目標節點ID,只能指定一個,因爲我們需要遷移到6385中,因此下面輸入6385的ID
- 第四步:之後輸入源節點的ID,redis會從這些源節點中平均取出對應數量的槽,然後遷移到6385中,下面我們分別輸入6379、6380、6381的節點ID。最後要輸入done表示結束
- 第五步:數據遷移之前會打印出所有的槽從源節點到目標節點的計劃,確認計劃無誤後輸入yes執行遷移工作:
- 輸入yes之後會輸入大量的遷移信息,遷移完成之後reshard命令自動退出
- 第六步:我們檢查節點和槽映射的變化,如下如所示,6385負責的槽變爲0-1364、4096、5461-6826、10923-12287
- 第七步:由於槽用於hash運算本身順序沒有意義,因此無須強制要求節點負責槽的順序性。遷移之後建議使用下面的命令檢查節點之間槽的均衡性。命令如下:
redis-cli --cluster rebalance 127.0.0.1:6380
- 通過上圖可以看出遷移之後所有主節點負責的槽數量差異在2%以內,因此集羣節點數據相對均勻,無需調整
⑥添加從節點
- 上面我們把6385、6386節點加入到集羣,節點6385遷移了部分槽和數據作爲主節點,但相比其他主節點目前還沒有從節點,因此該節點不具備故障轉移的能力
- 這時需要把節點6386作爲6385的從節點,從而保證整個集羣的高可用。使用下面的命令爲主節點添加對應從節點,注意在集羣模式下slaveof添加從節點操作不再支持。如下所示:
# 後面的ID爲主節點6385的ID cluster replicate e993fc8bb9e80cac7948e83a190e087bbe58f3aa
- 從節點內部除了對主節點發起全量複製之外,還需要更新本地節點的集羣相關狀態,查看節點6386狀態確認已經變成6385節點的從節點:
- 到此整個集羣擴容完成,集羣關係結構如下圖所示
三、集羣收縮
- 收縮集羣意味着縮減規模,需要從現有集羣中安全下線部分節點。安全下線節點流程如下圖所示
- 流程說明:
- 1)首先需要確定下線節點是否有負責的槽,如果是,需要把槽遷移到 其他節點,保證節點下線後整個集羣槽節點映射的完整性
- 2)當下線節點不再負責槽或者本身是從節點時,就可以通知集羣內其 他節點忘記下線節點,當所有的節點忘記該節點後可以正常關閉
- 下面我們以上面的環境爲基礎,進行下線遷移
①下線遷移槽
- 當前集羣中有4個主節點和4個從節點,現在我們想把6381和6382節點進行下線(其中6381是主節點,6382複製6381)
- 下線節點需要把自己負責的槽遷移到其他節點,原理與之前節點擴容的 遷移槽過程一致
- 第一步:下線之前,先查看一下6381和6382的信息,6381是主節點,負責槽12288-16383,6382是它的從節點
- 收縮正好和擴容遷移方向相反,6381變爲源節點,其他主節點變爲目標節點,源節點需要把自身負責的4096個槽均勻地遷移到其他主節點上。如下圖所示
- 下面使用reshard命令完成槽遷移,由於我們要將6381的槽均勻的分配到其它三個節點中,因此需要輸入3次reshard命令,每次遷移的目標節點ID不同
- 第二步:先遷移到6379節點上,下面輸入命令的步驟與“二”中的步驟一樣,需要輸入很多參數
redis-cli --cluster reshard 127.0.0.1:6381
- 然後輸入yes同意遷移
- 遷移完成之後查看一下信息,6379接管了1365個槽12288-16652
- 第三步:再遷移到6380節點上,步驟與上面一樣,不過需要輸入6380的ID
redis-cli --cluster reshard 127.0.0.1:6381
- 然後輸入yes同意遷移
- 遷移完成之後查看一下信息,6380接管了1365個槽13653-15017
- 第四步:再遷移到6385節點上,步驟與上面一樣,不過需要輸入6385的ID
redis-cli --cluster reshard 127.0.0.1:6381
- 然後輸入yes同意遷移
- 遷移完成之後查看一下信息,6385接管了1366個槽15018-16383
- 通過上圖也可以看出6381節點不再負責任何槽了
②忘記節點
- 由於集羣內的節點不停地通過Gossip消息彼此交換節點狀態,因此需要通過一種健壯的機制讓集羣內所有節點忘記下線的節點。也就是說讓其他節點不再與要下線節點進行Gossip消息交換
- Redis提供了clusteforget {downNodeId}命令實現該功能,如下圖所示:
- 當節點接收到cluster forget {down NodeId}命令後,會把nodeId指定的 點加入到禁用列表中,在禁用列表內的節點不再發送Gossip消息。禁用列表有效期是60秒,超過60秒節點會再次參與消息交換。也就是說當第一次forget命令發出後,我們有60秒的時間讓集羣內的所有節點忘記下線節點
- 線上操作不建議直接使用cluster forget命令下線節點,需要跟大量節點命令交互,實際操作起來過於繁瑣並且容易遺漏forget節點
- 建議使用redis-cli --cluster del-node {host:port} {downNodeId}命令,內部實現的僞代碼如下:
def delnode_cluster_cmd(downNode): # 下線節點不允許包含slots if downNode.slots.length != 0 exit 1 end # 向集羣內節點發送cluster forget for n in nodes: if n.id == downNode.id: # 不能對自己做forget操作 continue; # 如果下線節點有從節點則把從節點指向其他主節點 if n.replicate && n.replicate.nodeId == downNode.id : # 指向擁有最少從節點的主節點 master = get_master_with_least_replicas(); n.cluster("replicate",master.nodeId); #發送忘記節點命令 n.cluster('forget',downNode.id) # 節點關閉 downNode.shutdown();
- 從僞代碼看出del-node命令幫我們實現了安全下線的後續操作。當下線主節點具有從節點時需要把該從節點指向到其他主節點,因此對於主從節點都下線的情況,建議先下線從節點再下線主節點,防止不必要的全量複製。
開始移除節點
- 對於6381和6384節點下線操作,命令如下:
# 後面的爲6381的ID redis-cli --cluster del-node 127.0.0.1:6379 0c02519bbb8e6876e3376c94f9703d2d3d4db36a # 後面的爲6382的ID redis-cli --cluster del-node 127.0.0.1:6379 4fb31d352bc6cdb074c1b04715f7500a615a411e
- 節點下線後確認節點狀態,發現不已經不包含6381和6382這兩個節點了
- 到目前爲止,我們完成了節點的安全下線,新的集羣結構如下所示