文章目录

  • 节点
  • 槽指派
  • 在集群中执行命令
  • 重新分片
  • 分片过程
  • ASK错误
  • ASKING命令
  • 复制与故障转移
  • 设置从节点
  • 故障检测
  • 故障转移
  • 选举新的主节点
  • 消息
  • 消息种类
  • 消息组成


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

节点

Redis集群中有多个节点组成,节点之间通过CLUSTER MEET <ip> <port>将其他节点添加到自己的集群中。

运行在集群模式下的Redis服务器节点会继续使用所有在单机模式中使用的服务器组件,还会使用clusterNode结构、clusterLink结构,clusterState结构来记录集群信息。

每个节点都会使用一个clusterNode结构来记录自己的状态,核心属性如下:

struct clusterNode {
    //创建节点的时间
    mstime_t ctime;
 
    //节点的名字,由40 个十六进制字符组成
    //例如68eef66df23420a5862208ef5b1a7005b806f2ff
    char name[REDIS_CLUSTER_NAMELEN];
 
    //节点标识
    //使用各种不同的标识值记录节点的角色(比如主节点或者从节点),
    //以及节点目前所处的状态(比如在线或者下线)。
    int flags;
 
    //节点当前的配置纪元,用于实现故障转移
    uint64_t configEpoch;
 
    //节点的IP 地址
    char ip[REDIS_IP_STR_LEN];
 
    //节点的端口号
    int port;
 
    //保存连接节点所需的有关信息
    clusterLink *link;
    // ...
};

clusterNode 中的clusterLink 属性如下:

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

每个节点都保存着一个clusterState结构,clusterState下有clusterNode结构:

typedef struct clusterState {
    //指向当前节点的指针
    clusterNode *myself;
 
    //集群当前的配置纪元,用于实现故障转移
    uint64_t currentEpoch;
 
    //集群当前的状态:是在线还是下线
    int state;
 
    //集群中至少处理着一个槽的节点的数量
    int size;
 
    //集群节点名单(包括myself 节点)
    //字典的键为节点的名字,字典的值为节点对应的clusterNode 结构
    dict *nodes;
    // ...
} clusterState;

整体结构图如下:

redis分片与集群 redis分片集群故障转移_客户端


redis分片与集群 redis分片集群故障转移_redis分片与集群_02


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

槽指派

Redis集群采用分片方式来保存数据库中的键值对,集群的数据库划分16384个槽,当所有槽都有节点在处理的时候,集群处于上线状态,否则处于下线状态。

可以通过命令查看集群信息

CLUSTER INFO

可以使用命令进行槽指派。

CLUSTER ADDSLOTS  <slot> [slot]

clusterNode结构的slots属性和numslot属性记录了节点的槽指派信息。

struct clusterNode{
	//包含16384个位,代表根据16384个槽,二进制位的值来判断节点是否负责处理槽i,为1则代表处理,为0代表不处理
	unsigned char slots[16384/8]; 
	//记录节点负责处理的槽的数量
	int numslots;   
}

节点还会通过消息发送给集群中的其他节点,以此来告知其他节点自己目前负责处理哪些槽。每个接收到slots数组的节点都会将数组保存到相应节点的clusterNode结构里面

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

struct clusterState {
	//slots数组包含16384个项,每个数组项都是一个指向clusterNode结构的指针:指向NUL代表为指派,指向一个clusterNode结构,表示槽i已经被指派给了clusterNode结构所代表的节点。
	clusterNode *slots[REDIS_CLUSTER_SLOTS];
}

结构图如下:

redis分片与集群 redis分片集群故障转移_客户端_03


clusterState.slots数组记录了集群中所有槽的指派信息,而clusterNode.slots数组只记录了clusterNode结构所代表的节点的槽指派信息,这是两个slots数组的关键区别所在。

clusterState.slots数组记录了集群中所有槽的指派信息,但使用clusterNode结构的slots数组来记录单个节点的槽指派信息仍然是有必要的:当程序需要将某个节点的槽指派信息通过消息发送给其他节点时,程序只需要将相应节点的clusterNode.slots数组整个发送出去就可以了,否则还需要遍历clusterState.slots数组。

CLUSTER ADDSLOTS命令的实现

redis分片与集群 redis分片集群故障转移_客户端_04

在集群中执行命令

客户端向集群发送命令执行过程:

  • 如果键所在的槽正好就指派给了当前节点,那么节点直接执行这个命令。
  • 如果键所在的槽并没有指派给当前节点,那么节点会向客户端返回一个MOVED错误,指引客户端转向(redirect)至正确的节点,并再次发送之前想要执行的命令。

redis分片与集群 redis分片集群故障转移_redis分片与集群_05

节点首先计算出来键所属的槽i之后,节点就会检查自己在ClusterState.slots数组中的项i,判断键所在的槽是否自己负责。

  • 如果是ClusterState.slots[i]==ClusterState.myself,那么是当前节点负责,执行命令
  • 如果ClusterState.slots[i]!=ClusterState.myself,那么非当前节点负责,根据ClusterState.slots[i]的指向,向客户端返回MOVE错误,指向新节点。

MOVED错误:当节点发现键所在的槽不是自己处理的时候,会返回一个MOVED错误。

MOVED <slot> <ip>:<port>

节点和单机服务器在数据库方面的一个区别是,节点只能使用0号数据库,而单机Redis服务器则没有这一限制。

节点还会用ClusterState结构中的slots_to_keys跳跃表来保存槽与键之间的关系。

重新分片

指的是可以将已经指派的某个节点的槽指派给另外一个节点,同时键值跟随移动。

分片过程

重新分片可以在线进行,由集群管理软件redis-trib负责向源节点和目标节点发送命令,Redis本身提供了重新分片的所有命令。

  • 目标节点准备导入键值对
  • 源节点准备迁移键值对
  • 向源节点统计属于迁移槽的所有键值名
  • 根据键值名向每个源节点都发送一个迁移的键值
  • 迁移完成之后,将槽指派给目标节点。

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

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

clusterState结构的migrating_slots_to数组记录了当前节点正在迁移至其他节点的槽:

struct clusterState{
	//...
	clusterNode *importing_slots_to[16384];
	//...
}

ASK错误

重新分片期间,会存在这样一种情况,槽的一部分数据在源节点,一部分在目标节点,这样情况下命令的执行逻辑如下:

redis分片与集群 redis分片集群故障转移_Redis_06


接收到ASK错误的客户端会根据ASK提供的IP地址和端口号,转向目标节点,发送ASKING命令,之后再发送原本要执行的命令。

ASKING命令

ASKING命令唯一要做的就是打开发送该命令的客户端的REDIS_ASKING。

在一般情况下,如果客户端向节点发送一个关于槽i的命令,而槽i又没有指派给这个节点的话,那么节点将向客户端返回一个MOVED错误;但是,如果节点的clusterState.importing_slots_from[i]显示节点正在导入槽i,并且发送命令的客户端带有REDIS_ASKING标识,那么节点将破例执行这个关于槽i的命令一次。

redis分片与集群 redis分片集群故障转移_Redis_07

客户端的REDIS_ASKING标识是一个一次性标识,当节点执行了一个带有REDIS_ASKING标识的客户端发送的命令之后,客户端的REDIS_ASKING标识就会被移除。

ASK错误和MOVED错误的区别:

  • 相同点:都会导致客户端转向
  • 区别:MOVED错误代表槽从一个节点转向另一个节点,ASK错误只是两个节点在迁移槽的过程中使用的一种临时性措施。

复制与故障转移

Redis分为主节点和从节点,主节点负责处理槽,从节点负责复制某个主节点,主节点发生故障的时候,从节点负责替换主节点。

设置从节点

CLUSTER REPLICATE <node_id>

可以让接受命令的节点成为node_id所指定的从节点,并开始对主节点进行复制。

故障检测

集群中的每个节点都会定期地向及集群中的其他节点发送PING消息,以检测对方是否在线,如果在规定时间内没有返回PONG消息,那么发送PING消息的节点就会将接收PING消息的节点标记为疑似下线(PFAIL)。如果半数以上负责处理槽的主节点将某个节点X报告为疑似下线,那么这个主节点将被标记为已下线。

故障转移

转移过程

  • 从节点中选中一个一个节点,执行SLAVEOF no one,称为新节点。
  • 新节点撤销所有已下线主节点的槽指派,并将槽指派给自己
  • 新的节点向集群广播一条PONG消息,告知其他节点自己的信息变更
  • 新的节点开始接受和自己负责处理槽有关的命令请求

选举新的主节点

  • 当集群里的某个节点开始一次故障转移操作时,集群配置纪元的值会被增一
  • 每个配置纪元,每个主节点都有一次投票机会,第一个向主节点要求投票的从节点将获得主节点的投票,即先到先得。
  • 当从节点发现自己正在复制的主节点进入已下线状态时,从节点会向集群广播一条CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST消息,要求所有收到这条消息、并且具有投票权的主节点向这个从节点投票。
  • 如果一个主节点具有投票权(它正在负责处理槽),并且这个主节点尚未投票给其他从节点,那么主节点将向要求投票的从节点返回一条CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK消息,表示这个主节点支持从节点成为新的主节点。
  • 每个参与选举的从节点都会接收CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK消息,并根据自己收到了多少条这种消息来统计自己获得了多少主节点的支持。
  • 如果集群里有N个具有投票权的主节点,那么当一个从节点收集到大于等于N/2+1张支持票时,这个从节点就会当选为新的主节点。
  • 如果在一个配置纪元里面没有从节点能收集到足够多的支持票,那么集群进入一个新的配置纪元,并再次进行选举,直到选出新的主节点为止。

基于Raft算法的领头选举(leader election)方法来实现的。

消息

消息种类

  • MEET消息:当发送者接收到客户端发送的CLUSTER MEET命令时,发送者向接收者发送MEET消息,请求接收者加入到发送者当前所处的集群里面。
  • PING消息:集群里每个节点默认每隔1s就会从已知节点列表中随机选出五个节点,选择最长时间没有发过PING消息的节点发送PING消息,以此来检测节点是否在线。还有超过cluster-node-timeout一半时,节点A也会向节点B发送PING消息,避免随机选中节点的遗漏问题。
  • PONG消息:对于MEET消息和PING消息,接收端向发送者恢复一条确认到达的PONG消息,或者通过PONG消息告知其他节点自己的信息变化。
  • FAIL消息:当主节点A判断另一个主节点B进入FAIL状态时,节点A会广播一条主节点B FAIL消息,收到消息的节点将B标为已下线。
  • PUBLISH消息:当节点接收到一个PUBLISH命令时,节点会自动执行这个命令,并向集群中广播一条PUBLISH消息,所有接收到这条消息的节点都会执行相同的PUBLISH命令。

消息组成

消息由消息头和消息正文组成。