目录
- 节点的数据结构
- 槽指派及重新分片
- MOVE错误及ASK错误
- 复制与故障转移
节点的数据结构
一个Redis集群有多个节点组成,每个节点的数据结构是一个clusterNode:
struct clusterNode{
//创建节点时间
mstime_t ctime;
//节点名称
char name[REDIS_CLUSTER_NAME];
//节点的状态及集群角色
int flat;
//配置纪元,用于故障转移
unit64_t configEpoch;
//连接节点所需信息
clusterLink *link;
//节点状态
clusterState *state
}
其中clusterLink保存连接节点所需要信息,如套接字描述符,输入缓存区和输出缓存区:
typedef struct clusterLink{
//连接的创建时间
mstime_t ctime;
//TCP套接字描述符
int fd;
//输出缓存区
sds sndbuf;
//输入缓存区
sds rcvbuf;
//与这个连接相连接的节点
struct clusterNode *node;
}clusterLink;
clusterNode的clusterState结构保存着在当前节点的视角下,集群目前所处的状态。
typedef struct clusterState{
//指向当前节点的指针
clusterNode *myself;
//配置纪元,用于故障转移
unit64_t configEpoch;
//集群当前的状态:是在线还是下线
int state;
//集群规模
int size;
//集群节点名单
dict *nodes;
}clusterState;
其中myself指向自己的clusterNode结构,nodes保存着集群中所有节点的信息。
节点的数据结构可以用图来表示出来:
2. 槽指派及重新分片
槽指派
Redis集群通过分片的方式来保存数据库中的键值对,集群的整个数据库被分成16384个槽,数据库中的每个键都属于着16384个槽的其中一个。
如果16384个槽都有节点处理,则集群处于上线状态,否则处于下线状态。
下面是常见的几个命令:
# 将0-5000槽分派给7000端口的redis节点
127.0.0.1:7000 > CLUSTER ADDSLOTS 0 1 2 3 ... 5000
# 查看所有节点的信息及其负责的槽
127.0.0.1:7000 > CLUSTER NODES
# 计算键属于哪个槽
127.0.0.1:7000> CLUSTER KEYSLOT "haha"
(integer) 2022
每个节点的clusterNode结构记录着那些节点处理哪写槽:
struct clusterNode{
unsigned char slots[16384/8];
int numslots;
}
numslots记录着cluterNode负责处理的槽数量,slots数组是一个二进制位数组,记录着每个槽的信息,用1代表节点处理这个槽,用0代表节点不处理这个槽。除以8是因为一个char有8个bit。
从上面的节点的数据结构,我们知道每个节点用clusterState来记录集群的状态,clusterState结构中的slots数组会记录集群中所有槽的指派信息。
typedef struct clusterState{
clusterNode *slots[16384];
}
为什么要用clusterState来记录所有槽的信息而不是通过遍历clusterState的nodes,遍历其中每个节点的slots数组来获取槽的信息呢?答案很简单,通过clusterState.slots[i] 我们就能直接得到节点信息,复杂度为o(1)
因此节点与槽的关系可以用下图来表示:
重新分片
Redis集群的重新分片操作可以将任意数量已经指派给某个节点的槽改为指派给另一个节点,并且相关槽所属的键值对也会从源节点被移动到目标节点。
分片操作由Reidis集群管理软件redis-trib负责执行,步骤如下:
3. MOVE错误及ASK错误
MOVE错误
当节点发现键所在的槽并非由自己负责处理的时候,节点就会向客户端返回一个MOVED错误,指引客户端转向正在负责槽的节点。
例子:向节点7000得到一个槽,但是这个槽在7001中。
ASK错误
clusterNode有两个特殊的数组,importing_slots_from记录着当前节点从其他节点导入的槽,用migrating_slots_to数组记录着当前节点正在迁移至其他节点的槽。
typedef struct clusterState{
clusterNode *importing_slots_from;
clusterNode * migrating_slots_to;
}
如果节点收到一个key的命令请求,这个节点先会尝试在自己的数据库中查找key,如果找到就直接执行客户端发送的命令;如果没有找到,那么会检查clusterState.migrating_slots_to[i],查看key所属的槽i时候正在迁移,如果正在迁移的话,那么节点会客户端发送一个ASK错误,引导客户端到正在导入槽i的节点去查找键key。
步骤:
所以MOVE错误代表槽的负责权已经由一个节点转移到另一个节点,每次客户端遇到槽i的命令请求都可以直接发送给MOVED错误所指向的节点。ASK错误只是在迁移槽过程中的一种临时措施,不会对客户端今后发送关于槽i的命令请求产生任何影响。
4. 复制与故障转移
首先用一幅图了解复制与故障转移的概念:
设置从节点
CLUSTER REPLICATE <node_id>
首先接收到命令的节点首先在自己的clusterState.nodes找到node_id所对应的节点的clusterNode结构,并将自己的clusterState.myself.slaveof指向这个结构。
节点修改自己在culsterState.myself.flags的属性,关闭原来的REDIS_NODE_MASTER标识,设置为REDIS_NODE_SLAVE标识,表示这个节点由原来的主节点变成了从节点。
用clusterState.myself.slaveof指向的clusterNode结构中的IP地址和端口号,对主节点进行复制。
### 故障检测
集群中的每个节点都会定期地向其他节点发送一条PING命令,如果在规定的时间内,接受PING消息消息的节点没有向发送PING的节点发送PONG消息,那么发送PING消息的节点将会将接收PING消息的节点设置成疑似下线。
其中clusterNode用它的fail_report链表来更新节点的下线状态:
struct clusterNode{
//一个链表,来记录所有其他节点对该节点的下线报告
list *fail_reports;
}
每个下线报告由一个clusterNodeFailReport表示
struct clusterNodeFailReport{
//报告下线的节点
struct clusterNode *node;
//最后一次从node节点收到下线报告的时间
mstime_t time;
}
过程:当一个主节点A通过消息得知主节点B和C认为D进入了疑似下线状态时,主节点A会在自己的clusterState.nodes字典中找到主节点D所对应的clusterNode结构,并将主节点B和C的下线报告添加到clusterNode结构的fail_reports链表中。
如果在一个集群里面,半数以上负责处理槽的主节点将某个主节点x报告为疑似下线(PFAIL),那么这个主节点x将会被标记为已下线(FAIL),然后这个将主节点x标记为已下线的的节点会向集群广播一条关于主节点x的FAIL消息,然后所有的收到FAIL消息的节点会立即将主节点x标记为已下线。
### 选举主节点
这个选举方法与Sentinel的选举都是基于Raft算法,可以参考Sentinel的选举:
每个Sentinel都有成为领头的能力,而且每次选举无论是否成功,都会将配置纪元(confuguration epoch)的值自增,它实际上就是一个计数器。
局部领头:当一个Sentinel A向另一个Sentinel B发送请求SENTINEL is-master-down-by-addr + (Sentinel A 的 runid )代表A想成为B的局部领头。
所以这种规则就是先到先得,最早向目标Sentinel发送这个命令的必然成为它的Sentinel,后面的命令都会无效,当它的票数超过半数时,它就成为领头Sentinel,然后对已经下线的主服务器执行故障转移操作。
故障转移
- 首先在复制下线主节点的所有从节点里面,选举一个主节点。
- 被选中的主节点执行SLAVEOF no one,成为新的主节点。
- 新的主节点会撤销所有对已下线主节点的槽指派,并将这些槽全部指派给自己。
- 新的主节点向集群广播一条PONG消息,这条PING消息可以让集群中的其他节点立即知道这个节点已经由从节点变成了主节点,并且这个主节点已经接管了原本由已显现节点负责处理的槽。
- 新的节点开始接收和自己负责处理的槽有关的命令请求,故障转移完成。