redis cluster(2)

集羣伸縮

一、伸縮原理

集羣伸縮 = 槽和數據在節點之間的移動

二、擴容集羣

1.準備新節點(例如,加入6385,6386)

  • 需要是集羣模式 cluster_enabled = yes
  • 配置和其他集羣節點保持一致
  • 啓動後是一個孤兒節點
redis-server conf/redis-6385.conf
redis-server conf/redis-6386.conf

2.加入集羣

  • 方式一:通過meet操作將兩個新節點加入到集羣中
redis-cli -p 6379 cluster meet 127.0.0.1 6385
redis-cli -p 6379 cluster meet 127.0.0.1 6386

6385成爲master的從屬節點
redis-cli -p 6385 cluster replicate node-id
6386端口作爲主節點,加入集羣。

  • 方式二:通過redis-trib.rb工具,將節點加入到集羣
redis-trib.rb add-node newHost:newPort existsHost:existPort --slave --master-id
#
redis-trib.rb add-node 127.0.0.1:6385 127.0.0.1:6379
reids-trib.rb add-node 127.0.0.1:6386 127.0.0.1:6379
  • 在沒有圖形化界面時,建議使用redis-trib.rb能夠避免新節點已加入其它集羣,造成故障
  • 新節點加入到集羣中,有兩類作用
    • 爲它遷移槽和數據實現擴容
    • 作爲從節點負責參與故障轉移

3.遷移槽和數據

基於原生命令

  1. 對目標節點發送:***cluster setslot ${slot} importing ${sourceNodeId}***,讓目標節點準備導入槽的數據。
  2. 對源節點發送:***cluster set ${slot} migrating ${targetNodeId}***,讓源節點準備遷出槽的數據
  3. 源節點循環執行:***cluster getkeysinslot ${slot} ${count}***,每次獲取count個屬於槽的鍵
  4. 在源節點上執行:***migrate ${targetIp} ${targetPort} key 0 ${timeout}***,將指定的key遷移
  5. 重複執行步驟③~步驟④直到槽下所有的數據遷移至目標節點
  6. 向集羣內所有主節點發送***cluster setslot ${slot} node ${targetNodeId}***,通知槽分配給目標節點
    在這裏插入圖片描述
    基於redis-trib.rb
#指定任意一個集羣中的節點即可
redis-trib.rb reshard ${everyHost}:${everyPort}
#首先通過提示輸入需要遷移多少個slot
#然後提示輸入接收這些槽的nodeId
#接着提示輸入源節點的節點ID集合,輸入all則表示所有待遷移節點平均

redis-trib.rb reshard host:port --from ${fromNodeId} --to ${toNodeId} 
	--slots ${slotCount} --yes --timeout --pipeline ${batchSize}
#host:port 	必傳參數,集羣內任意節點地址,用來獲取整個集羣信息
#--from 	源節點ID,當有多個源節點用逗號分隔,如果是all,則源節點爲集合內除目標節點外的所有主節點
#--to		目標節點ID,只能填寫一個
#--slots	需要遷移槽的總數量
#--yes		遷移無需用戶手動確認
#--timeout	每次migrate操作的超時時間,默認爲60000毫秒=60秒
#--pipeline	控制每次批量遷移鍵的數量,默認是10

三、縮容集羣

1.遷移槽到其他節點

#將槽從當前節點遷出可以使用上文中的reshard進行完成
redis-trib.rb reshard ${everyHost}:${everyPort}
redis-trib.rb reshard host:port --from ${fromNodeId} --to ${toNodeId} 
	--slots ${slotCount} 

2.直接下線節點

 redis-trib.rb  del-node 127.0.0.1:7000  要下線的node-id 

幫我們完成了forget命令和shutdown命令
或者通過以下方式

2.通知其他節點忘記下線節點

** #在‘其他節點’上依次執行如下命令,需要在60秒內執行
cluster forget ${downNodeId}

3.關閉節點

客戶端路由

#計算指定key的對應的槽位
cluster keyslot ${key}

一、moved重定向

  1. 客戶端給任一節點發送請求
  2. 節點計算該請求的槽位以及對應的節點
  3. 如果對應的節點是自身,則將執行命令並返回執行結果
  4. 如果對應的節點不是當前節點自身,將會返回一個moved異常:MOVED ${slot} ${targetHost} ${targetPort}
  5. 然後客戶端再重新按照targetHost:targetPort連接另一redis節點,發送請求,跳轉至第②步。

二、ask重定向

  1. ask請求發生在slot遷移的過程中
  2. 客戶端向source發送命令(不存在moved重定向的情況)
  3. source節點回復客戶端一個ASK重定向:ASK ${slot} ${targetHost} ${targetPort}
  4. 客戶端給target節點發送一個Asking請求
  5. 然後緊接着客戶端發送命令給target節點
  6. target節點執行請求並響應結果

三、MOVED VS ASK

  • 兩者都是客戶端的重定向
  • MOVED的場景下,可以確定槽不在當前節點
  • ASK的場景下,槽還在遷移過程中

三、smart客戶端(JedisCluster)

1.smart客戶端原理:追求性能

  1. 從集羣中選一個可運行節點,使用clutser slots初始化槽和節點映射
  2. 將cluster slots結果映射到本地,爲每個節點創建JedisPool
  3. 準備執行命令

[外鏈圖片轉存失敗(img-wqXdDQQt-1563007938392)(F:\workspace\imooc_workspace\carlosfu_workspace\one_redis_workspace\1546503077784.png)]

2.JedisCluster使用方法

Set<HostAndPort> nodeList = new HashSet<>();
nodeList.add(new HostAndPort("127.0.0.1" , 7000) );
nodeList.add(new HostAndPort("127.0.0.1" , 7001) );
nodeList.add(new HostAndPort("127.0.0.1" , 7002) );
nodeList.add(new HostAndPort("127.0.0.1" , 7003) );
nodeList.add(new HostAndPort("127.0.0.1" , 7004) );
nodeList.add(new HostAndPort("127.0.0.1" , 7005) );
JedisPoolConfig poolConfig = new JedisPoolConfig();
int timeout = 30_000;
JedisCluster jedisCluster = new JedisCluster(nodeList , timeout , poolConfig);
jedisCluster.set("hello" , "world");
System.out.println(jedisCluster.get("hello"));

//獲取所有節點的JedisPool
Map<String,JedisPool> jedisPoolMap = jedisCluster.getClusterNodes();

3.使用技巧

  • 單例:內置了所有節點的連接池
  • 無需手動借還連接池

集羣模式下批量操作的實現

mget、mset需要一組key必須在同一個槽下

1.串行GET/SET

  • 含義:對每一個key在for循環中依次執行GET/SET
  • 優點:執行簡單
  • 缺點:n個key就需要n次網絡時間,效率低下

2.根據CRC16&16383的結果做內聚後串行IO

  • 含義:在本地通過CRC16&16383計算出各個key的槽,然後以各個槽做內聚,然後串行依次訪問各個node

3.根據CRC16&16383的結果做內聚後並行IO

  • 含義:在本地通過CRC16&16383計算出各個key的槽,然後以各個槽做內聚,然後並行訪問各個node

4.使用hash_tag的方式

  • 當一個key包含 {} 的時候,就不對整個key做hash,而僅對 {} 包括的字符串做hash。
  • 此時對每個key拼接一個字符串{tag},就可以訪問一個節點,完成O(1)的請求

5.四種方式的比較

方案 優點 缺點 網絡IO
串行mget 編程簡單
少量keys滿足需求
大量keys請求延遲嚴重 O(keys)
串行IO 編程較簡單
少量節點滿足需求
大量node延遲嚴重 O(nodes)
並行IO 利用並行特性
延遲取決於最慢的節點
編程複雜
超時定位問題難
O(max_slow(node))
hash_tag 性能最高 讀寫增加tag維護成本
tag分佈易出現數據傾斜
O(1)

故障轉移

一、故障發現

  • 通過ping、pong消息實現故障發現:不需要sentinel

1.主觀下線(pfail消息)

  • 定義:某個節點認爲另一個節點不可用,“偏見”
  • 主觀下線流程
    • 節點1每秒定時給節點2發送PING消息
    • 當節點2收到PING消息後返回的PONG消息被節點1收到時,節點1會更新與節點2的最後更新時間
    • 否則如果PING/PONG失敗,會觸發通信異常斷開連接
    • 當與節點2最後的通信時間達到或大於***cluster-node-timeout***,則節點1主觀認爲節點2下線。

2.客觀下線(fail消息)

  • 定義:當半數以上持有槽的主節點都標記某節點主觀下線
  • 流程
    • 當每個節點收到其他節點發來的PING消息時,會解析其中是否包含其他節點的主觀下線消息
    • 收到PING的結果會把收到的主觀下線消息存入故障鏈表中
    • 在故障鏈表中,節點可以知道有多少主節點主觀下線
    • 故障鏈表存在有效期,有效期爲***cluster-node-timeout X 2***,防止很久之前的主觀下線消息長久存在於故障鏈表中。
    • 計算有效的下線報告數量,當達到半數以上時,就將節點更新爲客觀下線
    • 並向集羣廣播下線節點的fail消息

3.客觀下線的作用

  • 通知集羣內所有節點標記故障節點爲客觀下線
  • 通知故障節點的從節點觸發故障轉移流程

二、故障恢復

1.資格檢查(對從節點進行資格檢查)

  • 每個從節點檢查與故障主節點的斷線時間
  • 超過cluster-node-timeout * cluster-slave-validity-factor取消資格
  • cluster-slave-validity-factor:默認是10

###2.準備選舉時間

  • 對各個符合資格的從節點按照偏移量進行排序

  • 偏移量越大(與主節點數據越接近)的節點準備選舉時間越小(越早被選舉,後續投票階段可獲得更多票數)

3.選舉投票

  • 由可用的master節點給各個符合資格的slave節點投票
  • 準備選舉時間越小的節點,越早被master節點投票,就更可能得到更多的票數
  • 當投票數達到 (可用master節點數)/2 + 1,可以晉升成爲master節點

4.替換主節點

  1. 當前從節點取消複製變成主節點(slaveof no one)
  2. 執行cluster del slot撤銷故障主節點負責的槽,並執行cluster add slot把這些槽分配給自己
  3. 向集羣廣播自己的pong消息,表明已經替換了故障從節點

三、故障轉移演練

Redis Clutser開發運維常見問題

一、集羣完整性

  • cluster-require-full-converage默認爲yes
  • 完整性要求***集羣中所有節點都是在線狀態***並且***16384個槽都在一個服務的狀態***纔對外提供服務
  • 大多數業務無法容忍,cluster-require-full-coverage建議設置爲no

二、帶寬消耗

  • 官方建議:RedisCluster的節點不要超過1000個

  • 每秒的PING/PONG消息會引起大量的帶寬消耗

  • 消息發送頻率:節點發現與其他節點通信時間超過cluster-node-timeout / 2時會直接發PING消息

  • 消息數據量:slots槽數組(2KB空間)和整個集羣1/10的狀態數據(10個節點狀態數據約1KB)

  • 節點部署的機器規模:集羣分佈的機器越多且每臺機器劃分的節點數越均勻,則集羣內整體的可用帶寬越高

  • 優化方法

    • 避免使用“大”集羣:避免多業務使用一個集羣,大業務可用多集羣
    • cluster-node-timeout:影響帶寬和故障轉移時間,需要權衡
    • 儘量均勻分配到多機器上:保證高可用和帶寬

三、Pub/Sub廣播

  • 問題:publish在集羣每個節點中廣播:加重帶寬壓力
  • 解決方案1:需要使用Pub/Sub時,爲了保證高可用,可以單獨開啓一套Redis Sentinel
  • 解決方案2:使用消息隊列

四、數據傾斜

1.數據傾斜

  • 節點和槽的分配不均勻
    • redis-trib.rb info ip:port 查看節點、槽、鍵值分佈
    • redis-trib.rb rebalance ip:port 進行槽的均衡(謹慎使用)
  • 不同槽對應的鍵值數差異較大
    • CRC16正常情況下比較均勻
    • 可能存在hashKey
    • cluster countkeysnslot ${slot} 獲取槽對應的鍵值個數
  • 包含bigKey(大字符串、幾百萬元素的hash、set、zset、list)
    • 可以在從節點上執行:redis-cli --bigkeys
    • 優化數據結構
  • 內存相關配置不一致

2.請求傾斜(熱點key或者bigKey)

  • 通過優化數據結構避免bigKey
  • 熱點Key不要使用hash_tag
  • 當一致性要求不高時,可以用本地緩存+MQ

五、讀寫分離

  • 只讀連接:集羣模式的從節點不接受任何讀寫請求
    • 重定向到負責槽的主節點
    • readonly命令可以讀:連接級別的命令
  • 讀寫分離:更加複雜
    • 與主從複製有同樣的問題:複製延遲、讀取過期數據、從節點故障
    • 需要實現自己的客戶端:cluster slave ${nodeId}
    • 思路與Redis Sentinel一致

六、數據遷移

  • 官方遷移工具:redis-trib.rb import
    • 只能從單機遷移到集羣
    • 不支持在線遷移:source需要停止寫入
    • 不支持斷點續傳
    • 單線程遷移:影響速度
    • 對source數據進行SCAN,然後進行導入操作
  • 在線遷移
    • 唯品會:redis-migrate-tool
    • 豌豆莢:redis-port
    • 均支持在線遷移,會僞裝成source節點的slave,利用這份更新數據再傳送給target

七、集羣VS單機

  • 集羣限制
    • key批量操作支持有限:例如mget、mset必須在一個slot
    • Key事務和Lua支持有限,操作的Key必須在一個節點
    • Key是數據分區的最小粒度,不支持bigKey分區
    • 不支持多數據庫:集羣模式下只有db 0
    • 複製只支持一層:不支持樹形複製結構
  • 分佈式Redis不一定好
    • Redis Cluster:滿足容量和性能的擴展性,很多業務不需要
      • 不多數客戶端性能會降低
      • 命令無法跨節點使用:mget、mset、scan、flush、sinter
      • Lua和事務無法跨節點使用
      • 客戶端維護更復雜:SDK和應用本身消耗(例如更多的連接池)
    • 很多場景下:Redis Sentinel已經足夠好
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章