10.1 數(shù)據(jù)分布
10.1.1 數(shù)據(jù)分布理論
-
數(shù)據(jù)分區(qū)示意圖
image.png
- 分區(qū)規(guī)則
分區(qū)方式 | 特點(diǎn) | 代表產(chǎn)品 |
---|---|---|
順序分區(qū) | 離散度易傾斜晦闰;數(shù)據(jù)分布業(yè)務(wù)相關(guān)冕末;可順序訪問 | Bigtable; HBase; Hypertable |
哈希分區(qū) | 離散度好;數(shù)據(jù)分布業(yè)務(wù)相關(guān);無法順序訪問 | Redis Cluster; Cassandra; Dynamo |
-
節(jié)點(diǎn)取余哈希
image.png
-
一致性哈希
image.png
優(yōu)點(diǎn):
- 加入和刪除節(jié)點(diǎn)只影響哈希環(huán)中相鄰的節(jié)點(diǎn)
缺點(diǎn):
- 加減節(jié)點(diǎn)后饮醇,會導(dǎo)致部分原節(jié)點(diǎn)中的數(shù)據(jù)無法命中毕源,需要遷移到新節(jié)點(diǎn)
- 當(dāng)使用少量節(jié)點(diǎn)時浪漠,單個節(jié)點(diǎn)的數(shù)據(jù)量較大,因此加減節(jié)點(diǎn)時影響的數(shù)據(jù)量較大霎褐,數(shù)據(jù)遷移成本較高
- 要保證負(fù)載均衡保持穩(wěn)定址愿,加減節(jié)點(diǎn)需要增加一倍或者減少一半的節(jié)點(diǎn)
- 虛擬槽分區(qū)hash
- 順著一致性哈希的思路,為了解決第二個缺點(diǎn)冻璃,需要盡量增加節(jié)點(diǎn)數(shù)响谓,但要解決第三個缺點(diǎn),又要減少節(jié)點(diǎn)數(shù)俱饿;按照計算機(jī)加層解決問題的思路歌粥,引入了中間概念:虛擬槽
- 虛擬槽分區(qū)巧妙的使用了哈希空間拍埠,使用分散度良好的哈希函數(shù)把所有數(shù)據(jù)映射到一個固定范圍的整數(shù)集合中失驶,整數(shù)定義為槽(slot),Redis Cluster槽范圍是0~16283枣购, 每個節(jié)點(diǎn)負(fù)責(zé)一部分槽嬉探。這樣一來,加減節(jié)點(diǎn)時只需要遷移一部分槽的數(shù)據(jù)棉圈,而依賴于槽的粒度較小的特點(diǎn)涩堤,做到數(shù)據(jù)哈希分散良好較為容易。
10.1.2 Redis數(shù)據(jù)分區(qū)
slot = CRC16(key) & 16383
10.1.3 集群功能限制
- key批量操作支持有限: 如mset, mget, 只支持具有相同slot值的key執(zhí)行批量操作
- key事務(wù)操作支持有限: 同理只支持多key在同一節(jié)點(diǎn)上的事務(wù)操作
- key作為數(shù)據(jù)分區(qū)的最小粒度, 因此不能將一個大的鍵值對象如hash, list映射到不同的節(jié)點(diǎn)
- 不支持多數(shù)據(jù)庫空間
- 復(fù)制結(jié)構(gòu)只支持一層, 從節(jié)點(diǎn)復(fù)制主節(jié)點(diǎn), 不支持嵌套樹狀復(fù)制結(jié)構(gòu)
10.2 搭建集群
10.2.1 節(jié)點(diǎn)準(zhǔn)備
- 目錄
# 建議集群內(nèi)所有節(jié)點(diǎn)統(tǒng)一目錄
conf # 配置
data # 數(shù)據(jù)
log # 日志
- 配置文件redis-6379.conf
# 配置示例
# 節(jié)點(diǎn)端口
port 6379
# 開啟集群模式
cluster-enabled yes
# 節(jié)點(diǎn)超時時間, 單位毫秒
cluster-node-timeout 15000
# 集群內(nèi)部配置文件: 由redis自動維護(hù), 不需要手動修改
cluster-config-file "nodes-6379.conf"
- 啟動節(jié)點(diǎn)
# 啟動所有節(jié)點(diǎn)
redis-server conf/redis-6379.conf
redis-server conf/redis-6380.conf
redis-server conf/redis-6381.conf
redis-server conf/redis-6382.conf
redis-server conf/redis-6383.conf
redis-server conf/redis-6384.conf
> cat data/nodes-6379.conf
cfb28ef1deee4e0fa78da86abe5d24566744411e 127.0.0.1:6379 myself,master - 0 0 0 connected vars currentEpoch 0 lastVoteEpoch 0
127.0.0.1:6380> cluster nodes
8e41673d59c9568aa9d29fb174ce733345b3e8f1 127.0.0.1:6380 myself,master - 0 0 0 connected
這一步完成后, 每個節(jié)點(diǎn)都啟動好了分瘾,但彼此并不知道對方的存在胎围。
10.2.2 節(jié)點(diǎn)握手
- 握手過程采用Gossip協(xié)議, 需要一個過程
-
示意圖
image.png
- 節(jié)點(diǎn)握手過程
6379 --meet--> 6380
6380 --pong--> 6379
6379 --定期ping--> 6380
6380 --定期pong--> 6379
- 命令
127.0.0.1:6379>cluster meet 127.0.0.1 6380
127.0.0.1:6379>cluster meet 127.0.0.1 6381
127.0.0.1:6379>cluster meet 127.0.0.1 6382
127.0.0.1:6379>cluster meet 127.0.0.1 6383
127.0.0.1:6379>cluster meet 127.0.0.1 6384
- 效果
上述命令執(zhí)行完畢一段時間后, 節(jié)點(diǎn)之間通過Gossip協(xié)議互相告知自己所知的節(jié)點(diǎn), 這樣最終形成一個兩兩互聯(lián)的節(jié)點(diǎn)網(wǎng)絡(luò).
10.2.3 槽位分配
- 先將16384個槽位(slot)平均分配給6379,6380,6381三個節(jié)點(diǎn)
redis-cli -h 127.0.0.1 -p 6379 cluster addslots {0...5461}
redis-cli -h 127.0.0.1 -p 6380 cluster addslots {5462...10922}
redis-cli -h 127.0.0.1 -p 6381 cluster addslots {10923...16383}
- 查看集群狀態(tài)
127.0.0.1:6379>cluster info
cluster_state:ok
cluster_slots_assigned:16384
cluster_slots_ok:16384
cluster_slots_pfail:0
cluster_slots_fail:0
cluster_known_nodes:6
cluster_size:3
cluster_current_epoch:5
cluster_my_epoch:0
cluster_stats_messages_sent:4874
cluster_stats_messages_received:4726
- 查看集群內(nèi)節(jié)點(diǎn)信息
127.0.0.1:6379>cluster nodes
- 讓剩余的節(jié)點(diǎn)6382,6383,6384成為從節(jié)點(diǎn)
127.0.0.1:6382>cluster replicate cfb28ef1deee4e0fa78da86abe5d24566744411e
127.0.0.1:6383>cluster replicate 8e41673d59c9568aa9d29fb174ce733345b3e8f1
127.0.0.1:6384>cluster replicate 4041673d59c9568aa9d29fb174ce733345b36746
這樣,我們就手動搭建了一個3主3從, 槽位均分的集群, 還是比較麻煩的德召,節(jié)點(diǎn)多了就需要更先進(jìn)的工具了
10.2.4 用redis-trib.rb搭建集群
redis-trib.rb是一個Ruby實(shí)現(xiàn)的Redis集群管理工具白魂, 內(nèi)部通過Cluster相關(guān)命令幫助我們簡化集群創(chuàng)建、檢查上岗、槽遷移和均衡等常見運(yùn)維操作福荸,使用之前需要安裝Ruby依賴環(huán)境。
- Ruby環(huán)境準(zhǔn)備
wget https://cache.ruby-lang.org/pub/ruby/2.3/ruby-2.3.1.tar.gz
tar xvf ruby-2.3.1.tar.gz
./configure -prefix=/usr/local/ruby
make
make install
cd /usr/local/ruby
sudo cp bin/ruby /usr/local/bin
sudo cp bin/gem /usr/local/bin
wget http://rubygems.org/downloads/redis-3.3.0.gem
gem install -l redis-3.3.0.gem
gem list --check redis gem
sudo cp /{redis_home}/src/redis-trib.rb /usr/local/bin
# redis-trib.rb
- 準(zhǔn)備節(jié)點(diǎn)
redis-server conf/redis-6481.conf
redis-server conf/redis-6482.conf
redis-server conf/redis-6483.conf
redis-server conf/redis-6484.conf
redis-server conf/redis-6485.conf
redis-server conf/redis-6486.conf
- 創(chuàng)建集群
# 每個主節(jié)點(diǎn)的從節(jié)點(diǎn)數(shù)為1, 前面3個為主, 后面3個為從, 按順序配對
redis-trib.rb create --replicas 1 127.0.0.1:6481 127.0.0.1:6482 127.0.0.1:6483 127.0.0.1:6484 127.0.0.1:6485 127.0.0.1:6486
- 集群完整性檢查
# 選擇集群中任意一個節(jié)點(diǎn)進(jìn)行檢查, 即可檢查整個集群的情況
redis-trib.rb check 127.0.0.1:6381
10.3 節(jié)點(diǎn)通信
10.3.1 通信流程
- 通信內(nèi)容: 節(jié)點(diǎn)元數(shù)據(jù)
節(jié)點(diǎn)負(fù)責(zé)哪些數(shù)據(jù)
是否出現(xiàn)故障
通信方式: Gossip協(xié)議
節(jié)點(diǎn)彼此不斷通信交換信息,一段時間后所有的節(jié)點(diǎn)都會知道集群完整的信息敲霍,類似流言傳播-
流程說明
- 每個節(jié)點(diǎn)單獨(dú)開辟一個TCP通道, 用來節(jié)點(diǎn)彼此通信, 端口號在基礎(chǔ)端口號上加10000
- 每個節(jié)點(diǎn)在固定周期內(nèi)通過特定規(guī)則選擇幾個節(jié)點(diǎn)發(fā)送ping消息
- 節(jié)點(diǎn)接收到ping后, 返回pong作為相應(yīng).
10.3.2 Gossip消息
- 分類
ping, pong, meet, fail
-
meet節(jié)點(diǎn)間建立通信
image.png
-
ping/pong節(jié)點(diǎn)間維持通信
image.png
-
判斷其他節(jié)點(diǎn)處于下線狀態(tài), 則發(fā)送fail消息給其他節(jié)點(diǎn)尋求共識
image.png
- 消息頭
typedef struct {
char sig[4]; // 信號標(biāo)識
uint32_t totlen; // 消息總長度
uint16_t ver; // 協(xié)議版本
uint16_t type; // 消息類型(meet,ping,pong,fail)
uint16_t count; // 消息體包含的節(jié)點(diǎn)數(shù)量, 進(jìn)用于meet,ping,pong消息類型
uint64_t currentEpoch; // 當(dāng)前發(fā)送節(jié)點(diǎn)的配置紀(jì)元
uint64_t configEpoch; // 主節(jié)點(diǎn)/從節(jié)點(diǎn)的主節(jié)點(diǎn)的配置紀(jì)元
uint64_offset; // 復(fù)制偏移量
char sender[CLUSTER_NAMELEN]; // 發(fā)送節(jié)點(diǎn)的nodeId
unsigned char myslots[CLUSTER_SLOTS/8]; // 發(fā)送節(jié)點(diǎn)負(fù)責(zé)的槽信息
char slaveof[CLUSTER_NAMELEN]; // 如果發(fā)送節(jié)點(diǎn)是從節(jié)點(diǎn), 記錄對應(yīng)主節(jié)點(diǎn)的nodeId
uint16_t port; // 端口號
uint16_t flags; // 發(fā)送節(jié)點(diǎn)標(biāo)識, 區(qū)分主從角色, 是否下線等
unsigned char state; // 發(fā)送節(jié)點(diǎn)所處的集群狀態(tài)
unsigned char mflags[3]; // 消息標(biāo)識
union clusterMsgData data; // 消息正文
} clusterMsg;
- 消息體
union clusterMsgData {
/* ping,meet,pong消息體 */
struct {
/* gossip消息結(jié)構(gòu)數(shù)組 */
clusterMsgDataGossip gossip[1];
} ping;
/* FAIL消息體 */
struct {
clusterMsgDataFail about;
} fail;
};
typedef struct {
char nodename[CLUSTER_NAMELEN]; /* 節(jié)點(diǎn)的nodeId */
uint32_t ping_sent; /* 最后一次向該節(jié)點(diǎn)發(fā)送ping消息時間 */
uint32_t pong_received; /* 最后一次接收該節(jié)點(diǎn)pong消息時間 */
char ip[NET_IP_STR_LEN]; /* IP */
uint16_t port; /* port */
uint16_t flags; /* 該節(jié)點(diǎn)標(biāo)識 */
} clusterMsgDataGossip;
-
消息解析流程
image.png
10.3.3 節(jié)點(diǎn)選擇
- Gossip天然耗費(fèi)帶寬和計算,需要選擇一部分節(jié)點(diǎn)來降低消耗
-
選擇規(guī)則和通信量
image.png
- 選擇發(fā)送消息的節(jié)點(diǎn)數(shù)量
每個節(jié)點(diǎn)每秒發(fā)送ping消息的數(shù)量: 1 + 10 * num(node.pong_received > cluster_node_timeout / 2)
當(dāng)帶寬資源緊張時, 可以適當(dāng)調(diào)大cluster_node_timeout參數(shù), 比如由默認(rèn)15秒改為30秒
- 消息數(shù)據(jù)量
消息頭主要部分: myslots[CLUSTER_SLOTS/8]: 2KB
消息體: 默認(rèn)包含節(jié)點(diǎn)總量的1/10個節(jié)點(diǎn)的信息, 所以集群越大, 消息體越大
10.4 集群伸縮
10.4.1 伸縮原理
-
槽和對應(yīng)數(shù)據(jù)在不同節(jié)點(diǎn)之間的移動
image.png
10.4.2 擴(kuò)容集群
- 準(zhǔn)備新節(jié)點(diǎn)
redis-server conf/redis-6385.conf
redis-server conf/redis-6386.conf
- 加入集群
redis-trib.rb add-node 127.0.0.1:6385 127.0.0.1:6379
redis-trib.rb add-node 127.0.0.1:6386 127.0.0.1:6379
- 遷移槽和數(shù)據(jù)
# [目標(biāo)節(jié)點(diǎn)]執(zhí)行: 目標(biāo)節(jié)點(diǎn)準(zhǔn)備導(dǎo)入槽
cluster setslot {slot} importing {sourceNodeId}
# [源節(jié)點(diǎn)]執(zhí)行: 源節(jié)點(diǎn)準(zhǔn)備遷出槽的數(shù)據(jù)
cluster setslot {slot} migrating {targetNodeId}
# [源節(jié)點(diǎn)]循環(huán)執(zhí)行: 獲取slot下{count}個鍵
cluster getkeysinslot {slot} {count}
# [源節(jié)點(diǎn)]執(zhí)行: 批量遷移相關(guān)鍵的數(shù)據(jù)
migrate {targetIp} {targetPort} "" 0 {timeout} keys {keys...}
# 向集群內(nèi)所有節(jié)點(diǎn)通知指定槽節(jié)點(diǎn)映射變更結(jié)果
cluster setslot {slot} node {targetNodeId}
例如
127.0.0.1:6385>cluster setslot 4096 importing cfb28eafadsfdsafesdf411e
127.0.0.1:6379>cluster setslot 4096 migrating 1a205dfafsafdfsdfadfb756
127.0.0.1:6379>cluster getkeysinslot 4096 100
127.0.0.1:6379>migrate 127.0.0.1 6385 "" 0 5000 keys key:test:5028 key:test:68253 key:test:79212
127.0.0.1:6379>cluster setslot 4096 node 1a205dfafsafdfsdfadfb756
127.0.0.1:6380>cluster setslot 4096 node 1a205dfafsafdfsdfadfb756
127.0.0.1:6381>cluster setslot 4096 node 1a205dfafsafdfsdfadfb756
127.0.0.1:6385>cluster setslot 4096 node 1a205dfafsafdfsdfadfb756
redis-trib.rb支持
redis-trib.rb reshard host:port --from <arg> --to <arg> --slots <arg> --yes --timeout <arg> --pipeline <arg>
# host:port: 集群內(nèi)任意節(jié)點(diǎn)地址
# --from: 源節(jié)點(diǎn)的id
# --to: 目標(biāo)節(jié)點(diǎn)的id
# --slots: 需要遷移鍵的總數(shù)量
# --yes: 是否需要用戶確認(rèn)才執(zhí)行
# --timeout: migrate操作的超時時間, 默認(rèn)60 000毫秒
# --pipeline: 每次批量遷移鍵的數(shù)量, 默認(rèn)為10
redis-trib.rb reshard 127.0.0.1:6379 --from cfb28eafadsfdsafesdf411e --to 1a205dfafsafdfsdfadfb756 --slots 4096
redis-trib.rb rebalance 127.0.0.1:6380
- 添加從節(jié)點(diǎn)
127.0.0.1:6386>cluster replicate 1a205dfafsafdfsdfadfb756
10.4.3 收縮集群
- 遷移槽
redis-trib.rb reshard 127.0.0.1:6379
- 忘記節(jié)點(diǎn)
redis-trib.rb del-node {host:port} {downNodeId}
10.5 請求路由
10.5.1 請求重定向
-
MOVED重定向流程
image.png
- 槽位計算方式
1. 如果鍵內(nèi)容包含{...}大括號字符, 則計算槽的有效部分是括號內(nèi)的內(nèi)容; 否則采用鍵的全內(nèi)容谷婆。
2. crc16(key, keylen) & 16383
- 槽節(jié)點(diǎn)查找
typedef struct clusterState {
clusterNode *myself;
clusterNode *slots[CLUSTER_SLOTS]; // 16384個槽和節(jié)點(diǎn)映射數(shù)組, 數(shù)組下標(biāo)代表槽位序號
} clusterState;
如果每次都重定向台夺,顯然比較耗費(fèi)帶寬和IO径玖,所以需要客戶端"聰明"一點(diǎn),能記住槽位所對應(yīng)的節(jié)點(diǎn)谒养。
10.5.2 Smart客戶端
- 原理: 客戶端維護(hù)
slot->node
映射
image.png
10.5.3 ASK重定向
- 當(dāng)一個slot數(shù)據(jù)分布在多個節(jié)點(diǎn)時, 客戶端去對應(yīng)的節(jié)點(diǎn)取數(shù)據(jù)如果取不到, 該節(jié)點(diǎn)會返回ASK重定向錯誤:
(error) ASK {slot} {targetIP}:{targetPort}
-
Smark客戶端工作流程
image.png
需要評估m(xù)set,mget,pipeline等批量操作在slot遷移中間過程中的容錯性挺狰,在客戶端寫出容錯代碼。
10.6 故障轉(zhuǎn)移
10.6.1 故障發(fā)現(xiàn)
-
主觀下線
image.png
typedef struct clusterState {
clusterNode *myself; // 自身節(jié)點(diǎn)
dict *nodes; // 當(dāng)前集群內(nèi)所有節(jié)點(diǎn)的字典集合, <節(jié)點(diǎn)ID --> 對應(yīng)節(jié)點(diǎn)的clusterNode結(jié)構(gòu)>
...
} clusterState;
typedef struct clusterNode {
int flags; // 當(dāng)前節(jié)點(diǎn)狀態(tài)(主從角色, 是否下線等)
mstime_t ping_sent; // 最后一次與該節(jié)點(diǎn)發(fā)送ping消息的時間
mstime_t pong_received; // 最后一次接收到該節(jié)點(diǎn)pong消息的時間
...
} clusterNode;
flags的取值:
CLUSTER_NODE_MASTER 1 // 當(dāng)前為主節(jié)點(diǎn)
CLUSTER_NODE_SLAVE 2 // 當(dāng)前為從節(jié)點(diǎn)
CLUSTER_NODE_PFAIL 4 // 主觀下線狀態(tài)
CLUSTER_NODE_FAIL 8 // 客觀下線狀態(tài)
CLUSTER_NODE_MYSELF 16 // 表示自身節(jié)點(diǎn)
CLUSTER_NODE_HANDSHAKE 32 // 握手狀態(tài), 未與其他節(jié)點(diǎn)進(jìn)行消息通信
CLUSTER_NODE_NOADDR 64 // 無地址節(jié)點(diǎn), 用于第一次meet通信未完成或者通信失敗
CLUSTER_NODE_MEET 128 // 需要接受meet消息的節(jié)點(diǎn)狀態(tài)
CLUSTER_NODE_MIGRATE_TO 256 // 該節(jié)點(diǎn)被選中為新的主節(jié)點(diǎn)狀態(tài)
- 客觀下線
struct clusterNode { // 認(rèn)為是主觀下線的clusterNode結(jié)構(gòu)
list *fail_reports; // 記錄了所有其他節(jié)點(diǎn)對該節(jié)點(diǎn)的下線報告
...
}
# 每個節(jié)點(diǎn)clusterNode都存在一個下線鏈表結(jié)構(gòu)买窟,維護(hù)著其他主節(jié)點(diǎn)針對當(dāng)前節(jié)點(diǎn)的下線報告
typedef struct clusterNodeFailReport {
struct clusterNode *node; // 報告該節(jié)點(diǎn)為主觀下線的節(jié)點(diǎn)
mstime_t time; // 最近收到下線報告的時間
} clusterNodeFailReport;
# 更新報告故障的節(jié)點(diǎn)結(jié)構(gòu)和最近收到下線報告的時間
def clusterNodeAddFailureReport(failNode -> clusterNode, senderNode -> clusterNode):
report_list = failNode.fail_reports
for report in report_list:
if senderNode == report.node:
report.time = now()
return 0
report_list.append(clusterNodeFailReport(senderNode, now()))
return 1
# 嘗試觸發(fā)客觀下線時, 刪除過期的下線報告
def clusterNodeCleanupFailureReports(node -> clusterNode):
report_list = node.fail_reports
maxtime = server.cluster_node_timeout * 2
now = now()
for report in report_list:
if now - report.time > maxtime:
report_list.remove(report)
break
cluster_node_timeout設(shè)置的過小時, 主觀下線報告的上傳速度趕不上下線報告過期的速度丰泊, 這樣故障節(jié)點(diǎn)就永遠(yuǎn)無法被客觀下線,從而導(dǎo)致故障轉(zhuǎn)移失敗始绍。需要有針對這種情況的監(jiān)控代碼瞳购,或者調(diào)大cluster_node_timeout值
def markNodeAsFailingIfNeeded(failNode -> clusterNode):
slotNodeSize = getSlotNodeSize()
needed_quorum = (slotNodeSize // 2) + 1
failures = clusterNodeFailureReportsCount(failNode)
if nodeIsMaster(myself):
failures += 1
if failures < needed_quorum:
return
failNode.flags = CLUSTER_NODE_FAIL
failNode.fail_time = mstime()
if nodeIsMaster(myself):
clusterSendFail(failNode)
假如網(wǎng)絡(luò)分區(qū)情況存在,會導(dǎo)致分割后的小集群無法收到大集群的fail消息亏推,從而無法順利完成故障轉(zhuǎn)移学赛,因此部署時需要避免這種情況。
10.6.2 故障恢復(fù)
主節(jié)點(diǎn)發(fā)生故障吞杭,則提升該主節(jié)點(diǎn)的從節(jié)點(diǎn)來替換它盏浇,保證集群的高可用
- 資格檢查
從節(jié)點(diǎn)與故障主節(jié)點(diǎn)的斷線時間 <= cluster_node_timeout * cluster_slave_validity_factor
其中cluster_slave_validity_factor默認(rèn)為10
- 準(zhǔn)備選舉時間
- 延遲觸發(fā)機(jī)制: 復(fù)制偏移量越大的從節(jié)點(diǎn), 具有更高的優(yōu)先級來替換故障主節(jié)點(diǎn)。
struct clusterState {
...
mstime_t failover_auth_time; // 記錄之前或者下次將要執(zhí)行故障選舉時間
int failover_auth_rank; // 記錄當(dāng)前從節(jié)點(diǎn)排名
}
# 計算從節(jié)點(diǎn)優(yōu)先級排名
def clusterGetSlaveRank():
rank = 0
master = myself.slaveof
myoffset = replicationGetSlaveOffset()
for j in range(len(master.slaves)):
if master.slaves[j] != myself and
master.slaves[j].repl_offset > myoffset:
rank += 1
return rank
# 使用優(yōu)先級排名, 更新從節(jié)點(diǎn)選舉觸發(fā)時間
def updateFailoverTime():
server.cluster.failover_auth_time = now() + 500 + random() % 500
rank = clusterGetSlaveRank()
added_delay = rank * 1000
server.cluster.failover_auth_time += added_delay
server.cluster.failover_auth_rank = rank
- 發(fā)起選舉
配置紀(jì)元
- 每個主節(jié)點(diǎn)有個配置紀(jì)元cluster_my_epoch
- 集群的全局配置紀(jì)元是所有主節(jié)點(diǎn)紀(jì)元的最大值cluster_current_epoch
- 從節(jié)點(diǎn)復(fù)制主節(jié)點(diǎn)的配置紀(jì)元
- 紀(jì)元同步靠ping/pong消息傳遞
- 發(fā)送主節(jié)點(diǎn)和接收主節(jié)點(diǎn)的配置紀(jì)元相等時,若發(fā)送方nodeId大于接收方, 則接收方紀(jì)元自增1
def clusterHandleConfigEpochCollision(sender -> clusterNode):
if sender.configEpoch != myself.configEpoch \
or not nodeIsMaster(sender)
or not nodeIsMaster(myself):
return
if sender.nodeId <= myself.nodeId:
return
server.cluster.currentEpoch += 1
myself.configEpoch = server.cluster.currentEpoch
配置紀(jì)元的應(yīng)用場景
- 新節(jié)點(diǎn)加入
- 槽節(jié)點(diǎn)映射沖突檢測
- 從節(jié)點(diǎn)投票選舉沖突檢測
發(fā)起選舉的操作步驟
- 自增集群的全局配置紀(jì)元, 單獨(dú)保存在clusterState.failover_auth_epoch
- 在集群內(nèi)廣播選舉消息(FAILOVER_AUTH_REQUEST)
- 選舉投票
投票資格
- 是主節(jié)點(diǎn)
- 持有數(shù)據(jù)槽
- 沒有投票過
當(dāng)選條件
- 收集到N/2+1張選票
投票作廢
- 開始投票后, 超過cluster_node_timeout * 2時間內(nèi)沒有從節(jié)點(diǎn)當(dāng)選, 則本次選舉作廢芽狗。
- 替換主節(jié)點(diǎn)
- 當(dāng)前從節(jié)點(diǎn)取消復(fù)制绢掰,變?yōu)橹鞴?jié)點(diǎn)
- 執(zhí)行clusterDelSlot操作撤銷故障主節(jié)點(diǎn)負(fù)責(zé)的槽,并執(zhí)行clusterAddSlot操作把這些槽委派給自己
- 向集群廣播自己的pong消息, 通知集群內(nèi)所有的節(jié)點(diǎn)當(dāng)前從節(jié)點(diǎn)變?yōu)橹鞴?jié)點(diǎn)并接管了故障主節(jié)點(diǎn)的槽信息童擎。
10.6.3 故障轉(zhuǎn)移時間
- 主觀下線(pfail)識別時間 = cluster_node_timeout
- 主觀下線狀態(tài)消息傳播時間 <= cluster_node_timeout / 2
- 從節(jié)點(diǎn)轉(zhuǎn)移時間 <= 1000毫秒
failover_time(毫秒) <= cluster_node_timeout + cluster_node_timeout / 2 + 1000
其中, cluster_node_timeout默認(rèn)15秒滴劲。
10.6.4 故障轉(zhuǎn)移演練
# 查看集群內(nèi)節(jié)點(diǎn)信息
cluster nodes
# 關(guān)閉6385節(jié)點(diǎn)進(jìn)程
ps -ef | grep redis-server | grep 6385
kill -9 6385的pid
# 從節(jié)點(diǎn)6386和主節(jié)點(diǎn)6385之間復(fù)制中斷
redis-6386.log
Connecting to MASTER 127.0.0.1:6385
MASTER <-> SLAVE sync started
Error Condition on socket for SYNC: Connection refused
# 主觀下線和客觀下線
redis-6380.log
Marking node 6385的nodeId as failing (quorum reached)
redis-6379.log
Marking node 6385的nodeId as failing (quorum reached)
# 從節(jié)點(diǎn)選舉
redis-6386.log
Start of election delayed for 964 milliseconds (rank #0, offset 1822).
Starting a failover election for epoch 17.
redis-6380.log
Failover auth granted to 6386的nodeId for epoch 17
redis-6379.log
Failover auth granted to 6386的nodeId for epoch 17
redis-6386.log
Failover election won: I'm the new master.
configEpoch set to 17 after successful failover.
10.7 集群運(yùn)維
10.7.1 集群完整性
- 所有的16384個槽都指派到了節(jié)點(diǎn)
- 通過cluster_require_full_coverage=no來控制主節(jié)點(diǎn)故障后,只影響該主節(jié)點(diǎn)內(nèi)槽位數(shù)據(jù)的相關(guān)命令執(zhí)行顾复,不干擾其他節(jié)點(diǎn)
10.7.2 帶寬消耗
- 集群節(jié)點(diǎn)數(shù)限制在1000以內(nèi)
- 消息發(fā)送頻率班挖, 受cluster_node_timeout控制
- 消息數(shù)據(jù)量,主要部分:slots槽數(shù)組(2KB)+集群1/10的狀態(tài)數(shù)據(jù)(10個節(jié)點(diǎn)1KB)
- 同樣節(jié)點(diǎn)數(shù)芯砸,機(jī)器越多可用帶寬越高
- 200節(jié)點(diǎn), 10節(jié)點(diǎn)/機(jī)器, cluster_node_timeout=15秒, ping/pong消息占用帶寬25Mb萧芙,如果clusterNode_timeout=30秒, 帶寬消耗降低到15Mb以下。
對策
- 避免大集群
- 適度提高cluster_node_timeout降低消息發(fā)送頻率
- 盡量均勻部署在更多機(jī)器上
10.7.3 Pub/Sub廣播問題
節(jié)點(diǎn)一多假丧,廣播風(fēng)暴會造成帶寬負(fù)擔(dān)加重末购。
對策
- 使用sentinel結(jié)構(gòu)而非集群來規(guī)避Pub/Sub廣播風(fēng)暴對帶寬的壓力。
10.7.4 集群傾斜
常見情況
- 數(shù)據(jù)傾斜
- 節(jié)點(diǎn)和槽分配不均
redis-trib.rb info 127.0.0.1:6379
redis-trib.rb rebalance 127.0.0.1:6379
- 不同槽對應(yīng)鍵數(shù)量差異過大
cluster countkeysinslot {slot}
cluster getkeysinslot {slot} {count}
- 集合對象包含大量元素
redis-cli --bigkeys
- 內(nèi)存相關(guān)配置不一致
查看hash_max_ziplist_value, set_max_intset_entries
- 請求傾斜
- 合理設(shè)計鍵, 熱點(diǎn)大集合進(jìn)行拆分
- 不實(shí)用熱鍵作為hash_tag, 避免映射到同一槽
- 實(shí)用本地緩存減少熱鍵調(diào)用
10.7.5 集群讀寫分離
- 只讀連接
client list
get key:test:3130 # 收到MOVED返回
readonly # 打開當(dāng)前連接為只讀狀態(tài)
client list
get key:test:3130 # 從節(jié)點(diǎn)響應(yīng)read命令
- 讀寫分離
cluster slaves {主節(jié)點(diǎn)的nodeId}
客戶端所做修改
- 維護(hù)每個主節(jié)點(diǎn)可用從節(jié)點(diǎn)列表
- 針對讀命令維護(hù)請求節(jié)點(diǎn)路由
- 從節(jié)點(diǎn)新建連接開啟readonly狀態(tài)
成本較高虎谢,不建議集群使用讀寫分離。
10.7.6 手動故障轉(zhuǎn)移
-
流程
image.png 場景
- 主節(jié)點(diǎn)遷移
- 強(qiáng)制故障轉(zhuǎn)移
10.7.7 數(shù)據(jù)遷移
redis-trib.rb import host:port --from <arg> --copy --replace
本章重點(diǎn)回顧
- Redis集群數(shù)據(jù)分區(qū)規(guī)則采用虛擬槽方式曹质,所有的鍵映射到16384個槽中婴噩,每個節(jié)點(diǎn)負(fù)責(zé)一部分槽和相關(guān)數(shù)據(jù)擎场,實(shí)現(xiàn)數(shù)據(jù)和請求的負(fù)載均衡。
- 搭建集群劃分三個步驟:準(zhǔn)備節(jié)點(diǎn)几莽、節(jié)點(diǎn)握手迅办、分配槽≌买迹可以使用redis-trib.rb create命令快速搭建集群站欺。
- 集群內(nèi)部節(jié)點(diǎn)通信采用Gossip協(xié)議彼此發(fā)送消息,消息類型分為:ping消息纤垂、pong消息矾策、meet消息、fail消息等峭沦。節(jié)點(diǎn)定期不斷發(fā)送和接受ping/pong消息來維護(hù)更新集群的狀態(tài)贾虽。消息內(nèi)容包括節(jié)點(diǎn)自身數(shù)據(jù)和部分其他節(jié)點(diǎn)的狀態(tài)數(shù)據(jù)。
- 集群伸縮通過在節(jié)點(diǎn)之間移動槽和相關(guān)數(shù)據(jù)實(shí)現(xiàn)吼鱼。擴(kuò)容時根據(jù)槽遷移計劃把槽從源節(jié)點(diǎn)遷移到目標(biāo)節(jié)點(diǎn)蓬豁,源節(jié)點(diǎn)負(fù)責(zé)的槽相比之前變少從而達(dá)到集群擴(kuò)容的目的,收縮時如果下線的節(jié)點(diǎn)有負(fù)責(zé)的槽需要遷移到其他節(jié)點(diǎn)菇肃,再通過cluster forget命令讓集群內(nèi)其他節(jié)點(diǎn)忘記被下線節(jié)點(diǎn)地粪。
- 使用Smart客戶端操作集群達(dá)到通信效率最大化,客戶端內(nèi)部負(fù)責(zé)計算維護(hù)鍵->槽->節(jié)點(diǎn)的映射琐谤,用于快速定位鍵命令到目標(biāo)節(jié)點(diǎn)蟆技。集群協(xié)議通過Smart客戶端全面高效的支持需要一個過程,用戶在選擇Smart客戶端時建議review下集群交互代碼如:異常判定和重試邏輯笑跛,更新槽的并發(fā)控制等付魔。節(jié)點(diǎn)接收到鍵命令時會判斷相關(guān)的槽是否由自身節(jié)點(diǎn)負(fù)責(zé),如果不是則返回重定向信息飞蹂。重定向分為MOVED和ASK几苍,ASK說明集群正在進(jìn)行槽數(shù)據(jù)遷移,客戶端只在本次請求中做臨時重定向陈哑,不會更新本地槽緩存妻坝。MOVED重定向說明槽已經(jīng)明確分配到另一個節(jié)點(diǎn),客戶端需要更新槽節(jié)點(diǎn)緩存惊窖。
- 集群自動故障轉(zhuǎn)移過程分為故障發(fā)現(xiàn)和故障恢復(fù)刽宪。節(jié)點(diǎn)下線分為主觀下線和客觀下線,當(dāng)超過半數(shù)主節(jié)點(diǎn)認(rèn)為故障節(jié)點(diǎn)為主觀下線時標(biāo)記它為客觀下線狀態(tài)界酒。從節(jié)點(diǎn)負(fù)責(zé)對客觀下線的主節(jié)點(diǎn)觸發(fā)故障恢復(fù)流程圣拄,保證集群的可用性。
- 開發(fā)和運(yùn)維集群過程中常見問題包括:超大規(guī)模集群帶寬消耗毁欣,pub/sub廣播問題庇谆,集群節(jié)點(diǎn)傾斜問題岳掐,手動故障轉(zhuǎn)移,在線遷移數(shù)據(jù)等饭耳。