标签:cluster ken 多个 自身 定时任务 服务器 val 响应 全局
一个节点就是一个运行在集群模式下的Redis服务器,Redis服务器在启动时会根据cluster-enabled配置选项是否为yes来决定是否开启服务器的集群模式。
运行在集群模式下的 Redis 服务器会继续使用所有在单机模式中使用的服务器组件:
那些只有在集群模式下才会用到的数据,节点将它们保存到了cluster.h/clusterNode结构、cluster.h/clusterLink结构,以及cluster.h/clusterState结构里。
(1)clusterState
每个节点都保存有一个clusterState结构,记录了在当前节点的视角下,集群目前所处的状态。
typedef struct clusterState {
// 指向当前节点的指针
clusterNode *myself;
// 集群当前的配置纪元,用于实现故障转移
uint64_t currentEpoch;
// 集群当前的状态:是在线还是下线
int state;
// 集群中至少处理着一个槽的节点的数量
int size;
// 集群目前包含的节点(包括 myself 节点),字典的键为节点的名字,字典的值为节点对应的 clusterNode 结构
dict *nodes;
// ...
} clusterState;
(2)clusterNode
每个节点都会使用一个 clusterNode 结构来记录自己的状态,并为集群中的所有其他节点(包括主节点和从节点)都创建一个相应的 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;
// ...
};
(3)clusterLink
clusterNode 结构的 link 属性是一个 clusterLink 结构,该结构保存了连接节点所需的有关信息。
typedef struct clusterLink {
// 连接的创建时间
mstime_t ctime;
// TCP 套接字描述符
int fd;
// 输出缓冲区,保存着等待发送给其他节点的消息(message)。
sds sndbuf;
// 输入缓冲区,保存着从其他节点接收到的消息。
sds rcvbuf;
// 与这个连接相关联的节点,如果没有的话就为 NULL
struct clusterNode *node;
} clusterLink;
redisClient 结构和 clusterLink 结构的区别:
redisClient 结构和 clusterLink 结构都有自己的套接字描述符和输入、输出缓冲区。redisClient 结构中的套接字和缓冲区是用于连接客户端的,而 clusterLink 结构中的套接字和缓冲区则是用于连接节点的。
向节点 A 发送 CLUSTER MEET <ip> <port> 命令,将另一个节点 B 添加到节点 A 当前所在的集群里面,收到命令的节点 A 将与节点 B 进行握手,以此来确认彼此的存在:
Redis集群采用P2P的Gossip(流言)协议,Gossip协议工作原理就是节点彼此不断通信交换信息,一段时间后所有的节点都会知道集群完整的信息。
(1)通信过程
集群中每个节点通过一定规则挑选要通信的节点,每个节点可能知道全部节点,也可能仅知道部分节点,只要这些节点彼此可以正常通信,最终它们会达到一致的状态。
(2)Gossip消息
(3)由于内部需要频繁地进行节点信息交换,而ping/pong消息会携带当前节点和部分其他节点的状态数据,势必会加重带宽和计算的负担。Redis集群内节点通信采用固定频率(定时任务每秒执行10次),集群内每个节点维护定时任务默认每秒执行10次,每秒会随机选取5个节点找出最久没有通信的节点发送ping消息,用于保证Gossip信息交换的随机性。
集群的整个数据库被分为16384个槽,数据库中的每个键都属于这16384个槽的其中一个,集群中的每个节点可以处理0个或最多16384个槽。如果数据库中有任何一个槽没有得到处理,那么集群处于下线状态。
(1)clusterNode.slots
struct clusterNode{
//...
//Redis以0为起始索引,16383为终止索引,对slots数组中的16384个二进制位进行编号,索引i上为1表示节点负责处理槽i,为0表示节点不负责处理槽i。长度为16384/8=2048个字节。
//取出和设置slots数组中的任意一个二进制位的值的复杂度为O(1)
unsigned char slots[16384/8];
//记录节点负责处理的槽的数量,即slots数组中值为1的二进制位的数量
int numslots;
//...
};
clusterNode结构的slots和numslots属性记录某个节点所负责的槽。
(2)clusterState.slots
typedef struct clusterState{
//...
//记录了集群中所有16384个槽的指派信息,每个数组项都是一个指向clusterNode结构的指针,若指向NULL表示槽i尚未指派给任何节点
clusterNode *slots[16384];
//...
} clusterState;
每个节点还会通过clusterState的slots属性记录集群所有槽的指派信息。
一个节点除了会将自己负责处理的槽记录在clusterNode结构的slots和numslots属性外,还会将自己的slots数组通过消息发送给集群中的其他节点,每个接收到信息的节点会在自己的clusterState.nodes中找到发送方对应的clusterNode并保存slots信息,因此集群中的每个节点都会知道数据库中的16384个槽分别被指派给了集群中的哪些节点。
在对数据库中的16384个槽都进行了指派之后,集群就会进入上线状态,这时客户端就可以向集群中的节点发送数据命令了。
集群中的节点只能使用0号数据库,节点会用clusterState结构中的slots_to_keys来保存槽和键之间的关系。
当客户端向节点发送与数据库键有关的命令时,接收命令的节点会计算出命令要处理的键属于哪个槽:
Redis集群的重新分片操作可以将任意数量已经指派给某个节点(源节点)的槽改为指派给另一个节点(目标节点),并且相关槽所属的键值对也会从源节点被移动到目标节点。由redis-trib执行,redis-trib对集群的单个槽slot进行重新分片的步骤如下:
当客户端向源节点发送一个与数据库键有关的命令,并且命令要处理的数据库键恰好正在被迁移时,会向客户端返回一个ASK错误。
1、Redis集群相对单机在功能上存在一些限制:
2、hash_tag
如果键内容包含{和}大括号字符,则计算槽的有效部分是括号内的内容;否则采用键的全内容计算槽。其中键内部使用大括号包含的内容又叫做hash_tag,它提供不同的键可以具备相同slot的功能,常用于Redis IO优化。
redis集群自身实现了高可用,当集群内少量节点出现故障时通过自动故障转移保证集群可以正常对外提供服务。作为一个完整的集群,每个负责处理槽的节点应该具有从节点,保证当它出现故障时可以自动进行故障转移。首次启动的节点和被分配槽的节点都是主节点,从节点负责复制主节点槽信息和相关的数据。
故障发现也是通过消息传播机制实现的,主要环节包括:
(1)主观下线(pfail)。
(2)客观下线(fail)
struct clusterNode { /* 认为是主观下线的clusterNode结构 */
list *fail_reports; /* 记录了所有其他节点对该节点的下线报告 */
...
};
只有负责槽的主节点参与故障发现决策,因为集群模式下只有处理槽的主节点才负责读写请求和集群槽等关键信息维护,而从节点只进行主机诶单数据和状态信息的复制。
故障节点变为客观下线后,如果下线节点是持有槽的主节点则需要在它的从节点中选出一个替换它,从而保证集群的高可用。下线主节点的所有从节点承担故障恢复的义务,当从节点通过内部定时任务发现自身复制的主节点进入客观下线时,将会触发故障恢复流程:
(1) 资格检查
每个从节点都要检查最后与主节点断线时间,判断是否有资格替换故障的主节点。如果从节点与主节点断线时间超过cluster-node-time * cluster-slave-validity-factor,则当前从节点不具备故障转移资格。
(2)准备选举时间
当从节点符合故障转移资格后,更新触发故障选举的时间,只有到达该时间后才能执行后续流程。复制偏移量越大说明从节点延迟越低,那么它应该具有更高的优先级来替换故障主节点。这里之所以采用延迟触发机制,主要是通过对多个从节点使用不同的延迟选举时间来支持优先级问题。
struct clusterState {
...
mstime_t failover_auth_time; /* 记录之前或者下次将要执行故障选举时间 */
int failover_auth_rank; /* 记录当前从节点排名 */
}
(3)发起选举
当从节点定时任务检测到达故障选举时间(failover_auth_time)到达后,发起选举流程如下:会先更新配置纪元,再在集群内广播选举消息,并记录已发送过消息的状态,保证该从节点在一个配置纪元内只能发起一次选举。
配置纪元的主要作用:
配置纪元的应用场景有:新节点加入、槽节点映射冲突检测、从节点投票选举冲突检测。
(4)选举投票
只有持有槽的主节点才会处理故障选举消息,因为每个持有槽的节点在一个配置纪元内都有唯一的一张选票,当接到第一个请求投票的从节点消息时回复FAILOVER_AUTH_ACK消息作为投票,之后相同配置纪元内其他从节点的选举消息将忽略。当从节点收集到N/2+1个持有槽的主节点投票时,从节点可以执行替换主机点操作。
Redis集群没有直接使用从节点进行领导者选举,主要因为从节点数必须大于等于3个才能保证凑够N/2+1个节点,将导致从节点资源浪费。使用集群内所有持有槽的主节点进行领导者选举,即使只有一个从节点也可以完成选举过程。
投票作废:每个配置纪元代表了一次选举周期,如果在开始投票之后的cluster-node-timeout * 2时间内从节点没有获取足够数量的投票,则本次选举作废。从节点对配置纪元自增并发起下一轮投票,直到选举成功为止。
(5)替换主节点
当从节点收集到足够的选票之后,触发替换主节点操作:
(6)故障转移时间预估
当节点发现与其他节点最后通信时间超过cluster-node-timeout/2时会直接发送ping消息,适当提高cluster-node-timeout可以降低消息发送频率,但同时cluster-node-timeout还影响故障转移的速度,因此需要根据自身业务场景兼顾二者的平衡。
把整个数据集按照分区规则划分到多个节点上,有3种规则:节点取余分区、一致性哈希分区、虚拟槽分区。Redis Cluster采用的是虚拟槽分区,将3种规则进行对比一下。
1、节点取余分区
使用特定的数据,如Redis的键或用户ID,再根据节点数量N使用公式:hash(key)%N 来决定数据映射到哪一个节点上。这种方案存在一个问题:当节点数量变化时,如扩容或收缩节点,数据节点映射关系需要重新计算,会导致数据的重新迁移。
2、一致性哈希分区
实现思路是为系统中每个节点分配一个token,范围一般在0~2^32,这些token构成一个哈希环。数据读写执行节点查找操作时,先根据key计算hash值,然后顺时针找到第一个大于等于该哈希值的token节点。这种方式相比节点取余最大的好处在于加入和删除节点只影响哈希环中相邻的节点,对其他节点无影响。但一致性哈希分区存在几个问题:
3、虚拟槽分区
虚拟槽分区巧妙地使用了哈希空间,使用分散度良好的哈希函数把所有数据映射到一个固定范围的整数集合中,整数定义为槽(slot)。这个范围一般远远大于节点数,比如Redis Cluster槽范围是0~16383。槽是集群内数据管理和迁移的基本单位。采用大范围槽的主要目的是为了方便数据拆分和集群扩展,每个节点会负责一定数量的槽。
标签:cluster ken 多个 自身 定时任务 服务器 val 响应 全局
原文地址:https://www.cnblogs.com/zjxiang/p/12484474.html