第十七章 集群

Redis集群是Redis提供的分布式数据库方案,集群通过分片(sharding)来进行数据共享,并提供复制和故障转移功能

17.1 节点

  一个Redis集群通常由多个节点(node,一个Redis服务器)组成。刚开始每个节点都是一个集群,需要手动指定将其他节点添加到当前集群。

  CLUSTER MEET <ip> <port>

  向一个节点发送以上命令,让目标节点和命令中指定节点进行握手,握手成功则将指定节点添加到当前集群

  17.1.1 启动节点

  Redis服务器在启动时,根据cluster-enabled配置选项是否为yes决定是否开启服务器的集群模式

  17.1.2 集群数据结构

  每个节点使用一个clusterNode结构记录自身的状态,并为集群中的其他节点创建一个相应的clusterNode结构

struct clusterNode{
    //创建节点的时间
    mstime_t ctime;
    //节点的名字,由40个十六进制字符组成
    char name[REDIS_CLUSTER_NAMELEN];

    //节点标识
    //使用各种不同的标识值记录节点的角色(比如主节点或从节点)
    //以及节点目前所处的状态(比如在线或者下线)
    int flags;

    //节点当前的配置纪元,用于实现故障转移
    uint64_t configEpoch;

    //节点的IP地址
    char ip[REDIS_IP_STR_LEN];
    //节点的端口号
    int port;
    //保存连接的节点所需的有关信息
    clusterLink *link;
    //....

}

  clusterNode结构的link属性是一个clusterLink结构,保存了连接节点所需要的信息

typedef struct clusterLink{
    //连接的创建时间
    mstime_t ctime;
    //TCP 套接字描述符
    int fd;
    //输出缓冲区,保存着等待发送给其他节点的消息
    sds sndbuf;
    //输入缓冲区,保存着从其他节点收到的消息
    sds rcvbuf;
    //与这个连接相关联的节点,如果没有的话就为NULL
    struct clusterNode *node;

}clusterLink;

  redisClient中的套接字和缓冲区用于连接客户端,clusterLink中的套接字和缓冲区用于连接其他节点。

  每个节点中还保存了一个clusterState结构,记录了当前节点的视角下,集群目前所处的状态

typedef struct clusterState{
    //指向当前节点的指针
    clusterNode *myself;

    //集群当前的配置纪元,用于实现故障转移
    uint64_t currentEpoch;

    //集群当前的状态,在线或下线
    int state;
    //集群中至少处理着一个槽的节点数量
    int size;
    //集群节点名单(包括myself节点)
    //字典的键位节点的名字,字典的值位节点对应的clusterNode结构
    dict *nodes;

}clusterState;

  17.1.3 CLUSTER MEET命令的实现

  • 节点A收到命令CLUSTER MEET命令,替节点B创建clusterNode结构,并保存在clusterState的nodes字典中。
  • 节点A根据命令中的地址和端口号向节点B发送MEET消息
  • 节点B收到消息后,替节点A创建一个clusterNode结构,存放在自己的clusterState.nodes字典中,并向节点A发送PONG消息
  • 节点A收到PONG消息后,再向节点B发送一条PING消息
  • 节点B收到PING消息,知道节点A已经成功收到PONG消息,握手完成

  节点A会将节点B的信息通过Gossip协议传播给集群中的其他节点,让其他节点也跟节点B进行握手

17.2 槽指派

  Redis集群通过分片的方式保存数据库中的键值对,集群的整个数据库被分成16384个槽(slot),数据库中的每个键属于其中一个槽,集群中的每个节点可以处理0个或至多16384个槽。当数据库中的每个槽都有节点在处理,集群处于上线状态;反之处于下线状态

  为什么是16384个槽?

  1. 为什么不能再大?节点之间发送心跳,消息头其中一项为char[槽数/8],槽越大,消息头越大,占用网络带宽越多。另外消息体

  2. 为什么不能再小?太小会导致每个节点分配的槽不均匀

  

  CLUSTER ADDSLOTS <slot> [slot ...]

  通过向节点发送CLUSTER ADDSLOTS命令,将一个或多个槽指派给节点负责。

  17.2.1 记录节点的槽指派信息

  clusterNode结构的solts属性和numslot属性记录了节点负责处理哪些槽:

struct clusterNode{
    //长度2048个字节,包含16348个二进制位
    //索引i记录了第i个处理槽由当前节点处理
    unsigned char slots[16384/8];
    int numslots;
};

  如图所示,代表当前节点处理槽0至槽7。检查和设置节点的处理槽,复杂度均为O(1)。numslots属性统计了节点负责处理的槽的数量。

  17.2.2 传播节点的槽指派信息

  每个节点会将自己的slots数组通过消息发送给集群中的其他节点,当节点收到其他节点发送的slots数组时,会在自己的clusterState.nodes字典中查找B节点对应的clusterNode结构,更新或保存其中的slots数组

  17.2.3 记录集群所有槽的指派信息

  clusterState结构中的slots数组记录了集群中所有16384个槽的指派信息:

typedef struct clusterState{
    //...
    clusterNode *slots[16384];
    //...

}clusterState; 

  slots每项都是一个指向clusterNode结构的指针,如果指向NULL,则表示尚未分配。

  在clusterState结构中保存slots数组的目的,是为了减少查找某个槽的分配信息的遍历操作,否则需要遍历nodes中保存的每个节点的slots数组,复杂度是O(N)。

  在clusterNode结构中保存slots数组的目的,是为了方便发送某个节点负责的槽信息,不需要遍历

  17.2.4 CLUSTER ADDSLOTS命令的实现

  收到命令后,在clusterState.slots中查找命令中分配的槽是否已被使用,只要有一个槽已被使用,拒绝执行命令,向客户端发送错误。否则先将clusterState.slots对应的槽位分配完毕,再分配clusterNode.slot的中的槽位。分配完后,节点发送消息告知集群中的其他节点,自己目前负责处理的槽。

17.3 在集群中执行命令

  当全部槽分配完毕,集群上线,开始处理命令。

  客户端连接上节点,向节点发送有关键操作的命令,节点会检查键所属的槽是否由自己负责,是则直接执行命令,否则返回客户端MOVED错误,指引客户端转向对应节点,之后由客户端重新发送命令。

  17.3.1 计算键属于哪个槽

def slot_number(key):
    return CRC16(key)&16383

 

  CRC16(key)计算键key的CRC-16校验和,而&16383语句则用于计算处一个介于0和16383之间的整数作为键key的槽号

  17.3.2 判断槽是否由当前节点负责处理

  检查clusterState.slots数组对应槽关联的节点,如果不是当前节点,则访问对应节点的clusterNode结构,根据结构中记录的IP地址和端口号向客户端返回MOVED错误

  MOVED <slot> <ip>:<port>

  17.3.3 MOVED错误

  客户端根据MOVED错误中记录的地址和端口号转向负责的节点,并向该节点重新发送想要执行的命令。转换节点的操作实际是转换发送的套接字,倘若还没有建立连接,则先建立连接。集群模式的客户端不会打印出MOVED错误,而是自动转向。

  17.3.4 节点数据库的实现

  节点只能使用0号数据库,保存键值对和键值对的过期方式跟单机无差别。此外,节点在clusterState结构中的slots_to_keys跳跃表保存槽和键之间的关系:

typedef struct clusterState{
    //...
    zskiplist *slots_to_keys;
    //...

}clusterState;

  slots_to_keys跳跃表的每个节点的分值score都是一个槽号,每个节点的成员object是一个数据库键:

  • 当节点往数据库中添加一个新的键值对时,节点会将这个键和键的槽号关联到slots_to_key跳跃表
  • 当节点删除数据库中的某个键值对时,节点在slots_to_keys跳跃表解除被删除键和槽号的关联

  通过维护这样的跳跃表,可以方便的获取一些统计信息,譬如某个槽下的所有键,只需要遍历整个跳跃表即可

17.4 重新分片

  Redis集群的重新分片可以将任意数量已经指派给某个节点的槽改为指向另一个节点,并且相关槽所属的键值对也会从源节点移动到目标节点

  重新分片操作可以在线进行,源节点和目标节点都可以继续处理命令请求

17.5 ASK错误

   在重新分片的过程中,当客户端向源节点发送键命令时,该键可能已经转移到目标节点,这时源节点会返回一个错误,客户端会转而向目标节点请求

  127.0.0.1:7002>GET "love"

  (error)ASK 16198 127.0.0.1:7003

  跟MOVED错误类似,在集群模式下,收到ASK错误的客户端也不对打印,而是自动根据提供的目标地址和端口进行转向

  17.5.1 CLUSTER SETSLOT IMPORTING 命令的实现

  clusterState结构的importing_stots_from数组记录了当前节点正在从其他节点导入的槽

typedef struct clusterState{
    //...
    clusterNode *importing_slots_from[16384];
    //...

}clusterState;

  如果importing_slot_from[i]的值不为NULL,而是指向一个clusterNode结构,表示正在从clusterNode导入槽i

  对集群重新分片时,向目标节点发送命令:

  CLUSTER SETSLOT <i> IMPORTING <source_id>

  目标节点将importing_slots_from[i]设置为source_id代表的clusterNode结构

  17.5.2 CLUSTER SETSLOT MIGRATING 命令的实现

  clusterState结构的migrating_slots_to数组记录了当前节点正在迁移至其他节点的槽,第i位若指向一个clusterNode结构,则代表该槽正迁移至该clusterNode

typedef struct clusterState{
    //...
    clusterNode *migrating_slots_to[16384];
    //...

}clusterState;

  在对集群进行重新分片时,向源节点发送命令:

  CLUSTER SETSLOT <i> MIGRATING <target_id>

  源节点收到命令后,将其migrating_slots_to[i]设置位target_id代表的clusterNode

  17.5.3 ASK错误(该键已经转到了其他节点)

  一个节点收到键key命令,如果当前key对应的槽恰好分配在自己名下(17.3.2),在数据库中查找该键,如果没有找到,则寻找migrating_slots_to[i]位置是否指向了某个clusterNode,向客户端返回ASK错误。

  客户端收到之后,转向新的节点,先发送一个ASKING命令,之后再重新发送原本想要执行的命令

  17.5.4 ASKING命令

  ASKING命令会打开发送该命令的客户端的REDIS_ASKING标识(标识自己下一个请求是经过其他节点指引过来的,目标节点需要执行)。

  如果客户端向目标节点发送键命令,而键对应的槽为 i 

  • 查询clusterState.slots[i],判断指向的节点是否是自身,如果是直接执行命令,否则继续
  • 查询clusterState.importing_slots_from[i],若不为空,代表正从某个源节点迁移键值对
  • 检查客户端是否带有REDIS_ASKING标识,若有则执行命令,有REDIS_ASKING表示请求的键已经成功迁移到当前节点
  • 否则返回MOVED错误。

  17.5.5 ASK错误和MOVED错误的区别

  遇到MOVED错误后,代表所有关于槽 i 的指令,都已经转向了新的节点,客户端后续的所有关于槽 i 的命令都会转向新节点

  遇到ASK错误,只会在本次请求中,转向新的节点,而后续仍然请求源节点。产生ASK错误,表示正处于重新分片的中间状态,槽 i 的键并非完全迁移完毕

17.6 复制与故障转移

   Redis集群中的节点分为主节点(master)和从节点(slave),其中主节点负责处理槽,从节点复制某个主节点,在主节点下线时,代替主节点。负责主节点负责的槽,而其余从节点也会转向复制新的主节点。故障检测无需Sentinel监控,当某个Master下的Slave宕机,导致该Master没有Slave,集群会自动将其余主节点的Slave迁移到该Master中。

  17.6.1 设置从节点

  向一个节点发送命令,让该节点成为命令中指定的节点的从节点,并开始对主节点进行复制

  CLUSTER REPLICATE <node_id>

  • 接收命令的节点首先在clusterState.nodes字典中找到node_id对应节点的clusterNode结构,并将自己的clusterState.myself.slaveof指针指向这个结构,记录这个节点正在复制这个主节点
struct clusterNode{
    //如果这是一个从节点,那么指向主节点
    strut clusterNode *slaveof;  
    //....
};
  • 修改自己在clusterState.myself.flags属性,关闭原本的REDIS_NODE_MASTER标识,打开REDIS_NODE_SLAVE标识,标识自己成为了从节点
  • 根据clusterState.myself.slaveof指向的clusterNode结构获取主节点的IP地址和端口,对主节点进行复制,相当于SLAVEOF <master_ip> <master_port>

  每个节点在clusterNode结构的slaves属性和numslaves属性中记录这个主节点的从节点名单:

struct clusterNode{
    //正在复制这个主节点的从节点数量
    int numslaves;
    //一个数组
    //每个数组项指向一个正在复制这个主节点的从节点的clusterNode结构
    struct clusterNode **slaves;
}

  17.6.2 故障检测

   集群中每个节点都会定期向集群中其他节点发送PING消息,以期在规定时间内收到回复,来检测对方是否在线。当半数以上的主节点都将主节点x标记为疑似下线,主节点x将被标记为已下线(FAIL),将主节点标记为下线的节点会发送一条广播,所有收到消息的节点都会立即将x标记为已下线

  17.6.3 故障转移

  当某个从节点检测到主节点进入已下线状态,进入故障转移:

  • 复制下线主节点的所有从节点中,选中一个从节点
  • 从节点执行SLAVEOF no one命令,成为新的主节点
  • 新的主节点会撤销所有对已下线主节点的槽指派,将这些槽重新指派给自己
  • 新的主节点向集群广播一条PONG消息,让其余节点知道新旧节点的交替
  • 新的主节点开始处理请求

  17.6.4 选举新的主节点

  跟领头sentinel选举类似,每个发现主节点下线的从节点向集群广播消息,要求选举自己为新的主节点。

 17.7 消息

   集群中的节点通过发送和接收消息进行通信,消息主要有:

  1. MEET消息:邀请某个节点加入到发送者的集群之中
  2. PING消息:集群中的每个节点每秒从已知节点中随机选出5个节点,发送PING消息,为了防止长时间未和某个节点进行通信,有时也不是完全随机的
  3. PONG消息:作为PING消息或者MEET消息的回应,表示已经收到。也用来在子节点成为主节点后发送,以刷新其余节点的认知
  4. FAIL消息:告知其他节点,某个节点已经进入已下线状态
  5. PUBLISH消息:当节点收到一个PUBLISH命令,节点本身执行的同时,会广播一条PUBLISH消息,所有收到消息的节点都将执行相同的PUBLISH命令

18. Redis集群方案

  Redis集群方案应该怎么做?

  知乎Redis集群架构

 

posted @ 2021-03-03 18:01  walker993  阅读(56)  评论(0编辑  收藏  举报