第10章 集群

image.png

image.png

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
  • 一致性哈希


    image.png

優(yōu)點(diǎn):

  1. 加入和刪除節(jié)點(diǎn)只影響哈希環(huán)中相鄰的節(jié)點(diǎn)

缺點(diǎn):

  1. 加減節(jié)點(diǎn)后饮醇,會導(dǎo)致部分原節(jié)點(diǎn)中的數(shù)據(jù)無法命中毕源,需要遷移到新節(jié)點(diǎn)
  2. 當(dāng)使用少量節(jié)點(diǎn)時浪漠,單個節(jié)點(diǎn)的數(shù)據(jù)量較大,因此加減節(jié)點(diǎn)時影響的數(shù)據(jù)量較大霎褐,數(shù)據(jù)遷移成本較高
  3. 要保證負(fù)載均衡保持穩(wěn)定址愿,加減節(jié)點(diǎn)需要增加一倍或者減少一半的節(jié)點(diǎn)
  • 虛擬槽分區(qū)hash
  1. 順著一致性哈希的思路,為了解決第二個缺點(diǎn)冻璃,需要盡量增加節(jié)點(diǎn)數(shù)响谓,但要解決第三個缺點(diǎn),又要減少節(jié)點(diǎn)數(shù)俱饿;按照計算機(jī)加層解決問題的思路歌粥,引入了中間概念:虛擬槽
  2. 虛擬槽分區(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
image.png

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
image.png
> 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 收縮集群

image.png
  • 遷移槽
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)的下線報告
    ...
}
image.png
# 每個節(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值

image.png
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)來替換它盏浇,保證集群的高可用


image.png
  1. 資格檢查
從節(jié)點(diǎn)與故障主節(jié)點(diǎn)的斷線時間 <= cluster_node_timeout * cluster_slave_validity_factor
其中cluster_slave_validity_factor默認(rèn)為10
  1. 準(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
  1. 發(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)
  1. 選舉投票

投票資格

  • 是主節(jié)點(diǎn)
  • 持有數(shù)據(jù)槽
  • 沒有投票過

當(dāng)選條件

  • 收集到N/2+1張選票

投票作廢

  • 開始投票后, 超過cluster_node_timeout * 2時間內(nèi)沒有從節(jié)點(diǎn)當(dāng)選, 則本次選舉作廢芽狗。
  1. 替換主節(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廣播問題

image.png

節(jié)點(diǎn)一多假丧,廣播風(fēng)暴會造成帶寬負(fù)擔(dān)加重末购。

對策

  • 使用sentinel結(jié)構(gòu)而非集群來規(guī)避Pub/Sub廣播風(fēng)暴對帶寬的壓力。

10.7.4 集群傾斜

常見情況

  1. 數(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
  1. 請求傾斜
  • 合理設(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)移

  1. 流程


    image.png
  2. 場景

  • 主節(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ù)等饭耳。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末串述,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子寞肖,更是在濱河造成了極大的恐慌纲酗,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,734評論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件新蟆,死亡現(xiàn)場離奇詭異觅赊,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)栅葡,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,931評論 3 394
  • 文/潘曉璐 我一進(jìn)店門茉兰,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人欣簇,你說我怎么就攤上這事规脸。” “怎么了熊咽?”我有些...
    開封第一講書人閱讀 164,133評論 0 354
  • 文/不壞的土叔 我叫張陵莫鸭,是天一觀的道長。 經(jīng)常有香客問我横殴,道長被因,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,532評論 1 293
  • 正文 為了忘掉前任衫仑,我火速辦了婚禮梨与,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘文狱。我一直安慰自己粥鞋,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,585評論 6 392
  • 文/花漫 我一把揭開白布瞄崇。 她就那樣靜靜地躺著呻粹,像睡著了一般。 火紅的嫁衣襯著肌膚如雪苏研。 梳的紋絲不亂的頭發(fā)上等浊,一...
    開封第一講書人閱讀 51,462評論 1 302
  • 那天,我揣著相機(jī)與錄音摹蘑,去河邊找鬼筹燕。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的庄萎。 我是一名探鬼主播踪少,決...
    沈念sama閱讀 40,262評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼糠涛!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起兼犯,我...
    開封第一講書人閱讀 39,153評論 0 276
  • 序言:老撾萬榮一對情侶失蹤忍捡,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后切黔,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體砸脊,經(jīng)...
    沈念sama閱讀 45,587評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,792評論 3 336
  • 正文 我和宋清朗相戀三年纬霞,在試婚紗的時候發(fā)現(xiàn)自己被綠了凌埂。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,919評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡诗芜,死狀恐怖瞳抓,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情伏恐,我是刑警寧澤孩哑,帶...
    沈念sama閱讀 35,635評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站翠桦,受9級特大地震影響横蜒,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜销凑,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,237評論 3 329
  • 文/蒙蒙 一丛晌、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧斗幼,春花似錦澎蛛、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,855評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至渠羞,卻和暖如春斤贰,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背次询。 一陣腳步聲響...
    開封第一講書人閱讀 32,983評論 1 269
  • 我被黑心中介騙來泰國打工荧恍, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 48,048評論 3 370
  • 正文 我出身青樓送巡,卻偏偏與公主長得像摹菠,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子骗爆,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,864評論 2 354

推薦閱讀更多精彩內(nèi)容

  • 1. 數(shù)據(jù)分布 1.1 數(shù)據(jù)分布理論 哈希分區(qū):離散度好次氨,數(shù)據(jù)分布業(yè)務(wù)無關(guān) 順序分區(qū):離散度易傾斜,數(shù)據(jù)分布業(yè)務(wù)相...
    leon4ever閱讀 331評論 0 0
  • 集群通過分片來進(jìn)行數(shù)據(jù)共享摘投,并提供復(fù)制和故障轉(zhuǎn)移 節(jié)點(diǎn) 1.一個集群有多個節(jié)點(diǎn) 2.節(jié)點(diǎn)代表的是master或者s...
    簡書徐小耳閱讀 560評論 0 0
  • 故障轉(zhuǎn)移 Redis集群自身實(shí)現(xiàn)了高可用犀呼。高可用首先需要解決集群部分失敗的場景:當(dāng)集群內(nèi)少量節(jié)點(diǎn)出現(xiàn)故障時通過自動...
    linuxzw閱讀 613評論 0 2
  • 集群運(yùn)維 Redis集群由于自身的分布式特性外臂,相比單機(jī)場景在開發(fā)和運(yùn)維方面存在一些差異坐儿。本節(jié)我們關(guān)注于常見問題進(jìn)行...
    linuxzw閱讀 1,039評論 0 2
  • Share猿,一個極客共同交流學(xué)習(xí)的社區(qū)宋光! 關(guān)鍵詞 cluster 去中心化 節(jié)點(diǎn)槽位 網(wǎng)絡(luò)抖動 CRC16算法(...
    Share猿閱讀 931評論 0 1