节点¶
一个 Redis 集群通常由多个节点(node)组成, 在刚开始的时候, 每个节点都是相互独立的, 它们都处于一个只包含自己的集群当中, 要组建一个真正可工作的集群, 我们必须将各个独立的节点连接起来, 构成一个包含多个节点的集群。
连接各个节点的工作可以使用 CLUSTER MEET 命令来完成, 该命令的格式如下:
CLUSTER MEET <ip> <port>
向一个节点 node
发送 CLUSTER MEET 命令,
可以让 node
节点与 ip
和 port
所指定的节点进行握手(handshake),
当握手成功时,
node
节点就会将 ip
和 port
所指定的节点添加到 node
节点当前所在的集群中。
举个例子,
假设现在有三个独立的节点 127.0.0.1:7000
、 127.0.0.1:7001
、 127.0.0.1:7002
(下文省略 IP 地址,直接使用端口号来区分各个节点),
我们首先使用客户端连上节点 7000 ,
通过发送 CLUSTER NODE 命令可以看到,
集群目前只包含 7000 自己一个节点:
$ redis-cli -c -p 7000
127.0.0.1:7000> CLUSTER NODES
51549e625cfda318ad27423a31e7476fe3cd2939 :0 myself,master - 0 0 0 connected
通过向节点 7000 发送以下命令, 我们可以将节点 7001 添加到节点 7000 所在的集群里面:
127.0.0.1:7000> CLUSTER MEET 127.0.0.1 7001
OK
127.0.0.1:7000> CLUSTER NODES
68eef66df23420a5862208ef5b1a7005b806f2ff 127.0.0.1:7001 master - 0 1388204746210 0 connected
51549e625cfda318ad27423a31e7476fe3cd2939 :0 myself,master - 0 0 0 connected
继续向节点 7000 发送以下命令, 我们可以将节点 7002 也添加到节点 7000 和节点 7001 所在的集群里面:
127.0.0.1:7000> CLUSTER MEET 127.0.0.1 7002
OK
127.0.0.1:7000> CLUSTER NODES
68eef66df23420a5862208ef5b1a7005b806f2ff 127.0.0.1:7001 master - 0 1388204848376 0 connected
9dfb4c4e016e627d9769e4c9bb0d4fa208e65c26 127.0.0.1:7002 master - 0 1388204847977 0 connected
51549e625cfda318ad27423a31e7476fe3cd2939 :0 myself,master - 0 0 0 connected
现在, 这个集群里面包含了 7000 、 7001 和 7002 三个节点, 图 IMAGE_CONNECT_NODES_1 至 IMAGE_CONNECT_NODES_5 展示了这三个节点进行握手的整个过程。
![digraph {
label = "\n 图 IMAGE_CONNECT_NODES_1 三个独立的节点";
node [shape = circle];
subgraph cluster_a {
label = "集群";
style = dashed;
7000;
}
subgraph cluster_b {
label = "集群";
style = dashed;
7001;
}
subgraph cluster_c {
label = "集群";
style = dashed;
7002;
}
}](../../_images/graphviz-3b61f7ff9d122380790ba6247759c0d37a640015.png)
![digraph {
label = "\n 图 IMAGE_CONNECT_NODES_2 节点 7000 和 7001 进行握手";
rankdir = LR;
node [shape = circle];
subgraph cluster_a {
label = "集群";
style = dashed;
7000;
}
subgraph cluster_b {
label = "集群";
style = dashed;
7001;
}
subgraph cluster_c {
label = "集群";
style = dashed;
7002;
}
7000 -> 7001 [label = "握手"];
7000 -> 7001 [dir = back, label = "响应握手"];
}](../../_images/graphviz-2c11767a7c6b97ae0280630ae1eb88b39e280f9f.png)
![digraph {
label = "\n 图 IMAGE_CONNECT_NODES_3 握手成功的 7000 与 7001 处于同一个集群";
rankdir = LR;
node [shape = circle];
subgraph cluster_a {
label = "集群";
style = dashed;
7000;
7001;
7000 -> 7001 [style = invis];
}
subgraph cluster_c {
label = "集群";
style = dashed;
7002;
}
}](../../_images/graphviz-9ad4d27d1ef45061beeb06d13f1948a906865bb7.png)
![digraph {
label = "\n 图 IMAGE_CONNECT_NODES_4 节点 7000 与节点 7002 进行握手";
rankdir = LR;
node [shape = circle];
subgraph cluster_a {
label = "集群";
style = dashed;
7000;
7001;
7000 -> 7001 [style = invis];
}
subgraph cluster_c {
label = "集群";
style = dashed;
7002;
}
7000 -> 7002 [label = "握手"];
7000 -> 7002 [dir = back, label = "响应握手"];
}](../../_images/graphviz-0c790fac1e7aba8e4cb00b1370c9c216f8828383.png)
![digraph {
label = "\n 图 IMAGE_CONNECT_NODES_5 握手成功的三个节点处于同一个集群";
rankdir = LR;
subgraph cluster_a {
label = "集群";
style = dashed;
node [shape = circle];
7000;
7002;
7001;
edge [style = invis];
7000 -> 7001;
7000 -> 7002;
}
}](../../_images/graphviz-c0042a589d0b8ba28b449093c3ccc8113cd0fa9e.png)
本节接下来的内容将介绍启动节点的方法, 和集群有关的数据结构, 以及 CLUSTER MEET 命令的实现原理。
启动节点¶
一个节点就是一个运行在集群模式下的 Redis 服务器,
Redis 服务器在启动时会根据 cluster-enabled
配置选项的是否为 yes
来决定是否开启服务器的集群模式,
如图 IMAGE_NODE_OR_SERVER 所示。
![digraph {
label = "\n 图 IMAGE_NODE_OR_SERVER 服务器判断是否开启集群模式的过程";
//
node [shape = box];
start [label = "启动服务器", width = 3.3];
cluster_enabled_or_not [label = "cluster-enabled \n选项的值为 yes ?", shape = diamond];
start_node [label = "开启服务器的集群模式\n成为一个节点"];
start_server [label = "开启服务器的单机(stand alone)模式\n成为一个普通 Redis 服务器"];
//
start -> cluster_enabled_or_not;
cluster_enabled_or_not -> start_node [label = "是"];
cluster_enabled_or_not -> start_server [label = "否"];
}](../../_images/graphviz-b89d5a47e3649510a4d72a3d6f186e1dbbe37b47.png)
节点(运行在集群模式下的 Redis 服务器)会继续使用所有在单机模式中使用的服务器组件, 比如说:
节点会继续使用文件事件处理器来处理命令请求和返回命令回复。
节点会继续使用时间事件处理器来执行
serverCron
函数, 而serverCron
函数又会调用集群模式特有的clusterCron
函数:clusterCron
函数负责执行在集群模式下需要执行的常规操作, 比如向集群中的其他节点发送 Gossip 消息, 检查节点是否断线; 又或者检查是否需要对下线节点进行自动故障转移, 等等。节点会继续使用数据库来保存键值对数据,键值对依然会是各种不同类型的对象。
节点会继续使用 RDB 持久化模块和 AOF 持久化模块来执行持久化工作。
节点会继续使用发布与订阅模块来执行 PUBLISH 、 SUBSCRIBE 等命令。
节点会继续使用复制模块来进行节点的复制工作。
节点会继续使用 Lua 脚本环境来执行客户端输入的 Lua 脚本。
诸如此类。
除此之外,
节点会继续使用 redisServer
结构来保存服务器的状态,
使用 redisClient
结构来保存客户端的状态,
至于那些只有在集群模式下才会用到的数据,
节点将它们保存到了 cluster.h/clusterNode
结构,
cluster.h/clusterLink
结构,
以及 cluster.h/clusterState
结构里面,
接下来的一节将对这三种数据结构进行介绍。
集群数据结构¶
clusterNode
结构保存了一个节点的当前状态,
比如节点的创建时间,
节点的名字,
节点当前的配置纪元,
节点的 IP 和地址,
等等。
每个节点都会使用一个 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;
// ...
};
clusterNode
结构的 link
属性是一个 clusterLink
结构,
该结构保存了连接节点所需的有关信息,
比如套接字描述符,
输入缓冲区和输出缓冲区:
typedef struct clusterLink {
// 连接的创建时间
mstime_t ctime;
// TCP 套接字描述符
int fd;
// 输出缓冲区,保存着等待发送给其他节点的消息(message)。
sds sndbuf;
// 输入缓冲区,保存着从其他节点接收到的消息。
sds rcvbuf;
// 与这个连接相关联的节点,如果没有的话就为 NULL
struct clusterNode *node;
} clusterLink;
最后,
每个节点都保存着一个 clusterState
结构,
这个结构记录了在当前节点的视角下,
集群目前所处的状态 ——
比如集群是在线还是下线,
集群包含多少个节点,
集群当前的配置纪元,
诸如此类:
typedef struct clusterState {
// 指向当前节点的指针
clusterNode *myself;
// 集群当前的配置纪元,用于实现故障转移
uint64_t currentEpoch;
// 集群当前的状态:是在线还是下线
int state;
// 集群中至少处理着一个槽的节点的数量
int size;
// 集群节点名单(包括 myself 节点)
// 字典的键为节点的名字,字典的值为节点对应的 clusterNode 结构
dict *nodes;
// ...
} clusterState;
以前面介绍的 7000 、 7001 、 7002 三个节点为例,
图 IMAGE_CLUSTER_STATE_OF_7000 展示了节点 7000 创建的 clusterState
结构,
这个结构从节点 7000 的角度记录了集群、以及集群包含的三个节点的当前状态
(为了空间考虑,图中省略了 clusterNode
结构的一部分属性):
结构的
currentEpoch
属性的值为0
, 表示集群当前的配置纪元为0
。结构的
size
属性的值为0
, 表示集群目前没有任何节点在处理槽: 因此结构的state
属性的值为REDIS_CLUSTER_FAIL
—— 这表示集群目前处于下线状态。结构的
nodes
字典记录了集群目前包含的三个节点, 这三个节点分别由三个clusterNode
结构表示: 其中myself
指针指向代表节点 7000 的clusterNode
结构, 而字典中的另外两个指针则分别指向代表节点 7001 和代表节点 7002 的clusterNode
结构, 这两个节点是节点 7000 已知的在集群中的其他节点。三个节点的
clusterNode
结构的flags
属性都是REDIS_NODE_MASTER
,说明三个节点都是主节点。
节点 7001 和节点 7002 也会创建类似的 clusterState
结构:
不过在节点 7001 创建的
clusterState
结构中,myself
指针将指向代表节点 7001 的clusterNode
结构, 而节点 7000 和节点 7002 则是集群中的其他节点。而在节点 7002 创建的
clusterState
结构中,myself
指针将指向代表节点 7002 的clusterNode
结构, 而节点 7000 和节点 7001 则是集群中的其他节点。
![digraph {
label = "\n 图 IMAGE_CLUSTER_STATE_OF_7000 节点 7000 创建的 clusterState 结构";
rankdir = LR;
//
node [shape = record];
clusterState [label = " <head> clusterState | <myself> myself | currentEpoch \n 0 | state \n REDIS_CLUSTER_FAIL | size \n 0 | <nodes> nodes | ... "];
nodes [label = " <head> nodes | <0> \"5154...2939\" | <1> \"68ee...f2ff\" | <2> \"9dfb...5c26\" "];
node7000 [label = " <head> clusterNode | name \n \"5154...2939\" | flags \n REDIS_NODE_MASTER | configEpoch \n 0 | ip \n \"127.0.0.1\" | port \n 7000 | ... "];
node7001 [label = " <head> clusterNode | name \n \"68ee...f2ff\"| flags \n REDIS_NODE_MASTER | configEpoch \n 0 | ip \n \"127.0.0.1\" | port \n 7001 | ... "];
node7002 [label = " <head> clusterNode | name \n \"9dfb...5c26\"| flags \n REDIS_NODE_MASTER | configEpoch \n 0 | ip \n \"127.0.0.1\" | port \n 7002 | ... "];
//link7000 [label = " <head> clusterLink | ctime | fd | sndbuf | rcvbuf "];
clusterState:myself -> node7000:head;
clusterState:nodes -> nodes:head;
nodes:0 -> node7000:head;
nodes:1 -> node7001:head;
nodes:2 -> node7002:head;
}](../../_images/graphviz-5853dea00be16ee11bcef311bd37ec5e9daf7e4b.png)
CLUSTER MEET 命令的实现¶
通过向节点 A 发送 CLUSTER MEET 命令, 客户端可以让接收命令的节点 A 将另一个节点 B 添加到节点 A 当前所在的集群里面:
CLUSTER MEET <ip> <port>
收到命令的节点 A 将与节点 B 进行握手(handshake), 以此来确认彼此的存在, 并为将来的进一步通信打好基础:
节点 A 会为节点 B 创建一个
clusterNode
结构, 并将该结构添加到自己的clusterState.nodes
字典里面。之后, 节点 A 将根据 CLUSTER MEET 命令给定的 IP 地址和端口号, 向节点 B 发送一条
MEET
消息(message)。如果一切顺利, 节点 B 将接收到节点 A 发送的
MEET
消息, 节点 B 会为节点 A 创建一个clusterNode
结构, 并将该结构添加到自己的clusterState.nodes
字典里面。之后, 节点 B 将向节点 A 返回一条
PONG
消息。如果一切顺利, 节点 A 将接收到节点 B 返回的
PONG
消息, 通过这条PONG
消息节点 A 可以知道节点 B 已经成功地接收到了自己发送的MEET
消息。之后, 节点 A 将向节点 B 返回一条
PING
消息。如果一切顺利, 节点 B 将接收到节点 A 返回的
PING
消息, 通过这条PING
消息节点 B 可以知道节点 A 已经成功地接收到了自己返回的PONG
消息, 握手完成。
图 IMAGE_HANDSHAKE 展示了以上步骤描述的握手过程。
![digraph {
label = "\n 图 IMAGE_HANDSHAKE 节点的握手过程";
rankdir = LR;
splines = ortho;
//
node [shape = box, height = 2];
client [label = "客\n户\n端"];
A [label = "节\n点\nA"];
B [label = "节\n点\nB"];
//
client -> A [label = "发送命令 \n CLUSTER MEET \n <B_ip> <B_port>"];
A -> B [label = "发送 MEET 消息"];
A -> B [dir = back, label = "\n返回 PONG 消息"];
A -> B [label = "\n返回 PING 消息"];
}](../../_images/graphviz-d6c65faae9f96bc02ec725d3af9f698e8e9b1bdd.png)
之后, 节点 A 会将节点 B 的信息通过 Gossip 协议传播给集群中的其他节点, 让其他节点也与节点 B 进行握手, 最终, 经过一段时间之后, 节点 B 会被集群中的所有节点认识。