12.redis设计与实现学习笔记-集群

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/jun8148/article/details/83028325

16. 集群

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

16.1. 节点

一个Redis 集群通常由多个节点组成,在刚开始的时候,每个节点都是独立的,只处于只包含自己的集群中,当要组成一个真正可工作的集群时,就需要将这些独立的节点连接起来,构建成一个包含多个节点的集群。

如何连接各个节点:

CLUSTER MEET <ip> <port>

向一个节点发送上面的命令,可以让节点与ip和port所指定的节点进行握手,握手成功,节点就会将ip和port指定的节点添加到当前的集群中。

16.1.1. 启动节点

节点的本质还是服务器,服务器会根据 cluster-enabled 配置选项来决定是否开启集群模式。

在这里插入图片描述

16.1.2. 集群数据结构

//一个节点的当前状态
struct clusterNode{    
    // 创建节点的时间    
    mstime_t ctime;    
    // 节点的名字,由40个16进制字符组成    
    char name[REDIS_CLUSTER_NAMELEN];    
    // 节点标识    
    int flags;    
    // 节点当前的配置纪元,用于实现故障转移    
    uint64_t configEpoch;    
    // 节点的ip地址    
    char ip[REDIS_IP_STR_LEN];    
    // 节点的端口号    
    int port;    
    // 保存连接节点所需的有关信息   
    clusterLink *link;    
    //……
};

clusterNode 结构的 link 属性:

typedef struct clusterLink {

    // 连接的创建时间
    mstime_t ctime;

    // TCP 套接字描述符
    int fd;

    // 输出缓冲区,保存着等待发送给其他节点的消息(message)。
    sds sndbuf;

    // 输入缓冲区,保存着从其他节点接收到的消息。
    sds rcvbuf;

    // 与这个连接相关联的节点,如果没有的话就为 NULL
    struct clusterNode *node;

} clusterLink;

每个节点都保存着 clushState 结构,记录当前节点所处的集群目前所处状态,包含多少节点,集群当前配置纪元:

typedef struct clusterState {

    // 指向当前节点的指针
    clusterNode *myself;

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

    // 集群当前的状态:是在线还是下线
    int state;

    // 集群中至少处理着一个槽的节点的数量
    int size;

    // 集群节点名单(包括 myself 节点)
    // 字典的键为节点的名字,字典的值为节点对应的 clusterNode 结构
    dict *nodes;

    // ...
} clusterState;

CLUSTER MEET 命令的实现:

向节点A发送 CLUSTER MEET 命令,能让接收命令的节点A将另一个节点B添加到节点A当前所处的集群里。

收到命令的节点A 和节点B进行握手,以此来确认彼此的存在,并为将来的进一步通信打好基础:

  1. 节点A为节点B创建一个clusterNode结构,并将该结构添加到自己的clusterState.nodes字典中。

  2. 节点A根据ipport发送meet消息给节点B。

  3. 如果一切顺利,节点B收到meet消息,为节点A创建一个clusterNode结构,并将该结构添加到自己的clusterState.nodes字典中。

  4. 如果一切顺利,节点B向节点A发送PONG消息

  5. 如果一切顺利,节点A向节点B返回PING消息

  6. 如果一切顺利,至此,握手完成

在这里插入图片描述

16.2. 槽指派

当数据库中的 16384 个槽都有节点在处理时,集群处于上线状态,否则,处于下线状态。

16.2.1. 记录节点的槽指派信息

// 一个节点的当前状态
struct clusterNode{
    //……
    // 记录处理那些槽
    // 二进制位数组,长度为 2048 个字节,包含 16384 个二进制位
    // 如果slots数组在索引i上的二进制位的值为1,那么表示节点负责处理槽i;否则表示节点不负责处理槽i
    unsigned char slots[16384/8];
    //记录自己负责处理的槽的数量
    int numslots;

    //……
};

16.2.2. 传播节点的槽指派信息

一个节点除了会将自己负责处理的槽记录在clusterNode结构的slots属性和numslots属性之外,它还会将自己的slots数组通过消息发送给集群中其他的节点,以此来告知其他节点自己目前负责处理哪些槽。

当节点A通过消息从节点B那里接收到节点B的slots数组时,节点A会在自己的clusterState.nodes字典中查找接电脑B对应的clusterNode结构,并对结构中的slots数组进行保存或者更新。

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

// 当前节点视角下集群目前所处的状态,集群中所有16384个槽的指派信息
typedef struct clusterState{
    // ……
    // slots[i]指针如果指向NULL,说明槽i尚未被指派给任何节点;
    // slots[i]指针如果指向一个clusterNode 结构,
    // 说明槽i已经被指派给了这个clusterNode结构所代表的节点;
    clusterNode *slots[16384];
    // ……
};

16.2.4. CLUSTER ADDSLOTS 命令的实现

这个命令接受一个或多个槽作为参数,并将所有输入的槽指派给接收该命令的节点负责:

CLUSTER ADDSLOTS <slot> [slot ...]

命令实现的伪代码:

def CLUSTER_ADDSLOTS(*all_input_slots):
    # 遍历所有输出槽,检查他们是否都是未指派槽
    for i in all_input_slots;

        # 如果有任意一个槽已经被指派给了某个节点,那么向客户端返回错误,并终止命令执行
        if clusterState.slots[i] != NULL
            reply_error()
            return;
	# 如果所有输入槽都是未指派槽 
	# 如果通过检查,再一次遍历所有槽,将这些槽指派给当前节点
    for i in all_input_slots;

		# 设置clusterState.slots数组,
		# 将slots[i]的指针指向代表当前节点的clusterNode结构
		clusterState.slots[i] = clusterState.myself
		# 访问当前节点的clusterNode结构的slots数组,将数组在索引i上的二进制位设置位1
		setSlotBit(clusterState.myself.slots,i)

	# 发送消息告知集群中的其他节点,自己目前正在负责处理那些槽

16.3. 在集群中执行命令

在这里插入图片描述

16.3.1. 计算键属于哪个槽

节点用一下算法计算给定键 key 属于哪个槽:

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

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

介绍一个命令:

# 用于查看一个给定键属于哪个槽
CLUSTER KETSLOT <key>

这个命令的伪代码:

def CLUSTER_KEYSLOT(key):
	# 计算槽号
	slot = slot_number(key);
    # 将槽号返回给客户端
    reply_client(slot);

17.3.2. 判断槽是否由当前节点

当节点计算出键所属槽 i 之后,节点会检查自己在 clusterState.slots 数组中的项 i ,判断键所处的槽是否由自己负责:

  • 如果 clusterState.slots[i] 等于 clusterState.myself ,那么说明槽 i 由当前节点负责,节点可以执行客户端发送的命令;
  • 否则,槽 i 不由当前节点负责,节点会根据 clusterState.slots[i] 所指向的 clusterNode 结构所记录的节点IP和端口号,向客户端返回 MOVED 错误并指引客户端转向正在处理槽i的节点。

17.3.3. MOVED 错误

当节点发现键所在的槽并非由自己负责处理的时候,就会向客户端返回一个 MOVED 错误,指引客户端转向正在负责槽的节点。

格式如下:

MOVED <slot> <ip>:<port>

客户端接收到 MOVED 命令之后,根据其提供的IP和端口,转向负责处理槽 slot 的节点,并向节点重新发送之前想要执行的命令。

17.3.4. 节点数据库的实现

节点只能使用 0 号数据库,单机服务器没有限制,但两者都能保存键值对及键值对过期时间,且实现都是一样的。

除了将键值对保存在数据库里面之外,节点还会用 clusterState 结构中的 slots_to_keys 跳跃表来保存槽和键之间的关系:

typedef struct clusterState{
    // ...
    zskiolist *slots_to_keys;
    // ...
} clusterState;

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

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

通过这个跳跃表中记录各个数据库键对应的槽,节点可以很方便对某个或某些槽的所有数据库键进行批量操作。

16.4. 重新分片

Redis集群的重新分片操作可以将任意数量已经指派给某个节点(源节点)的槽改为指派给另一个节点(目标节点),并且相关槽所属的键值对也会从源节点移动到目标节点。 重新分片可以在线进行,在这过程中,集群不用下线,且源节点和目标节点都可以继续处理命令。

重新分片的原理:

redis-trib 负责执行,redis-trib 通过向源节点和目标节点发送命令来进行重新分片。

redis-trib对集群的单个槽 slot 进行重新分片的步骤如下:

  1. redis-trib 对目标节点发送 CLUSTER SETSLOT <slot> IMPORTING <source_id> 命令,让目标节点准备好从源节点导入槽 slot 的键值对

  2. redis-trib 对源节点发送 CLUSTER SETSLOT <slot> MIGRATING <source_id> 命令,让源节点准备好将属于槽 slot的键值对迁移至目标节点

  3. redis-trib 对源节点发送 CLUSTER GETKEYSINSLOT<slot> <count> 命令,获得最多 count 个属于槽 slot 的键值对的键名。

  4. 对于步骤三获得的每个键名,redis-trib 都向源节点发送一个MIGRATE <target_ip> <target_port> <key_name> 0 <timeout> 命令,将被选中的键原子的从源节点迁移至目标节点.

    在这里插入图片描述

  5. 重复步骤3和4,直到源节点保存的所有属于槽slot的键值对都被迁移到目标节点为止。

  6. redis-trib向集群中的任意一个节点发送 CLUSTER SETSLOT <slot> NODE <target_id> 命令,将槽slot指派给目标节点的信息发送给整个集群。

如果重新分片涉及多个槽,那么 redis-trib 将对每个给定的槽分别执行上面给出的步骤。

在这里插入图片描述

16.5. ASK 错误

在重新分片期间,源节点向目标节点迁移一个槽的过程中,可能会出现这样一种情况:属于被迁移槽的一部分键值对保存在源节点里面,而另一部分键值对保存在目标节点中。

在这里插入图片描述

大致原理就是,当客户端向源节点发送关于键key的命令,源节点先在自己的数据库里查找这个键,如果找到就直接返回执行客户端命令,如果没找到,这个键可能已经被迁移到了目标节点,源节点向客户端返回一个 ASK 错误,指引客户端转向正在导入槽的目标节点,并再次发送之前要执行的命令。

16.5.1. CLUSTER SETSLOT IMPORTING 命令的实现

格式:

CLUSTER SETSLOT <slot> IMPORTING <source_id>

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

typedef struct clusterState{
	// ……
    // 如果importing_slots_from[i]的值不为NULL,而是指向一个clusterNode结构,表示当前节点正在从
    // clusterNode所代表的节点导入槽i
    clusterNode *importing_slots_from[16384];
     // ……
}clusterState;

16.5.2. CLUSTER SETSLOT MIGRTING 命令的实现

格式:

CLUSTER SETSLOT <i> MTGRATING <target_id>

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

typedef struct clusterState{
    // ……
    // 如果migrating_slots_to[i]的值不为NULL,而是指向一个clusterNode结构,表示当前节点正在将
    // 槽i迁移至clusterNode所代表的节点
    clusterNode *migrating_slots_to[16384];
    // ……
}clusterState;

16.5.3. ASK 错误

接到ASK 错误的客户端会根据错误提供的IP地址和端口号,转向至正在导入槽的目标节点,然后向目标节点发送一个 ASKING 命令, 之后再重新发送原本想要执行的命令。

16.5.4. ASKING 命令

ASKING命令要做的就是打开发送该命令的客户端的 REDIS_ASKING 标识。如果该客户端的 REDIS_ASKING 标识未打开,直接发送请求,由于槽的迁移过程还未完成,请求的键还属于源节点,此时直接请求目标节点,目标节点会返回一个MOVED错误。 但是,如果节点的 clusterState.importing_slots_from[i] 显示节点正在导入槽 i ,并且发送命令的客户端带有 REDIS_ASKING 标识,那么节点将破例执行这个关于槽 i 的命令一次。

在这里插入图片描述

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

命令的伪代码:

def ASKING():
	# 打开标识
	client.flags |= REDIS_ASKING
	# 向客户端返回OK回复
	reply("OK")

16.5.5. ASK错误和MOVED错误的区别

  • MOVED错误代表槽的负责全已经从一个结点转移到了另一个节点

  • ASK错误只是两个节点在迁移槽的过程中使用的一种临时措施

16.6. 复制与故障转移

16.6.1. 设置从节点

向一个节点发送命令:CLUSTER REPLICATE <node_id>

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

  • 这个节点会先在自己的 clusterState.nodes 字典中找到 node_id 所对应节点的 clusterNode 结构,并将自己的 clusterState.myself.slaveof 指针指向这个结构,以此来记录这个节点正在复制的主节点
  • 然后节点修改自己在 clusterState.myself.flags 中的属性,关闭原本的 REDIS_NODE_MASTER 标识,打开 REDIS_NODE_SLAVE 标识,表示这个节点由原来的主节点变成了从节点
  • 最后,节点调用复制代码,并跟据 clusterState.myself.slaveof 指向的 clusterNode 结构所保存的IP地址和端口号,对主节点进行复制。就是相当于向从节点发送命令 SLAVEOF <master_ip> <maste_port>

16.6.2. 故障检测

集群中的每个节点都会定期地想集群中的其他节点发送 PING 消息,以此来检测对方是否在线,如果接受 PING 消息的节点没有在规定时间内返回 PONG ,那么发送 PING 的节点就会将接受 PING 消息的节点标记为意思下线(PFAIL)。

当一个主节点A通过消息得知主节点B认为主节点C进入疑似下线状态,主节点A会在自己的 clusterState.nodes 字典中找到主节点C所对应的 clusterNode 结构,并将主节点B的下线报告添加到这个结构的 fail_reports 链表里面

struct clusterNode{
    // ...
    // 一个链表,记录了所有其他节点对该节点的下线 报告 
    list *fail_reports;
    // ...
}

每个节点的下线报告由一个 clusterNodeFailReport 结构表示:

struct clusterNodeFailReport{
    // 报告目标节点已经下线的节点
    struct clusterNode *node;
    // 最后一次从node节点收到下线报告的时间
    // 程序使用这个时间戳来检查下线报告是否过期
    // (与电气概念时间相差太久的下线报告会被删除)
    mstime_t time;
}

如果一个集群里,半数以上负责处理槽的主节点都将某个主节点X报告为疑似下线,那么这个主节点X将被标记为已下线(FAIL),将主节点X标记为已下线的节点会向集群广播一条关于主节点X的FAIL消息,所有收到这条FAIL消息的节点都会立即将主节点X标记为已下线。

16.6.3. 故障转移

当一个从节点发现自己正在复制的主节点进入了已下线状态,从节点将开始对下线主节点进行故障转移:

  1. 复制下线主节点的所有从节点里面,会有一个从节点被选中
  2. 被选中的从节点将执行 slaveof no one 命令,成为新的主节点
  3. 新的主节点撤销已下线主节点对指派槽的管理,并将这些槽全部指派给自己
  4. 新的主节点向集群广播一条PONG消息,告诉集群中的其他节点自己成为了新的主节点。
  5. 新的主节点开始接收和自己负责处理的槽有关的命令请求,故障转移完成。

16.6.4. 选举新的主节点

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

16.7. 消息

  1. MEET 消息:当发送者接到客户端发送的 CLUSTER MEET 命令时,发送者会向接收者发送 MEET 消息,请求接收者加入发送者当前所处的集群中。
  2. PING 消息:集群里的每个节点默认每隔一秒钟就会从已知节点列表中随机选出五个节点,然后对这五个节点中最长时间没有发送过 PING 消息的节点发送 PING 消息,以此来检测选中的节点是否在线
  3. PONG 消息:当接收者收到发送者发来的 MEET 消息或者 PING 消息时,为了向发送者确认这条MEET消息或者PING消息已到达,接收者会向发送者返回一条 PONG 消息
  4. FAIL 消息:当一个主节点A判断另一个主节点B已经进入FAIL 状态时,节点A会向集群广播一条关于B的FAIL 消息,所有收到这条消息的节点都会立即将节点B标记为已下线。
  5. PUBLISH 消息:当节点接收到一个 PUBLISH 命令时,节点会执行这个命令,并向集群广播一条PUBLISH 消息,所有接收到这条 PUBLISH 消息的节点都会执行相同的 PUBLISH 消息。

16.7.1. 消息头

节点发送的所有消息都由一个消息头包裹,消息头除了包含消息正文之外,还记录了消息发送者自身的一些消息,因为这些消息也会被消息接收者用到,所有消息头本身也是消息的一部分

每个消息头都由一个 cluster.h/clushterMsg 结构表示:

typedef struct{
    // 消息的长度(包括这个消息头的长度和消息正文的长度)
    unit32_t totlen;
 
    // 消息的类型
    uint16_t type;
 
    // 消息正文包含的节点信息数量,只在发送MEET、PING、PONG这小中Gossip协议消息时使用
    uint16_t count;
    
    // 发送者所处的配置纪元
    uint64_t currentEpoch;
    
    // 如果发送者是一个主节点,那么这里记录的是发送者的配置纪元
    // 如果发送者是一个从节点,那么这里记录的是发送者正在复制的主节点的配置纪元
    uint64_t configEpoch;
 
    // 发送者的名字(ID)
    char sender[REDIS_CLUSTER_NAMELEN];
 
    // 发送者目前的槽指派信息
    unsigned char myslots[REDIS_CLUSTER_SLOTS/8];
 
    // 如果发送者是一个从节点,那么这里记录的是发送者正在复制的主节点的名字
    // 如果发送者是一个主节点,那么这里记录的是REDIS_NONE_NULL_NAME
    // (一个40字节长,值全为0的字节数组)
    char slaveof[REDIS_CLUSTER_NAMELEN];
 
    // 发送者的端口号
    uint16_t port;
 
    // 发送者的标识值
    uint16_t flags;
 
    // 发送者所处集群的状态
    unsigned char state;
 
    // 消息的正文
    union clusterMsgData data;
}clusterMsg;

clusterMsg.data 属性指向联合 clusterh/clusterMsgData,这个联合就是消息正文:

union clusterMsgData{
    // MEET、PING、PONG消息的正文
    struct{
        // 每条MEET、PING、PONG消息都包括两个clusterMsgDataGossip结构
        clusterMsgDataGossip gossip[1];
    }ping;
 
    // FAIL消息的正文
    struct{
        clusterMsgDataFail about;
    }fail;
 
    // PUBLISH消息的正文
    struct{
        clusterMsgDataPublish msg;
    }publish;
 
    // 其他消息的正文……
};

clusterMsg 结构的currentEpochsendermyslots 等属性记录了发送者自身的节点信息。接受者会根据这些信息,在自己的 clusterState.nodes 字典里找到发送者对应的 clusterNode 结构,并对结构进行更新。

16.7.2. MEET 、PING 、PONG 消息的实现

节点通过消息头的 type 判断是三种消息中的哪一种。每次发送 MEETPINGPONG 消息时,发送者都会从自己的已知节点中随机选出两个节点(可以是主节点或者从节点),并将这两个被选中节点的信息分别保存到两个clusterMsgDataGossip中。接收者收到消息,会取出这两个 clusterMsgDataGossip结构 ,并根据其中的信息对自己的 clusterState.nodes 进行更新。

  • 如果被选中节点不存在与接收者的已知节点列表,那么表明接收者第一次接触到被选中节点,接收者将根据结构中的IP地址和端口号等信息,与被选中节点进行握手
  • 如果在列表中,那么说明接收者之前已经与被选中节点进行过接触,接收者将根据 clusterMsgDataFossip结构记录的信息,对被选中节点所对应的 clusterNode 结构进行更新。

clusterMsgDataGossip(记录了被选中节点的名字,发送者和被选中节点最后一次发送和接收 PING 消息和 PONG 消息的时间戳,被选中节点的IP地址和端口号,以及被选中节点的标识值)

custerMsgDataGossip 结构:

typedef struct {
    // 节点的名字
    char nodename[REDIS_CLUSTER_NAMELEN];
    // 最后一次向该节点发送PING消息的时间戳
    uint32_t ping_sent;
    // 最后一次从该节点接收到PONG消息的时间戳
    uint32_t pong_received;
    // 节点的IP地址
    char ip[16];
    // 节点的端口号
    uint16_t port;
    // 节点的标识值
    uint16_t flags;
}clusterMsgDataGossip;

16.7.3. FAIL 消息的实现

在集群的节点数量比较大的情况下,单纯用 Gossip 协议来传播节点的已下线信息会给节点的信息更新带来一定延迟,,而FAIL消息对实时性要求比较高。

FAIL 消息的正文由 cluster.h/clusterMsgDataFail 结构表示,这个节点只包含一个 nodename 属性,改属性记录了已下线节点的名字:

typedef struct {
    // 记录已下线节点的名字(名字是唯一的哦)
    char nodename[REDIS_CLUSTER_NAMELEN];
}clusterMsgDataFail;

16.7.4. PUBLISH 消息的实现

当客户端先集群中的某个节点发送命令

PUBLISH <channel> <message>

的时候,接收到 PUBLISH 命令的节点不仅会向 channel 频道发送消息 message ,它还会想集群广播一条 PUBLISH 消息,所接收这条消息的节点都会向 channel 频道发送 message 消息。

PUBLISH 消息的正文由 cluster.h/clusterMsgDataPublish 结构表示:

typedef struct{
    // 保存了channel参数的长度
    uint32_t channel_len;
    // 保存了message参数的长度
    uint32_t message_len;
    // 定义为 8 字节只是为了对齐其他消息结构
    // 保存了客户单通过PUBLISH命令发送给节点的channel和message参数
    // 0-channel_len-1 字节保存的是channel参数
    // channel_len - channel_len+message_len-1 保存的是message参数
    unsigned char bulk_data[8];
}clusterMsgDataPublish;

猜你喜欢

转载自blog.csdn.net/jun8148/article/details/83028325