目录:
- 集群是什么?有什么用途?
- 集群的环境如何搭建?
- 集群的功能如何实现?
- 集群中的消息类型?
- 关于集群的命令?
- 集群相关的结构体?
1.集群是什么?有什么用途?
- Redis集群是Redis提供的分布式数据库方案(adistributed implementation of Redis),将数据库分成 16384 个槽(slot)分派到集群中多个主机之上,并支持自动故障转移。
- 集群模式的出现是为了解决单机Redis容量有限的问题(Sentinel哨兵模式虽然提供了高可用性,但仍然是单主节点提供写操作,海量数据场景下存在性能瓶颈)。
2.集群的环境如何搭建?
方法一: 自动创建cluster:
https://blog.youkuaiyun.com/u012062455/article/details/87280467
Step 1: 修改redis.conf
需要先修改 redis.conf
配置文件中的配置项:
cluster-enabled yes
Redis服务器在启动时会根据cluster-enabled配置选项是否为yes来决定是否开启服务器的集群模式。
Step 2 : 逐一启动各节点
单独启动各个节点(可使用脚本批量启动):
./redis-server redis.conf
Step 3 : redis-cli创建集群
使用 redis-cli
创建 cluster集群,命令如下:
./redis-cli --cluster create 127.0.0.1:9001 127.0.0.1:9002 127.0.0.1:9003 127.0.0.1:9004 127.0.0.1:9005 127.0.0.1:9006 --cluster-replicas 1
其中,--cluster-replicas 1
选项表示每个主节点配备一个从节点;
redis-cli
会按照给定的顺序设置主节点和从节点,例如当有6个节点时,前3个会被默认设置成主节点
(9001,9002,9003),后三个是从节点(9004,9005,9006)。
(使用 ./redis-cli --cluster help
可查看cluster支持的操作)
方法二: 手动创建cluster:
https://blog.youkuaiyun.com/u012062455/article/details/87258490
CLUSTER MEET
: 将一个节点添加到集群中
127.0.0.1:9007> CLUSTER MEET 127.0.0.1:9001
OK
使9007这一节点加入到9001节点所在的集群之中;
CLUSTER ADDSLOTS
: 手动指派槽
127.0.0.1:9007> CLUSTER ADDSLOTS 0 1 2 3 4 5
OK
将 0 1 2 3 4 5 号槽指派给9007这个节点。
3. 集群的功能如何实现?
集群的两个主要功能:
(1)高性能:多主节点(可扩展高达1000个节点)
(2)高可用性:故障转移(主从复制、故障检测、故障转移)
为实现高性能和高可用性,集群的具体实现包括:
3.1 集群节点维护;
3.2 槽指派(分片);
3.3 处理客户端请求命令(重定向);
3.4 主从复制;
3.5 故障检测;
3.6 故障转移。
3.1 集群中的节点:
集群的拓扑结构是网状结构,在一个共有N个节点的集群中,每个节点会与其余的(N-1)个节点都建
立TCP连接并且是长连接,因此,每个节点都会创建 N个 clusterNode
结构体,用于维护集群中所有节点的状态信息。
同时,每个节点还需另外维护N个 clusterLink
结构体用于表示与某节点的TCP连接信息;
维护一个 cluserState
结构体用于表示集群的状态信息。
(1) clusterNode: 表示集群中的某一个节点
struct clusterNode {
mstime_t ctime; //节点创建时间
char name[REDIS_CLUSTER_NAMELEN]; //节点ID(40位十六进制符)
int flags; //节点标识(例如主/从、在线/下线)
uint64_t configEpoch; //节点当前纪元(用于故障转移)
char ip[REDIS_IP_STR_LEN]; //节点IP地址
int port; //节点端口号
clusterLink *link; //节点连接信息(如是本节点则=NULL)
unsigned char slots[16384/8]; //用16384个bit位表示16384个槽
int numbers; //本节点负责处理的槽数量
};
(2) clusterLink: 表示与某节点的TCP连接信息
typedef struct clusterLink {
mstime_t ctime; //TCP连接的创建时间
int fd; //套接字描述符(connfd)
sds sndbuf; //发送缓冲区(待发送到其他节点的数据)
sds rcvbuf; //接收缓冲区(从其他节点接收到的数据)
struct clusterNode *node; //与此clusterLink关联的节点
} clusterLink;
(3) clusterState: 表示集群的状态信息
typedef struct clusterState {
clusterNode *myself; //指向自己
uint64_t currentEpoch; //集群当前配置纪元(用于故障转移)
int state; //集群状态(ok/fail)
int size; //集群中至少处理着一个槽的节点的数量
dict *nodes; //集群节点名单
clusterNode *slots[16384]; //记录16384个槽中每个槽被分配到哪个节点上
} clusterState;
3.2 槽指派:
Redis将整个数据库分成了 16384 个槽,并将它们分配到集群中的每个主节点上去,从而解决了单机Redis容量有限的问题。
关于槽信息的维护:
主节点会将自身所负责的槽保存在
struct clusterNode {
unsigned char slots[16384/8];
};
slots[ ]数组之中,并会将本节点的槽指派信息发送给集群中的其他主节点;
其他主节点收到消息后,更新
typedef strut clusterState {
struct clusterNode *slots[16384];
} clusterState;
*slots[ ]数组的值,这样集群中的每个节点就都知道了所有16384个槽的分布情况。
注:
struct clusterNode *nodes[16384];
是一个超大的数组,这是一种空间换时间的做法,可以迅速找到任一槽位所在的节点。
3.3 处理客户端请求命令(重定向):
集群处理客户端请求命令的流程如下:
Step 1: 计算键所属槽位号:
slot_num = CRC16(key) & 16383;
Step 2: 判断槽是否由当前节点负责处理:
伪代码流程:
if(clusterState.slots[slot_num] == clusterState.myself) {
//key所属槽位在本节点上,直接处理
handleCommand();
}
else {
ip = clusterState.slots[slot_num].ip;
port = clusterState.slots[slot_num].ip;
return {(error)MOVED <slot_num> <ip>:<port>};
}
如果客户端请求中的key所属的槽并不在本主节点上,则返回 MOVED
错误,并指示槽所在节点的IP地址和端口号,客户端收到 MOVED
错误信息后,即可根据IP和端口信息重定向到对应的节点。
注:
- 集群模式下的客户端(
./redis-cli -c
)会自动完成重定向,并隐藏MOVED
错误打印,而是直接打印出Redirected to
重定向提示:
127.0.0.1:9001>set key value
-> Reditected to slot[12539] located at 127.0.0.1:9006
OK
- 集群模式下的客户端通常会与集群中的所有节点都建立网络连接,所以重定向无需新建连接,只是换一个套接字来发送命令即可。
3.4 主从复制:
(1) 主从信息维护:
struct clusterNode {
struct clusterNode *slaveof; //当本节点是从节点,*slaveof指向主节点
struct clusterNode **slaves; //当本节点是主节点,**slaves指向正在复制本节点的从节点数组
int numslaves; //当本节点是主节点,numslaves表示正在复制本节点的从节点个数
};
(2) 手动配置主从关系:
//命令:
CLUSTER REPLICATE <node_id>
//举例:
127.0.0.1:9007> CLUSTER REPLICATE bc638929759c1ec5b4d4e748e03f5344fe36ac35
OK
注意在集群模式下不能使用 SLAVEOF
命令去配置主从关系,否则会报错。
3.5 故障检测:
Step 1. PING + PFAIL:
每个节点会按固定频率向集群中其他节点发送 PING 命令,若超时未收到PONG回复,则标记目标节点为疑似下线状态:
clusterNode.flags |= REDIS_NODE_PFAIL;
Step 2. fail_reports + FAIL:
集群各节点间会互相发送消息交换各个节点的状态信息,如果A节点通过消息得知B节点判定C节点进入了疑似下线状态,则A节点会将B节点的下线报告添加到本地 clusterNode结构的fail_reports链表中:
struct clusterNode {
list *fail_reports; //一个链表,记录所有其他节点对该节点的下线报告
};
当A节点发现集群中半数以上的节点都判定C节点进入了疑似下线状态,则A节点更新C节点的状态为 已下线(FAIL),并向集群广播C节点已下线的消息,所有收到FAIL消息的节点都会立即将C结点标记为已下线。
clusterNode.flags |= REDIS_NODE_FAIL;
3.6 故障转移:
当从节点正在复制的主节点已下线(状态变为FAIL),则从节点发起故障转移。
(注:故障转移由已下线主节点的从节点发起
)
故障转移的步骤:
- 在已下线主节点的从节点中选举出一个新的主节点;
- 被选中的从节点执行
SLAVEOF no one
,变为主节点; - 新主节点将旧主节点的槽转移到自己身上;
- 新主节点广播
PONG
消息到集群中的其他节点,通知更新主节点信息; - 新主节点开始处理自己负责的槽有关的请求命令,转移完成。
选举新的主节点的步骤:
Step 1: 从节点发起投票:
当集群中的某个节点判定主节点已下线(超半数判断PFAIL),则更新此主节点状态为FAIL并向集群中的其他节点广播。当已下线主节点属下的从节点发现主节点已下线后,会向集群中广播 CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST
,要求收到此消息的主节点给自己投票;
Step 2: 其余主节点开始投票:
如果主节点具有投票权(正在负责处理槽)且尚未投票,则投给第一个发过来 FAILOVER_AUTH_REQUEST 消息的从节点;
Step 3: 投票结果:
如果从节点收到 (N/2 + 1) 张选票(即超过半数),则这个从节点升级为主节点;
如果没有任何从节点的选票超半数,则集群将纪元值(epoch) + 1,并重新开始一轮选举。
重新分片与ASK错误:
Redis集群可将由某个节点(源节点)负责处理的槽指派给另一个节点(目标节点),并且相关的槽所属的键值对也会从源节点移动到目标节点,这类操作称为 重新分片
。
在进行重新分片的源节点与目标节点间迁移某些槽的过程中,如果客户端访问的键所属的槽正在被迁移,则服务器会返回 ASK错误
,客户端根据ASK错误中携带的IP地址和端口号去重定向访问新的目标节点。(ASK错误与MOVED错误的实现方式类似,区别在于ASK用于数据迁移过程中的重定向)
4. 集群中的消息类型:
Redis节点间通信使用的是Gossip协议。
Gossip协议是一种去中心化的分布式协议,用于实现节点或进程之间的信息交换,通常被用于大型的无中心化网络环境中,并且假设网络环境不太稳定,是分布式系统中被广泛使用的一种最终一致性协议。
关于Gossip协议的介绍:
https://blog.youkuaiyun.com/weixin_44676392/article/details/88053299
集群中节点间收发的消息分为5种:
(1) MEET 消息:
在调用 CLUSTER MEET <ip> <port>
命令后产生MEET消息,使节点加入到 < ip:port > 所在的集群中,并开始与其他节点进行网络通信;
(2) PING 消息:
节点间会频繁的发送PING消息,用于交换自己的状态及集群状态信息,并监测集群中其他节点的在线状态;
(3) PONG 消息:
PONG 是 PING 和 MEET 消息的返回消息,包含自己的状态和其他信息;
也可用于广播和更新;
(4) FAIL 消息:
某个节点判定另一个节点为已下线时,会向集群中的其他节点发送FAIL消息;
(5) PUBLISH 消息:
当节点收到一条PUBLISH命令后,就会向集群PUBLISH消息。
5. 关于集群的命令:
CLUSTER HELP
CLUSTER NODES /* Return cluster configuration seen by node. */
CLUSTER INFO /* Return information about the cluster. */
CLUSTER MEET <ip> <port> /* Connect nodes into a working cluster. */
CLUSTER ADDSLOTS <slot> [slot ...] /* Assign slots to current node. */
CLUSTER SLOTS /* Return information about slots range mappings. */
CLUSTER REPLICATE <node_id> /* Configure current node as replica to <node-id>. */
6. 集群相关的结构体:
clusterNode:
struct clusterNode {
mstime_t ctime; //节点创建时间
char name[REDIS_CLUSTER_NAMELEN]; //节点ID(40位十六进制符)
int flags; //节点标识(例如主/从、在线/疑似下线/下线)
uint64_t configEpoch; //节点当前纪元(用于故障转移)
char ip[REDIS_IP_STR_LEN]; //节点IP地址
int port; //节点端口号
clusterLink *link; //节点连接信息(如实本节点则=NULL)
unsigned char slots[16384/8]; //用16384个bit位表示16384个槽
int numbers; //本节点负责处理的槽数量
//主从信息维护:
struct clusterNode *slaveof; //当本节点是从节点,*slaveof指向主节点
struct clusterNode **slaves; //当本节点是主节点,**slaves指向正在复制本节点的从节点数组
int numslaves; //当本节点是主节点,numslaves表示正在复制本节点的从节点个数
//故障检测:
list *fail_reports; //记录其他节点对该节点的下线报告
};
clusterLink:
typedef struct clusterLink {
mstime_t ctime; //TCP连接的创建时间
int fd; //套接字描述符(connfd)
sds sndbuf; //发送缓冲区(待发送到其他节点的数据)
sds rcvbuf; //接收缓冲区(从其他节点接收到的数据)
struct clusterNode *node; //与此clusterLink关联的结节点
} clusterLink;
clusterState:
typedef sturct clusterState {
clusterNode *myself; //指向自己
uint64_t currentEpoch; //集群当前配置纪元(用于故障转移)
int state; //集群状态(ok/fail)
int size; //集群中至少处理着一个槽的节点的数量
dict *nodes; //集群节点名单
clusterNode *slots[16384]; //记录16384个槽中每个槽被分配到哪个节点上
} clusterState;
参考内容:
《Redis设计与实现》第三部分第17章 集群
Redis官方文档: https://redis.io/topics/cluster-spec