集羣伸縮
一、伸縮原理
集羣伸縮 = 槽和數據在節點之間的移動
二、擴容集羣
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.遷移槽和數據
基於原生命令
- 對目標節點發送:***cluster setslot ${slot} importing ${sourceNodeId}***,讓目標節點準備導入槽的數據。
- 對源節點發送:***cluster set ${slot} migrating ${targetNodeId}***,讓源節點準備遷出槽的數據
- 源節點循環執行:***cluster getkeysinslot ${slot} ${count}***,每次獲取count個屬於槽的鍵
- 在源節點上執行:***migrate ${targetIp} ${targetPort} key 0 ${timeout}***,將指定的key遷移
- 重複執行步驟③~步驟④直到槽下所有的數據遷移至目標節點
- 向集羣內所有主節點發送***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重定向
- 客戶端給任一節點發送請求
- 節點計算該請求的槽位以及對應的節點
- 如果對應的節點是自身,則將執行命令並返回執行結果
- 如果對應的節點不是當前節點自身,將會返回一個moved異常:MOVED ${slot} ${targetHost} ${targetPort}
- 然後客戶端再重新按照targetHost:targetPort連接另一redis節點,發送請求,跳轉至第②步。
二、ask重定向
- ask請求發生在slot遷移的過程中
- 客戶端向source發送命令(不存在moved重定向的情況)
- source節點回復客戶端一個ASK重定向:ASK ${slot} ${targetHost} ${targetPort}
- 客戶端給target節點發送一個Asking請求
- 然後緊接着客戶端發送命令給target節點
- target節點執行請求並響應結果
三、MOVED VS ASK
- 兩者都是客戶端的重定向
- MOVED的場景下,可以確定槽不在當前節點
- ASK的場景下,槽還在遷移過程中
三、smart客戶端(JedisCluster)
1.smart客戶端原理:追求性能
- 從集羣中選一個可運行節點,使用clutser slots初始化槽和節點映射
- 將cluster slots結果映射到本地,爲每個節點創建JedisPool
- 準備執行命令
[外鏈圖片轉存失敗(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.替換主節點
- 當前從節點取消複製變成主節點(slaveof no one)
- 執行cluster del slot撤銷故障主節點負責的槽,並執行cluster add slot把這些槽分配給自己
- 向集羣廣播自己的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已經足夠好
- Redis Cluster:滿足容量和性能的擴展性,很多業務不需要